diff --git a/logs/error.log b/logs/error.log index 707319f..14c6c2e 100644 --- a/logs/error.log +++ b/logs/error.log @@ -83,3 +83,34 @@ [2025-10-16 00:06:02 +0300] [288324] [INFO] Worker exiting (pid: 288324) [2025-10-16 00:06:02 +0300] [288325] [INFO] Worker exiting (pid: 288325) [2025-10-16 00:06:03 +0300] [288316] [INFO] Shutting down: Master +[2025-10-16 02:34:31 +0300] [299414] [INFO] Starting gunicorn 23.0.0 +[2025-10-16 02:34:31 +0300] [299414] [INFO] Listening at: http://0.0.0.0:8781 (299414) +[2025-10-16 02:34:31 +0300] [299414] [INFO] Using worker: sync +[2025-10-16 02:34:31 +0300] [299414] [INFO] Trasabilitate Application server is ready. Listening on: [('0.0.0.0', 8781)] +[2025-10-16 02:34:31 +0300] [299414] [INFO] Worker spawned (pid: [booting]) +[2025-10-16 02:34:31 +0300] [299432] [INFO] Booting worker with pid: 299432 +[2025-10-16 02:34:31 +0300] [299432] [INFO] Worker spawned (pid: 299432) +[2025-10-16 02:34:31 +0300] [299414] [INFO] Worker spawned (pid: [booting]) +[2025-10-16 02:34:31 +0300] [299438] [INFO] Booting worker with pid: 299438 +[2025-10-16 02:34:31 +0300] [299438] [INFO] Worker spawned (pid: 299438) +[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting]) +[2025-10-16 02:34:32 +0300] [299439] [INFO] Booting worker with pid: 299439 +[2025-10-16 02:34:32 +0300] [299439] [INFO] Worker spawned (pid: 299439) +[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting]) +[2025-10-16 02:34:32 +0300] [299440] [INFO] Booting worker with pid: 299440 +[2025-10-16 02:34:32 +0300] [299440] [INFO] Worker spawned (pid: 299440) +[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting]) +[2025-10-16 02:34:32 +0300] [299441] [INFO] Booting worker with pid: 299441 +[2025-10-16 02:34:32 +0300] [299441] [INFO] Worker spawned (pid: 299441) +[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting]) +[2025-10-16 02:34:32 +0300] [299442] [INFO] Booting worker with pid: 299442 +[2025-10-16 02:34:32 +0300] [299442] [INFO] Worker spawned (pid: 299442) +[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting]) +[2025-10-16 02:34:32 +0300] [299443] [INFO] Booting worker with pid: 299443 +[2025-10-16 02:34:32 +0300] [299443] [INFO] Worker spawned (pid: 299443) +[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting]) +[2025-10-16 02:34:32 +0300] [299444] [INFO] Booting worker with pid: 299444 +[2025-10-16 02:34:32 +0300] [299444] [INFO] Worker spawned (pid: 299444) +[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting]) +[2025-10-16 02:34:32 +0300] [299445] [INFO] Booting worker with pid: 299445 +[2025-10-16 02:34:32 +0300] [299445] [INFO] Worker spawned (pid: 299445) diff --git a/py_app/CSS_MODULAR_GUIDE.md b/old code/CSS_MODULAR_GUIDE.md similarity index 100% rename from py_app/CSS_MODULAR_GUIDE.md rename to old code/CSS_MODULAR_GUIDE.md diff --git a/py_app/app/backup_db_scripts/add_email_column.py b/old code/backup_db_scripts/add_email_column.py similarity index 100% rename from py_app/app/backup_db_scripts/add_email_column.py rename to old code/backup_db_scripts/add_email_column.py diff --git a/py_app/app/backup_db_scripts/check_external_db_users.py b/old code/backup_db_scripts/check_external_db_users.py similarity index 100% rename from py_app/app/backup_db_scripts/check_external_db_users.py rename to old code/backup_db_scripts/check_external_db_users.py diff --git a/py_app/app/backup_db_scripts/create_external_superadmin.py b/old code/backup_db_scripts/create_external_superadmin.py similarity index 100% rename from py_app/app/backup_db_scripts/create_external_superadmin.py rename to old code/backup_db_scripts/create_external_superadmin.py diff --git a/py_app/app/backup_db_scripts/create_order_for_labels_table.py b/old code/backup_db_scripts/create_order_for_labels_table.py similarity index 100% rename from py_app/app/backup_db_scripts/create_order_for_labels_table.py rename to old code/backup_db_scripts/create_order_for_labels_table.py diff --git a/py_app/app/backup_db_scripts/create_permissions_tables.py b/old code/backup_db_scripts/create_permissions_tables.py similarity index 100% rename from py_app/app/backup_db_scripts/create_permissions_tables.py rename to old code/backup_db_scripts/create_permissions_tables.py diff --git a/py_app/app/backup_db_scripts/create_roles_table.py b/old code/backup_db_scripts/create_roles_table.py similarity index 100% rename from py_app/app/backup_db_scripts/create_roles_table.py rename to old code/backup_db_scripts/create_roles_table.py diff --git a/py_app/app/backup_db_scripts/create_scan_1db.py b/old code/backup_db_scripts/create_scan_1db.py similarity index 100% rename from py_app/app/backup_db_scripts/create_scan_1db.py rename to old code/backup_db_scripts/create_scan_1db.py diff --git a/py_app/app/backup_db_scripts/create_scanfg_orders.py b/old code/backup_db_scripts/create_scanfg_orders.py similarity index 100% rename from py_app/app/backup_db_scripts/create_scanfg_orders.py rename to old code/backup_db_scripts/create_scanfg_orders.py diff --git a/py_app/app/backup_db_scripts/create_triggers.py b/old code/backup_db_scripts/create_triggers.py similarity index 100% rename from py_app/app/backup_db_scripts/create_triggers.py rename to old code/backup_db_scripts/create_triggers.py diff --git a/py_app/app/backup_db_scripts/create_triggers_fg.py b/old code/backup_db_scripts/create_triggers_fg.py similarity index 100% rename from py_app/app/backup_db_scripts/create_triggers_fg.py rename to old code/backup_db_scripts/create_triggers_fg.py diff --git a/py_app/app/backup_db_scripts/create_warehouse_locations_table.py b/old code/backup_db_scripts/create_warehouse_locations_table.py similarity index 100% rename from py_app/app/backup_db_scripts/create_warehouse_locations_table.py rename to old code/backup_db_scripts/create_warehouse_locations_table.py diff --git a/py_app/app/backup_db_scripts/delet scan1_orders values.py b/old code/backup_db_scripts/delet scan1_orders values.py similarity index 100% rename from py_app/app/backup_db_scripts/delet scan1_orders values.py rename to old code/backup_db_scripts/delet scan1_orders values.py diff --git a/py_app/app/backup_db_scripts/drop_external_users_roles_tables.py b/old code/backup_db_scripts/drop_external_users_roles_tables.py similarity index 100% rename from py_app/app/backup_db_scripts/drop_external_users_roles_tables.py rename to old code/backup_db_scripts/drop_external_users_roles_tables.py diff --git a/py_app/app/backup_db_scripts/find_users_databases.py b/old code/backup_db_scripts/find_users_databases.py similarity index 100% rename from py_app/app/backup_db_scripts/find_users_databases.py rename to old code/backup_db_scripts/find_users_databases.py diff --git a/py_app/app/backup_db_scripts/populate_permissions.py b/old code/backup_db_scripts/populate_permissions.py similarity index 100% rename from py_app/app/backup_db_scripts/populate_permissions.py rename to old code/backup_db_scripts/populate_permissions.py diff --git a/py_app/app/backup_db_scripts/print_internal_users.py b/old code/backup_db_scripts/print_internal_users.py similarity index 100% rename from py_app/app/backup_db_scripts/print_internal_users.py rename to old code/backup_db_scripts/print_internal_users.py diff --git a/py_app/app/backup_db_scripts/query.py b/old code/backup_db_scripts/query.py similarity index 100% rename from py_app/app/backup_db_scripts/query.py rename to old code/backup_db_scripts/query.py diff --git a/py_app/app/backup_db_scripts/recreate_order_for_labels_table.py b/old code/backup_db_scripts/recreate_order_for_labels_table.py similarity index 100% rename from py_app/app/backup_db_scripts/recreate_order_for_labels_table.py rename to old code/backup_db_scripts/recreate_order_for_labels_table.py diff --git a/py_app/app/backup_db_scripts/seed_internal_superadmin.py b/old code/backup_db_scripts/seed_internal_superadmin.py similarity index 100% rename from py_app/app/backup_db_scripts/seed_internal_superadmin.py rename to old code/backup_db_scripts/seed_internal_superadmin.py diff --git a/py_app/app/backup_db_scripts/test.py b/old code/backup_db_scripts/test.py similarity index 100% rename from py_app/app/backup_db_scripts/test.py rename to old code/backup_db_scripts/test.py diff --git a/py_app/app/static/documentation/INSTALLATION_GUIDE.md b/old code/documentation/INSTALLATION_GUIDE.md similarity index 100% rename from py_app/app/static/documentation/INSTALLATION_GUIDE.md rename to old code/documentation/INSTALLATION_GUIDE.md diff --git a/py_app/app/static/documentation/QUICK_SETUP.md b/old code/documentation/QUICK_SETUP.md similarity index 100% rename from py_app/app/static/documentation/QUICK_SETUP.md rename to old code/documentation/QUICK_SETUP.md diff --git a/py_app/app/static/documentation/README.md b/old code/documentation/README.md similarity index 100% rename from py_app/app/static/documentation/README.md rename to old code/documentation/README.md diff --git a/old code/migrate_external_db.py b/old code/migrate_external_db.py new file mode 100644 index 0000000..28afeff --- /dev/null +++ b/old code/migrate_external_db.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +Script to add modules column to external database and migrate existing users +""" + +import os +import sys +import mariadb + +def migrate_external_database(): + """Add modules column to external database and update existing users""" + try: + # Read external database configuration from instance folder + config_file = os.path.join(os.path.dirname(__file__), 'instance/external_server.conf') + if not os.path.exists(config_file): + print("External database configuration file not found at instance/external_server.conf") + return False + + with open(config_file, 'r') as f: + lines = f.read().strip().split('\n') + + # Parse the config file format "key=value" + config = {} + for line in lines: + if '=' in line and not line.strip().startswith('#'): + key, value = line.split('=', 1) + config[key.strip()] = value.strip() + + host = config.get('server_domain', 'localhost') + port = int(config.get('port', '3306')) + database = config.get('database_name', '') + user = config.get('username', '') + password = config.get('password', '') + + if not all([host, database, user, password]): + print("Missing required database configuration values.") + return False + + print(f"Connecting to external database: {host}:{port}/{database}") + + # Connect to external database + conn = mariadb.connect( + user=user, + password=password, + host=host, + port=port, + database=database + ) + cursor = conn.cursor() + + # Check if users table exists + cursor.execute("SHOW TABLES LIKE 'users'") + if not cursor.fetchone(): + print("Users table not found in external database.") + conn.close() + return False + + # Check if modules column already exists + cursor.execute("DESCRIBE users") + columns = [row[0] for row in cursor.fetchall()] + + if 'modules' not in columns: + print("Adding modules column to users table...") + cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT") + print("Modules column added successfully.") + else: + print("Modules column already exists.") + + # Get current users and convert their roles + cursor.execute("SELECT id, username, role FROM users") + users = cursor.fetchall() + + role_mapping = { + 'superadmin': ('superadmin', None), + 'administrator': ('admin', None), + 'admin': ('admin', None), + 'quality': ('manager', '["quality"]'), + 'warehouse': ('manager', '["warehouse"]'), + 'warehouse_manager': ('manager', '["warehouse"]'), + 'scan': ('worker', '["quality"]'), + 'etichete': ('manager', '["labels"]'), + 'quality_manager': ('manager', '["quality"]'), + 'quality_worker': ('worker', '["quality"]'), + } + + print(f"Migrating {len(users)} users...") + + for user_id, username, old_role in users: + if old_role in role_mapping: + new_role, modules_json = role_mapping[old_role] + + cursor.execute("UPDATE users SET role = ?, modules = ? WHERE id = ?", + (new_role, modules_json, user_id)) + + print(f" {username}: {old_role} -> {new_role} with modules {modules_json}") + else: + print(f" {username}: Unknown role '{old_role}', keeping as-is") + + conn.commit() + conn.close() + + print("External database migration completed successfully!") + return True + + except Exception as e: + print(f"Error migrating external database: {e}") + return False + +if __name__ == "__main__": + print("External Database Migration for Simplified 4-Tier Permission System") + print("=" * 70) + + success = migrate_external_database() + + if success: + print("\n✅ Migration completed successfully!") + print("\nUsers can now log in with the new simplified permission system.") + print("Role structure: superadmin → admin → manager → worker") + print("Modules: quality, warehouse, labels") + else: + print("\n❌ Migration failed. Please check the error messages above.") \ No newline at end of file diff --git a/old code/migrate_permissions.py b/old code/migrate_permissions.py new file mode 100755 index 0000000..ad39d86 --- /dev/null +++ b/old code/migrate_permissions.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +""" +Migration script to convert from complex permission system to simplified 4-tier system +This script will: +1. Add 'modules' column to users table +2. Convert existing roles to new 4-tier system +3. Assign appropriate modules based on old roles +""" + +import sqlite3 +import json +import os +import sys + +# Add the app directory to Python path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) + +def get_db_connections(): + """Get both internal SQLite and external database connections""" + connections = {} + + # Internal SQLite database + internal_db_path = os.path.join(os.path.dirname(__file__), 'instance/users.db') + if os.path.exists(internal_db_path): + connections['internal'] = sqlite3.connect(internal_db_path) + print(f"Connected to internal SQLite database: {internal_db_path}") + + # External database (try to connect using existing method) + try: + import mariadb + + # Read external database configuration + config_file = os.path.join(os.path.dirname(__file__), '../external_database_settings') + if os.path.exists(config_file): + with open(config_file, 'r') as f: + lines = f.read().strip().split('\n') + if len(lines) >= 5: + host = lines[0].strip() + port = int(lines[1].strip()) + database = lines[2].strip() + user = lines[3].strip() + password = lines[4].strip() + + conn = mariadb.connect( + user=user, + password=password, + host=host, + port=port, + database=database + ) + connections['external'] = conn + print(f"Connected to external MariaDB database: {host}:{port}/{database}") + except Exception as e: + print(f"Could not connect to external database: {e}") + + return connections + +def role_mapping(): + """Map old roles to new 4-tier system""" + return { + # Old role -> (new_role, modules) + 'superadmin': ('superadmin', []), # All modules by default + 'administrator': ('admin', []), # All modules by default + 'admin': ('admin', []), # All modules by default + 'quality': ('manager', ['quality']), + 'warehouse': ('manager', ['warehouse']), + 'warehouse_manager': ('manager', ['warehouse']), + 'scan': ('worker', ['quality']), # Assume scan users are quality workers + 'etichete': ('manager', ['labels']), + 'quality_manager': ('manager', ['quality']), + 'quality_worker': ('worker', ['quality']), + } + +def migrate_database(conn, db_type): + """Migrate a specific database""" + cursor = conn.cursor() + + print(f"Migrating {db_type} database...") + + # Check if users table exists + if db_type == 'internal': + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'") + else: # external/MariaDB + cursor.execute("SHOW TABLES LIKE 'users'") + + if not cursor.fetchone(): + print(f"No users table found in {db_type} database") + return + + # Check if modules column already exists + try: + if db_type == 'internal': + cursor.execute("PRAGMA table_info(users)") + columns = [row[1] for row in cursor.fetchall()] + else: # external/MariaDB + cursor.execute("DESCRIBE users") + columns = [row[0] for row in cursor.fetchall()] + + if 'modules' not in columns: + print(f"Adding modules column to {db_type} database...") + if db_type == 'internal': + cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT") + else: # external/MariaDB + cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT") + else: + print(f"Modules column already exists in {db_type} database") + + except Exception as e: + print(f"Error checking/adding modules column in {db_type}: {e}") + return + + # Get current users + cursor.execute("SELECT id, username, role FROM users") + users = cursor.fetchall() + + print(f"Found {len(users)} users in {db_type} database") + + # Convert roles and assign modules + mapping = role_mapping() + updates = [] + + for user_id, username, old_role in users: + if old_role in mapping: + new_role, modules = mapping[old_role] + modules_json = json.dumps(modules) if modules else None + updates.append((new_role, modules_json, user_id, username)) + print(f" {username}: {old_role} -> {new_role} with modules {modules}") + else: + print(f" {username}: Unknown role '{old_role}', keeping as-is") + + # Apply updates + for new_role, modules_json, user_id, username in updates: + try: + cursor.execute("UPDATE users SET role = ?, modules = ? WHERE id = ?", + (new_role, modules_json, user_id)) + print(f" Updated {username} successfully") + except Exception as e: + print(f" Error updating {username}: {e}") + + conn.commit() + print(f"Migration completed for {db_type} database") + +def main(): + """Main migration function""" + print("Starting migration to simplified 4-tier permission system...") + print("="*60) + + connections = get_db_connections() + + if not connections: + print("No database connections available. Please check your configuration.") + return + + for db_type, conn in connections.items(): + try: + migrate_database(conn, db_type) + print() + except Exception as e: + print(f"Error migrating {db_type} database: {e}") + finally: + conn.close() + + print("Migration completed!") + print("\nNew role structure:") + print("- superadmin: Full system access") + print("- admin: Full app access (except role_permissions and download_extension)") + print("- manager: Module-based access (can have multiple modules)") + print("- worker: Limited module access (one module only)") + print("\nAvailable modules: quality, warehouse, labels") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/py_app/app/static/qz-tray-PATCH-NOTES.txt b/old code/qz-tray-PATCH-NOTES.txt similarity index 100% rename from py_app/app/static/qz-tray-PATCH-NOTES.txt rename to old code/qz-tray-PATCH-NOTES.txt diff --git a/py_app/app/templates/role_permissions.html b/old code/role_permissions.html similarity index 100% rename from py_app/app/templates/role_permissions.html rename to old code/role_permissions.html diff --git a/old code/test_permissions.py b/old code/test_permissions.py new file mode 100644 index 0000000..4753243 --- /dev/null +++ b/old code/test_permissions.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +Test script for the new simplified 4-tier permission system +""" + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app')) + +from permissions_simple import check_access, validate_user_modules, get_user_accessible_pages + +def test_permission_system(): + """Test the new permission system with various scenarios""" + print("Testing Simplified 4-Tier Permission System") + print("=" * 50) + + # Test cases: (role, modules, page, expected_result) + test_cases = [ + # Superadmin tests + ('superadmin', [], 'dashboard', True), + ('superadmin', [], 'role_permissions', True), + ('superadmin', [], 'quality', True), + ('superadmin', [], 'warehouse', True), + + # Admin tests + ('admin', [], 'dashboard', True), + ('admin', [], 'role_permissions', False), # Restricted for admin + ('admin', [], 'download_extension', False), # Restricted for admin + ('admin', [], 'quality', True), + ('admin', [], 'warehouse', True), + + # Manager tests + ('manager', ['quality'], 'quality', True), + ('manager', ['quality'], 'quality_reports', True), + ('manager', ['quality'], 'warehouse', False), # No warehouse module + ('manager', ['warehouse'], 'warehouse', True), + ('manager', ['warehouse'], 'quality', False), # No quality module + ('manager', ['quality', 'warehouse'], 'quality', True), # Multiple modules + ('manager', ['quality', 'warehouse'], 'warehouse', True), + + # Worker tests + ('worker', ['quality'], 'quality', True), + ('worker', ['quality'], 'quality_reports', False), # Workers can't access reports + ('worker', ['quality'], 'warehouse', False), # No warehouse module + ('worker', ['warehouse'], 'move_orders', True), + ('worker', ['warehouse'], 'create_locations', False), # Workers can't create locations + + # Invalid role test + ('invalid_role', ['quality'], 'quality', False), + ] + + print("Testing access control:") + print("-" * 30) + + passed = 0 + failed = 0 + + for role, modules, page, expected in test_cases: + result = check_access(role, modules, page) + status = "PASS" if result == expected else "FAIL" + print(f"{status}: {role:12} {str(modules):20} {page:18} -> {result} (expected {expected})") + + if result == expected: + passed += 1 + else: + failed += 1 + + print(f"\nResults: {passed} passed, {failed} failed") + + # Test module validation + print("\nTesting module validation:") + print("-" * 30) + + validation_tests = [ + ('superadmin', ['quality'], True), # Superadmin can have any modules + ('admin', ['warehouse'], True), # Admin can have any modules + ('manager', ['quality'], True), # Manager can have one module + ('manager', ['quality', 'warehouse'], True), # Manager can have multiple modules + ('manager', [], False), # Manager must have at least one module + ('worker', ['quality'], True), # Worker can have one module + ('worker', ['quality', 'warehouse'], False), # Worker cannot have multiple modules + ('worker', [], False), # Worker must have exactly one module + ('invalid_role', ['quality'], False), # Invalid role + ] + + for role, modules, expected in validation_tests: + is_valid, error_msg = validate_user_modules(role, modules) + status = "PASS" if is_valid == expected else "FAIL" + print(f"{status}: {role:12} {str(modules):20} -> {is_valid} (expected {expected})") + if error_msg: + print(f" Error: {error_msg}") + + # Test accessible pages for different users + print("\nTesting accessible pages:") + print("-" * 30) + + user_tests = [ + ('superadmin', []), + ('admin', []), + ('manager', ['quality']), + ('manager', ['warehouse']), + ('worker', ['quality']), + ('worker', ['warehouse']), + ] + + for role, modules in user_tests: + accessible_pages = get_user_accessible_pages(role, modules) + print(f"{role:12} {str(modules):20} -> {len(accessible_pages)} pages: {', '.join(accessible_pages[:5])}{'...' if len(accessible_pages) > 5 else ''}") + +if __name__ == "__main__": + test_permission_system() \ No newline at end of file diff --git a/old code/tray/src/qz/communication/H4J_HidUtilities.java b/old code/tray/src/qz/communication/H4J_HidUtilities.java new file mode 100755 index 0000000..de3dc82 --- /dev/null +++ b/old code/tray/src/qz/communication/H4J_HidUtilities.java @@ -0,0 +1,65 @@ +package qz.communication; + + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.hid4java.HidDevice; +import org.hid4java.HidManager; +import org.hid4java.HidServices; + +import javax.usb.util.UsbUtil; +import java.util.HashSet; +import java.util.List; + +public class H4J_HidUtilities { + private static final HidServices service = HidManager.getHidServices(); + + public static List getHidDevices() { + return service.getAttachedHidDevices(); + } + + public static JSONArray getHidDevicesJSON() throws JSONException { + List devices = getHidDevices(); + JSONArray devicesJSON = new JSONArray(); + + HashSet unique = new HashSet<>(); + for(HidDevice device : devices) { + JSONObject deviceJSON = new JSONObject(); + + deviceJSON.put("vendorId", UsbUtil.toHexString(device.getVendorId())) + .put("productId", UsbUtil.toHexString(device.getProductId())) + .put("usagePage", UsbUtil.toHexString((short)device.getUsagePage())) + .put("serial", device.getSerialNumber()) + .put("manufacturer", device.getManufacturer()) + .put("product", device.getProduct()); + + String uid = String.format("v%sp%su%ss%s", deviceJSON.optString("vendorId"), deviceJSON.optString("productId"), deviceJSON.optString("usagePage"), deviceJSON.optString("serial")); + if (!unique.contains(uid)) { + devicesJSON.put(deviceJSON); + unique.add(uid); + } + } + + return devicesJSON; + } + + public static HidDevice findDevice(DeviceOptions dOpts) { + if (dOpts.getVendorId() == null) { + throw new IllegalArgumentException("Vendor ID cannot be null"); + } + if (dOpts.getProductId() == null) { + throw new IllegalArgumentException("Product ID cannot be null"); + } + + List devices = getHidDevices(); + for(HidDevice device : devices) { + if (device.isVidPidSerial(dOpts.getVendorId(), dOpts.getProductId(), dOpts.getSerial()) + && (dOpts.getUsagePage() == null || dOpts.getUsagePage() == device.getUsagePage())) { + return device; + } + } + + return null; + } +} diff --git a/old code/tray/src/qz/communication/PJHA_HidIO.java b/old code/tray/src/qz/communication/PJHA_HidIO.java new file mode 100755 index 0000000..60ce9a6 --- /dev/null +++ b/old code/tray/src/qz/communication/PJHA_HidIO.java @@ -0,0 +1,154 @@ +package qz.communication; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import purejavahidapi.HidDevice; +import purejavahidapi.HidDeviceInfo; +import purejavahidapi.InputReportListener; +import purejavahidapi.PureJavaHidApi; +import qz.utils.SystemUtilities; +import qz.ws.SocketConnection; + +import javax.usb.util.UsbUtil; +import java.io.IOException; +import java.util.Vector; + +public class PJHA_HidIO implements DeviceIO { + + private static final Logger log = LogManager.getLogger(PJHA_HidIO.class); + + private HidDeviceInfo deviceInfo; + private HidDevice device; + + private static final int BUFFER_SIZE = 32; + private Vector dataBuffer; + private boolean streaming; + private DeviceOptions dOpts; + private SocketConnection websocket; + + public PJHA_HidIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException { + this(PJHA_HidUtilities.findDevice(dOpts), dOpts, websocket); + } + + private PJHA_HidIO(HidDeviceInfo deviceInfo, DeviceOptions dOpts, SocketConnection websocket) throws DeviceException { + this.dOpts = dOpts; + this.websocket = websocket; + if (deviceInfo == null) { + throw new DeviceException("HID device could not be found"); + } + + this.deviceInfo = deviceInfo; + + dataBuffer = new Vector() { + @Override + public synchronized boolean add(byte[] e) { + while(this.size() >= BUFFER_SIZE) { + this.remove(0); + } + return super.add(e); + } + }; + } + + public void open() throws DeviceException { + if (!isOpen()) { + try { + device = PureJavaHidApi.openDevice(deviceInfo); + device.setInputReportListener(new InputReportListener() { + @Override + public void onInputReport(HidDevice source, byte id, byte[] data, int len) { + byte[] dataCopy = new byte[len]; + System.arraycopy(data, 0, dataCopy, 0, len); + dataBuffer.add(dataCopy); + } + }); + } + catch(IOException ex) { + throw new DeviceException(ex); + } + } + } + + public boolean isOpen() { + return device != null; + } + + public void setStreaming(boolean active) { + streaming = active; + } + + public boolean isStreaming() { + return streaming; + } + + public String getVendorId() { + return UsbUtil.toHexString(deviceInfo.getVendorId()); + } + + public String getProductId() { + return UsbUtil.toHexString(deviceInfo.getProductId()); + } + + public byte[] readData(int responseSize, Byte unused) throws DeviceException { + byte[] response = new byte[responseSize]; + if (dataBuffer.isEmpty()) { + return new byte[0]; //no data received yet + } + + byte[] latestData = dataBuffer.remove(0); + if (SystemUtilities.isWindows()) { + //windows missing the leading byte + System.arraycopy(latestData, 0, response, 1, Math.min(responseSize - 1, latestData.length)); + } else { + System.arraycopy(latestData, 0, response, 0, Math.min(responseSize - 1, latestData.length)); + } + return response; + } + + public void sendData(byte[] data, Byte reportId) throws DeviceException { + if (reportId == null) { reportId = (byte)0x00; } + + int wrote = device.setOutputReport(reportId, data, data.length); + if (wrote == -1) { + throw new DeviceException("Failed to write to device"); + } + } + + public byte[] getFeatureReport(int responseSize, Byte unused) throws DeviceException { + byte[] response = new byte[responseSize]; + int read = device.getFeatureReport(response, responseSize); + if (read == -1) { + throw new DeviceException("Failed to read from device"); + } + return response; + } + + public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException { + if (reportId == null) { reportId = (byte)0x00; } + int wrote = device.setFeatureReport(reportId, data, data.length); + + if (wrote == -1) { + throw new DeviceException("Failed to write to device"); + } + + } + + @Override + public void close() { + setStreaming(false); + // Remove orphaned reference + websocket.removeDevice(dOpts); + if (isOpen()) { + try { + device.setInputReportListener(null); + device.close(); + } + catch(IllegalStateException e) { + log.warn("Device already closed"); + } + } + + device = null; + } + +} diff --git a/old code/tray/src/qz/communication/PJHA_HidListener.java b/old code/tray/src/qz/communication/PJHA_HidListener.java new file mode 100755 index 0000000..5b95b8c --- /dev/null +++ b/old code/tray/src/qz/communication/PJHA_HidListener.java @@ -0,0 +1,50 @@ +package qz.communication; + +import org.eclipse.jetty.websocket.api.Session; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import purejavahidapi.DeviceRemovalListener; +import purejavahidapi.HidDevice; +import qz.ws.PrintSocketClient; +import qz.ws.StreamEvent; + +import javax.usb.util.UsbUtil; + +public class PJHA_HidListener implements DeviceListener, DeviceRemovalListener { + + private static final Logger log = LogManager.getLogger(PJHA_HidListener.class); + + private Session session; + private HidDevice device; + + + public PJHA_HidListener(Session session) { + this.session = session; + } + + public void setDevice(HidDevice device) { + this.device = device; + device.setDeviceRemovalListener(this); + } + + private StreamEvent createStreamAction(HidDevice device, String action) { + return new StreamEvent(StreamEvent.Stream.HID, StreamEvent.Type.ACTION) + .withData("vendorId", UsbUtil.toHexString(device.getHidDeviceInfo().getVendorId())) + .withData("productId", UsbUtil.toHexString(device.getHidDeviceInfo().getProductId())) + .withData("actionType", action); + } + + + @Override + public void close() { + if (device != null) { + device.setDeviceRemovalListener(null); + } + } + + @Override + public void onDeviceRemoval(HidDevice device) { + log.debug("Device detached: {}", device.getHidDeviceInfo().getProductString()); + PrintSocketClient.sendStream(session, createStreamAction(device, "Device Detached"), this); + } +} diff --git a/old code/tray/src/qz/communication/PJHA_HidUtilities.java b/old code/tray/src/qz/communication/PJHA_HidUtilities.java new file mode 100755 index 0000000..e8e8682 --- /dev/null +++ b/old code/tray/src/qz/communication/PJHA_HidUtilities.java @@ -0,0 +1,56 @@ +package qz.communication; + + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import purejavahidapi.HidDeviceInfo; +import purejavahidapi.PureJavaHidApi; + +import javax.usb.util.UsbUtil; +import java.util.List; + +public class PJHA_HidUtilities { + + public static JSONArray getHidDevicesJSON() throws JSONException { + List devices = PureJavaHidApi.enumerateDevices(); + JSONArray devicesJSON = new JSONArray(); + + for(HidDeviceInfo device : devices) { + JSONObject deviceJSON = new JSONObject(); + + deviceJSON.put("vendorId", UsbUtil.toHexString(device.getVendorId())) + .put("productId", UsbUtil.toHexString(device.getProductId())) + .put("usagePage", UsbUtil.toHexString(device.getUsagePage())) + .put("serial", device.getSerialNumberString()) + .put("manufacturer", device.getManufacturerString()) + .put("product", device.getProductString()); + + devicesJSON.put(deviceJSON); + } + + return devicesJSON; + } + + public static HidDeviceInfo findDevice(DeviceOptions dOpts) { + if (dOpts.getVendorId() == null) { + throw new IllegalArgumentException("Vendor ID cannot be null"); + } + if (dOpts.getProductId() == null) { + throw new IllegalArgumentException("Product ID cannot be null"); + } + + + List devList = PureJavaHidApi.enumerateDevices(); + for(HidDeviceInfo device : devList) { + if (device.getVendorId() == dOpts.getVendorId().shortValue() && device.getProductId() == dOpts.getProductId().shortValue() + && (dOpts.getUsagePage() == null || dOpts.getUsagePage().shortValue() == device.getUsagePage()) + && (dOpts.getSerial() == null || dOpts.getSerial().equals(device.getSerialNumberString()))) { + return device; + } + } + + return null; + } + +} diff --git a/old code/tray/src/qz/communication/SerialIO.java b/old code/tray/src/qz/communication/SerialIO.java new file mode 100755 index 0000000..c4ebe3f --- /dev/null +++ b/old code/tray/src/qz/communication/SerialIO.java @@ -0,0 +1,297 @@ +package qz.communication; + +import jssc.*; +import org.apache.commons.codec.binary.StringUtils; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.ByteArrayBuilder; +import qz.utils.ByteUtilities; +import qz.utils.DeviceUtilities; +import qz.ws.SocketConnection; + +import java.io.IOException; + +/** + * @author Tres + */ +public class SerialIO implements DeviceListener { + + private static final Logger log = LogManager.getLogger(SerialIO.class); + + // Timeout to wait before giving up on reading the specified amount of bytes + private static final int TIMEOUT = 1200; + + private String portName; + private SerialPort port; + private SerialOptions serialOpts; + + private ByteArrayBuilder data = new ByteArrayBuilder(); + + private SocketConnection websocket; + + + /** + * Controller for serial communications + * + * @param portName Port name to open, such as "COM1" or "/dev/tty0/" + */ + public SerialIO(String portName, SocketConnection websocket) { + this.portName = portName; + this.websocket = websocket; + } + + /** + * Open the specified port name. + * + * @param opts Parsed serial options + * @return Boolean indicating success. + * @throws SerialPortException If the port fails to open. + */ + public boolean open(SerialOptions opts) throws SerialPortException { + if (isOpen()) { + log.warn("Serial port [{}] is already open", portName); + return false; + } + + port = new SerialPort(portName); + port.openPort(); + + serialOpts = new SerialOptions(); + setOptions(opts); + + return port.isOpened(); + } + + public void applyPortListener(SerialPortEventListener listener) throws SerialPortException { + port.addEventListener(listener); + } + + /** + * @return Boolean indicating if port is currently open. + */ + public boolean isOpen() { + return port != null && port.isOpened(); + } + + public String processSerialEvent(SerialPortEvent event) { + SerialOptions.ResponseFormat format = serialOpts.getResponseFormat(); + + try { + // Receive data + if (event.isRXCHAR()) { + data.append(port.readBytes(event.getEventValue(), TIMEOUT)); + + String response = null; + if (format.isBoundNewline()) { + //process as line delimited + + // check for CR AND NL + Integer endIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r', '\n'}); + int delimSize = 2; + + // check for CR OR NL + if(endIdx == null) { + endIdx = min( + ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\r'}), + ByteUtilities.firstMatchingIndex(data.getByteArray(), new byte[] {'\n'})); + delimSize = 1; + } + if (endIdx != null) { + log.trace("Reading newline-delimited response"); + byte[] output = new byte[endIdx]; + System.arraycopy(data.getByteArray(), 0, output, 0, endIdx); + String buffer = new String(output, format.getEncoding()); + + if (!buffer.isEmpty()) { + //send non-empty string + response = buffer; + } + + data.clearRange(0, endIdx + delimSize); + } + } else if (format.getBoundStart() != null && format.getBoundStart().length > 0) { + //process as formatted response + Integer startIdx = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundStart()); + + if (startIdx != null) { + int startOffset = startIdx + format.getBoundStart().length; + + int copyLength = 0; + int endIdx = 0; + + if (format.getBoundEnd() != null && format.getBoundEnd().length > 0) { + //process as bounded response + Integer boundEnd = ByteUtilities.firstMatchingIndex(data.getByteArray(), format.getBoundEnd(), startIdx); + + if (boundEnd != null) { + log.trace("Reading bounded response"); + + copyLength = boundEnd - startOffset; + endIdx = boundEnd + 1; + if (format.isIncludeStart()) { + //also include the ending bytes + copyLength += format.getBoundEnd().length; + } + } + } else if (format.getFixedWidth() > 0) { + //process as fixed length prefixed response + log.trace("Reading fixed length prefixed response"); + + copyLength = format.getFixedWidth(); + endIdx = startOffset + format.getFixedWidth(); + } else if (format.getLength() != null) { + //process as dynamic formatted response + SerialOptions.ByteParam lengthParam = format.getLength(); + + if (data.getLength() > startOffset + lengthParam.getIndex() + lengthParam.getLength()) { //ensure there's length bytes to read + log.trace("Reading dynamic formatted response"); + + int expectedLength = ByteUtilities.parseBytes(data.getByteArray(), startOffset + lengthParam.getIndex(), lengthParam.getLength(), lengthParam.getEndian()); + log.trace("Found length byte, expecting {} bytes", expectedLength); + + startOffset += lengthParam.getIndex() + lengthParam.getLength(); // don't include the length byte(s) in the response + copyLength = expectedLength; + endIdx = startOffset + copyLength; + + if (format.getCrc() != null) { + SerialOptions.ByteParam crcParam = format.getCrc(); + + log.trace("Expecting {} crc bytes", crcParam.getLength()); + int expand = crcParam.getIndex() + crcParam.getLength(); + + //include crc in copy + copyLength += expand; + endIdx += expand; + } + } + } else { + //process as header formatted raw response - high risk of lost data, likely unintended settings + log.warn("Reading header formatted raw response, are you missing an rx option?"); + + copyLength = data.getLength() - startOffset; + endIdx = data.getLength(); + } + + + if (copyLength > 0 && data.getLength() >= endIdx) { + log.debug("Response format readable, starting copy"); + + if (format.isIncludeStart()) { + //increase length to account for header bytes and bump offset back to include in copy + copyLength += (startOffset - startIdx); + startOffset = startIdx; + } + + byte[] responseData = new byte[copyLength]; + System.arraycopy(data.getByteArray(), startOffset, responseData, 0, copyLength); + + response = new String(responseData, format.getEncoding()); + data.clearRange(startIdx, endIdx); + } + } + } else if (format.getFixedWidth() > 0) { + if (data.getLength() >= format.getFixedWidth()) { + //process as fixed width response + log.trace("Reading fixed length response"); + + byte[] output = new byte[format.getFixedWidth()]; + System.arraycopy(data.getByteArray(), 0, output, 0, format.getFixedWidth()); + + response = StringUtils.newStringUtf8(output); + data.clearRange(0, format.getFixedWidth()); + } + } else { + //no processing, return raw + log.trace("Reading raw response"); + + response = new String(data.getByteArray(), format.getEncoding()); + data.clear(); + } + + return response; + } + } + catch(SerialPortException e) { + log.error("Exception occurred while reading data from port.", e); + } + catch(SerialPortTimeoutException e) { + log.error("Timeout occurred waiting for port to respond.", e); + } + + return null; + } + + /** + * Sets and caches the properties as to not set them every data call + * + * @throws SerialPortException If the properties fail to set + */ + private void setOptions(SerialOptions opts) throws SerialPortException { + if (opts == null) { return; } + + SerialOptions.PortSettings ps = opts.getPortSettings(); + if (ps != null && !ps.equals(serialOpts.getPortSettings())) { + log.debug("Applying new port settings"); + port.setParams(ps.getBaudRate(), ps.getDataBits(), ps.getStopBits(), ps.getParity()); + port.setFlowControlMode(ps.getFlowControl()); + serialOpts.setPortSettings(ps); + } + + SerialOptions.ResponseFormat rf = opts.getResponseFormat(); + if (rf != null) { + log.debug("Applying new response formatting"); + serialOpts.setResponseFormat(rf); + } + } + + /** + * Applies the port parameters and writes the buffered data to the serial port. + */ + public void sendData(JSONObject params, SerialOptions opts) throws JSONException, IOException, SerialPortException { + if (opts != null) { + setOptions(opts); + } + + log.debug("Sending data over [{}]", portName); + port.writeBytes(DeviceUtilities.getDataBytes(params, serialOpts.getPortSettings().getEncoding())); + } + + /** + * Closes the serial port, if open. + * + * @throws SerialPortException If the port fails to close. + */ + @Override + public void close() { + // Remove orphaned reference + websocket.removeSerialPort(portName); + + if (!isOpen()) { + log.warn("Serial port [{}] is not open.", portName); + } + + try { + boolean closed = port.closePort(); + if (closed) { + log.info("Serial port [{}] closed successfully.", portName); + } else { + // Handle ambiguity in JSSCs API + throw new SerialPortException(portName, "closePort", "Port not closed"); + } + } catch(SerialPortException e) { + log.warn("Serial port [{}] was not closed properly.", portName); + } + + port = null; + portName = null; + } + + private Integer min(Integer a, Integer b) { + if (a == null) { return b; } + if (b == null) { return a; } + return Math.min(a, b); + } + +} diff --git a/old code/tray/src/qz/communication/SerialOptions.java b/old code/tray/src/qz/communication/SerialOptions.java new file mode 100755 index 0000000..99a703b --- /dev/null +++ b/old code/tray/src/qz/communication/SerialOptions.java @@ -0,0 +1,350 @@ +package qz.communication; + +import jssc.SerialPort; +import org.apache.commons.lang3.ArrayUtils; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.ByteUtilities; +import qz.utils.DeviceUtilities; +import qz.utils.LoggerUtilities; +import qz.utils.SerialUtilities; + +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Locale; + +public class SerialOptions { + + private static final Logger log = LogManager.getLogger(SerialOptions.class); + + private static final String DEFAULT_BEGIN = "0x0002"; + private static final String DEFAULT_END = "0x000D"; + + private PortSettings portSettings = null; + private ResponseFormat responseFormat = null; + + /** + * Creates an empty/default options object + */ + public SerialOptions() { + portSettings = new PortSettings(); + responseFormat = new ResponseFormat(); + } + + /** + * Parses the provided JSON object into relevant SerialPort constants + */ + public SerialOptions(JSONObject serialOpts, boolean isOpening) { + if (serialOpts == null) { return; } + + //only apply port settings if opening or explicitly set in a send data call + if (isOpening || serialOpts.has("baudRate") || serialOpts.has("dataBits") || serialOpts.has("stopBits") || serialOpts.has("parity") || serialOpts.has("flowControl")) { + portSettings = new PortSettings(); + + if (!serialOpts.isNull("baudRate")) { + try { portSettings.baudRate = SerialUtilities.parseBaudRate(serialOpts.getString("baudRate")); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "baudRate", serialOpts.opt("baudRate")); } + } + + if (!serialOpts.isNull("dataBits")) { + try { portSettings.dataBits = SerialUtilities.parseDataBits(serialOpts.getString("dataBits")); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "dataBits", serialOpts.opt("dataBits")); } + } + + if (!serialOpts.isNull("stopBits")) { + try { portSettings.stopBits = SerialUtilities.parseStopBits(serialOpts.getString("stopBits")); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "stopBits", serialOpts.opt("stopBits")); } + } + + if (!serialOpts.isNull("parity")) { + try { portSettings.parity = SerialUtilities.parseParity(serialOpts.getString("parity")); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "parity", serialOpts.opt("parity")); } + } + + if (!serialOpts.isNull("flowControl")) { + try { portSettings.flowControl = SerialUtilities.parseFlowControl(serialOpts.getString("flowControl")); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "flowControl", serialOpts.opt("flowControl")); } + } + + if (!serialOpts.isNull("encoding") && !serialOpts.optString("encoding").isEmpty()) { + try { portSettings.encoding = Charset.forName(serialOpts.getString("encoding")); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "encoding", serialOpts.opt("encoding")); } + } + } + + if (!serialOpts.isNull("rx")) { + responseFormat = new ResponseFormat(); + //Make the response encoding default to the port encoding. If this is removed it will default to UTF-8 + responseFormat.encoding = portSettings.encoding; + + JSONObject respOpts = serialOpts.optJSONObject("rx"); + if (respOpts != null) { + if (!respOpts.isNull("start")) { + try { + JSONArray startBits = respOpts.getJSONArray("start"); + ArrayList bytes = new ArrayList<>(); + for(int i = 0; i < startBits.length(); i++) { + byte[] charByte = DeviceUtilities.characterBytes(startBits.getString(i), responseFormat.encoding); + for(byte b : charByte) { bytes.add(b); } + } + responseFormat.boundStart = ArrayUtils.toPrimitive(bytes.toArray(new Byte[0])); + } + catch(JSONException e) { + try { responseFormat.boundStart = DeviceUtilities.characterBytes(respOpts.getString("start"), responseFormat.encoding); } + catch(JSONException e2) { LoggerUtilities.optionWarn(log, "string", "start", respOpts.opt("start")); } + } + } + + if (!respOpts.isNull("includeHeader")) { + try { responseFormat.includeStart = respOpts.getBoolean("includeHeader"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "includeHeader", respOpts.opt("includeHeader")); } + } + + if (!respOpts.isNull("end")) { + try { responseFormat.boundEnd = DeviceUtilities.characterBytes(respOpts.getString("end"), responseFormat.encoding); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "string", "end", respOpts.opt("end")); } + + if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) { + log.warn("End bound set without start bound defined"); + } + } + + if (!respOpts.isNull("untilNewline")) { + try { responseFormat.boundNewline = respOpts.getBoolean("untilNewline"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "untilNewline", respOpts.opt("untilNewline")); } + } + + if (!respOpts.isNull("width")) { + try { responseFormat.fixedWidth = respOpts.getInt("width"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "width", respOpts.opt("width")); } + } + + if (!respOpts.isNull("lengthBytes")) { + try { + JSONObject lengthOpts = respOpts.optJSONObject("lengthBytes"); + responseFormat.length = new ByteParam(); + + if (lengthOpts != null) { + if (!lengthOpts.isNull("index")) { + try { responseFormat.length.index = lengthOpts.getInt("index"); } + catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes.index", lengthOpts.opt("index")); } + } + + if (!lengthOpts.isNull("length")) { + try { responseFormat.length.length = lengthOpts.getInt("length"); } + catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes.length", lengthOpts.opt("length")); } + } + + if (!lengthOpts.isNull("endian")) { + try { responseFormat.length.endian = ByteUtilities.Endian.valueOf(lengthOpts.getString("endian").toUpperCase(Locale.ENGLISH)); } + catch(JSONException se) { LoggerUtilities.optionWarn(log, "string", "lengthBytes.endian", lengthOpts.opt("endian")); } + } + } else { + responseFormat.length.index = respOpts.getInt("lengthBytes"); + } + } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "lengthBytes", respOpts.opt("lengthBytes")); } + + if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) { + log.warn("Length byte(s) defined without start bound defined"); + } + } + + if (!respOpts.isNull("crcBytes")) { + try { + JSONObject crcOpts = respOpts.optJSONObject("crcBytes"); + responseFormat.crc = new ByteParam(); + + if (crcOpts != null) { + if (!crcOpts.isNull("index")) { + try { responseFormat.crc.index = crcOpts.getInt("index"); } + catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "crcBytes.index", crcOpts.opt("index")); } + } + + if (!crcOpts.isNull("length")) { + try { responseFormat.crc.length = crcOpts.getInt("length"); } + catch(JSONException se) { LoggerUtilities.optionWarn(log, "integer", "crcBytes.length", crcOpts.opt("length")); } + } + } else { + responseFormat.crc.length = respOpts.getInt("crcBytes"); + } + } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "crcBytes", respOpts.opt("crcBytes")); } + + if (responseFormat.boundStart == null || responseFormat.boundStart.length == 0) { + log.warn("CRC byte(s) defined without start bound defined"); + } + } + + if (!respOpts.isNull("encoding") && !respOpts.optString("encoding").isEmpty()) { + try { responseFormat.encoding = Charset.forName(respOpts.getString("encoding")); } + catch(JSONException | IllegalArgumentException e) { LoggerUtilities.optionWarn(log, "charset", "encoding", respOpts.opt("encoding")); } + } + } else { + LoggerUtilities.optionWarn(log, "JSONObject", "rx", serialOpts.opt("rx")); + } + } else if (isOpening) { + // legacy support - only applies on port open + responseFormat = new ResponseFormat(); + + // legacy start only supports string, not an array + if (!serialOpts.isNull("start")) { + responseFormat.boundStart = DeviceUtilities.characterBytes(serialOpts.optString("start", DEFAULT_BEGIN), responseFormat.encoding); + } else { + responseFormat.boundStart = DeviceUtilities.characterBytes(DEFAULT_BEGIN, responseFormat.encoding); + } + + if (!serialOpts.isNull("end")) { + responseFormat.boundEnd = DeviceUtilities.characterBytes(serialOpts.optString("end", DEFAULT_END), responseFormat.encoding); + } else { + responseFormat.boundEnd = DeviceUtilities.characterBytes(DEFAULT_END, responseFormat.encoding); + } + + if (!serialOpts.isNull("width")) { + try { + responseFormat.fixedWidth = serialOpts.getInt("width"); + if (responseFormat.boundEnd.length > 0) { + log.warn("Combining 'width' property with 'end' property has undefined behavior and should not be used"); + } + } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "width", serialOpts.opt("width")); } + } + } + } + + public PortSettings getPortSettings() { + return portSettings; + } + + public ResponseFormat getResponseFormat() { + return responseFormat; + } + + public void setPortSettings(PortSettings portSettings) { + this.portSettings = portSettings; + } + + public void setResponseFormat(ResponseFormat responseFormat) { + this.responseFormat = responseFormat; + } + + public class PortSettings { + + private Charset encoding = Charset.forName("UTF-8"); + private int baudRate = SerialPort.BAUDRATE_9600; + private int dataBits = SerialPort.DATABITS_8; + private int stopBits = SerialPort.STOPBITS_1; + private int parity = SerialPort.PARITY_NONE; + private int flowControl = SerialPort.FLOWCONTROL_NONE; + + + public Charset getEncoding() { + return encoding; + } + + public int getBaudRate() { + return baudRate; + } + + public int getDataBits() { + return dataBits; + } + + public int getStopBits() { + return stopBits; + } + + public int getParity() { + return parity; + } + + public int getFlowControl() { + return flowControl; + } + + @Override + public boolean equals(Object o) { + if (o instanceof PortSettings) { + PortSettings that = (PortSettings)o; + + return getEncoding().equals(that.getEncoding()) && + getBaudRate() == that.getBaudRate() && + getDataBits() == that.getDataBits() && + getStopBits() == that.getStopBits() && + getParity() == that.getParity() && + getFlowControl() == that.getFlowControl(); + } else { + return false; + } + } + } + + public class ResponseFormat { + + private Charset encoding = Charset.forName("UTF-8"); //Response charset + private byte[] boundStart; //Character(s) denoting start of new response + private byte[] boundEnd; //Character denoting end of a response + private boolean boundNewline; //If the response should be split on \r?\n + private int fixedWidth; //Fixed length response bounds + private ByteParam length; //Info about the data length byte(s) + private ByteParam crc; //Info about the data crc byte(s) + private boolean includeStart; //If the response headers should be sent as well + + + public Charset getEncoding() { + return encoding; + } + + public byte[] getBoundStart() { + return boundStart; + } + + public byte[] getBoundEnd() { + return boundEnd; + } + + public int getFixedWidth() { + return fixedWidth; + } + + public ByteParam getLength() { + return length; + } + + public ByteParam getCrc() { + return crc; + } + + public boolean isIncludeStart() { + return includeStart; + } + + public boolean isBoundNewline() { + return boundNewline; + } + } + + public class ByteParam { + + private int index = 0; + private int length = 1; + private ByteUtilities.Endian endian = ByteUtilities.Endian.BIG; + + + public int getIndex() { + return index; + } + + public int getLength() { + return length; + } + + public ByteUtilities.Endian getEndian() { + return endian; + } + } + +} diff --git a/old code/tray/src/qz/communication/SocketIO.java b/old code/tray/src/qz/communication/SocketIO.java new file mode 100755 index 0000000..08906f2 --- /dev/null +++ b/old code/tray/src/qz/communication/SocketIO.java @@ -0,0 +1,100 @@ +package qz.communication; + +import org.apache.commons.lang3.ArrayUtils; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.DeviceUtilities; +import qz.utils.NetworkUtilities; +import qz.ws.SocketConnection; + +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.net.Socket; +import java.nio.charset.Charset; +import java.util.ArrayList; + +public class SocketIO implements DeviceListener { + + private static final Logger log = LogManager.getLogger(SocketIO.class); + + private String host; + private int port; + private Charset encoding; + + private Socket socket; + private DataOutputStream dataOut; + private DataInputStream dataIn; + + private SocketConnection websocket; + + public SocketIO(String host, int port, Charset encoding, SocketConnection websocket) { + this.host = host; + this.port = port; + this.encoding = encoding; + this.websocket = websocket; + } + + public boolean open() throws IOException { + socket = new Socket(host, port); + socket.setSoTimeout(NetworkUtilities.SOCKET_TIMEOUT); + dataOut = new DataOutputStream(socket.getOutputStream()); + dataIn = new DataInputStream(socket.getInputStream()); + + return socket.isConnected(); + } + + public boolean isOpen() { + return socket.isConnected(); + } + + public void sendData(JSONObject params) throws JSONException, IOException { + log.debug("Sending data over [{}:{}]", host, port); + dataOut.write(DeviceUtilities.getDataBytes(params, encoding)); + dataOut.flush(); + } + + public String processSocketResponse() throws IOException { + byte[] response = new byte[1024]; + ArrayList fullResponse = new ArrayList<>(); + do { + int size = dataIn.read(response); + for(int i = 0; i < size; i++) { + fullResponse.add(response[i]); + } + } + while(dataIn.available() > 0); + if(fullResponse.size() > 0) { + return new String(ArrayUtils.toPrimitive(fullResponse.toArray(new Byte[0])), encoding); + } + return null; + } + + @Override + public void close() { + // Remove orphaned reference + websocket.removeNetworkSocket(String.format("%s:%s", host, port)); + + try { + dataOut.close(); + } catch(IOException e) { + log.warn("Could not close socket output stream", e); + } + try { + socket.close(); + } catch(IOException e) { + log.warn("Could not close socket", e); + } + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + +} diff --git a/old code/tray/src/qz/communication/UsbIO.java b/old code/tray/src/qz/communication/UsbIO.java new file mode 100755 index 0000000..6f0d80a --- /dev/null +++ b/old code/tray/src/qz/communication/UsbIO.java @@ -0,0 +1,153 @@ +package qz.communication; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.UsbUtilities; +import qz.ws.SocketConnection; + +import javax.usb.*; +import javax.usb.util.UsbUtil; + +public class UsbIO implements DeviceIO { + private static final Logger log = LogManager.getLogger(UsbIO.class); + private UsbDevice device; + private UsbInterface iface; + + private boolean streaming; + + private DeviceOptions dOpts; + private SocketConnection websocket; + + public UsbIO(DeviceOptions dOpts, SocketConnection websocket) throws DeviceException { + this.dOpts = dOpts; + this.websocket = websocket; + UsbDevice device = UsbUtilities.findDevice(dOpts.getVendorId().shortValue(), dOpts.getProductId().shortValue()); + if (device == null) { + throw new DeviceException("USB device could not be found"); + } + if (dOpts.getInterfaceId() == null) { + throw new IllegalArgumentException("Device interface cannot be null"); + } + this.iface = device.getActiveUsbConfiguration().getUsbInterface(dOpts.getInterfaceId()); + if (iface == null) { + throw new DeviceException(String.format("Could not find USB interface matching [ vendorId: '%s', productId: '%s', interface: '%s' ]", + "0x" + UsbUtil.toHexString(dOpts.getVendorId()), + "0x" + UsbUtil.toHexString(dOpts.getProductId()), + "0x" + UsbUtil.toHexString(dOpts.getInterfaceId()))); + } + this.device = device; + + } + + public void open() throws DeviceException { + try { + iface.claim(new UsbInterfacePolicy() { + @Override + public boolean forceClaim(UsbInterface usbInterface) { + // Releases kernel driver for systems that auto-claim usb devices + return true; + } + }); + } + catch(UsbException e) { + throw new DeviceException(e); + } + } + + public boolean isOpen() { + return iface.isClaimed(); + } + + public void setStreaming(boolean active) { + streaming = active; + } + + public boolean isStreaming() { + return streaming; + } + + public String getVendorId() { + return UsbUtil.toHexString(device.getUsbDeviceDescriptor().idVendor()); + } + + public String getProductId() { + return UsbUtil.toHexString(device.getUsbDeviceDescriptor().idProduct()); + } + + public String getInterface() { + return UsbUtil.toHexString(iface.getUsbInterfaceDescriptor().iInterface()); + } + + public byte[] readData(int responseSize, Byte endpoint) throws DeviceException { + try { + byte[] response = new byte[responseSize]; + exchangeData(endpoint, response); + return response; + } + catch(UsbException e) { + throw new DeviceException(e); + } + } + + public void sendData(byte[] data, Byte endpoint) throws DeviceException { + try { + exchangeData(endpoint, data); + } + catch(UsbException e) { + throw new DeviceException(e); + } + } + + public byte[] getFeatureReport(int responseSize, Byte reportId) throws DeviceException { + throw new DeviceException("USB feature reports are not supported"); + } + + public void sendFeatureReport(byte[] data, Byte reportId) throws DeviceException { + throw new DeviceException("USB feature reports are not supported"); + } + + /** + * Data will be sent to or received from the open usb device, depending on the {@code endpoint} used. + * + * @param endpoint Endpoint on the usb device interface to pass data across + * @param data Byte array of data to send, or to be written from a receive + */ + private synchronized void exchangeData(Byte endpoint, byte[] data) throws UsbException, DeviceException { + if (endpoint == null) { + throw new IllegalArgumentException("Interface endpoint cannot be null"); + } + + UsbEndpoint usbEndpoint = iface.getUsbEndpoint(endpoint); + if(usbEndpoint == null) { + throw new DeviceException(String.format("Could not find USB endpoint matching [ endpoint: '%s' ]", + "0x" + UsbUtil.toHexString(endpoint))); + } + UsbPipe pipe = usbEndpoint.getUsbPipe(); + if (!pipe.isOpen()) { pipe.open(); } + + try { + pipe.syncSubmit(data); + } + finally { + if(pipe != null) { + pipe.close(); + } + } + } + + @Override + public void close() { + setStreaming(false); + // Remove orphaned reference + websocket.removeDevice(dOpts); + if (iface.isClaimed()) { + try { + iface.release(); + } + catch(UsbException e) { + log.error("Unable to close USB device", e); + } + } + } + +} diff --git a/old code/tray/src/qz/communication/WinspoolEx.java b/old code/tray/src/qz/communication/WinspoolEx.java new file mode 100755 index 0000000..1966da0 --- /dev/null +++ b/old code/tray/src/qz/communication/WinspoolEx.java @@ -0,0 +1,28 @@ +package qz.communication; + +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.platform.win32.WinNT; +import com.sun.jna.platform.win32.Winspool; +import com.sun.jna.win32.W32APIOptions; + +/** + * TODO: Remove when JNA 5.14.0+ is bundled + */ +@SuppressWarnings("unused") +public interface WinspoolEx extends Winspool { + WinspoolEx INSTANCE = Native.load("Winspool.drv", WinspoolEx.class, W32APIOptions.DEFAULT_OPTIONS); + + int JOB_CONTROL_NONE = 0x00000000; // Perform no additional action. + int JOB_CONTROL_PAUSE = 0x00000001; // Pause the print job. + int JOB_CONTROL_RESUME = 0x00000002; // Resume a paused print job. + int JOB_CONTROL_CANCEL = 0x00000003; // Delete a print job. + int JOB_CONTROL_RESTART = 0x00000004; // Restart a print job. + int JOB_CONTROL_DELETE = 0x00000005; // Delete a print job. + int JOB_CONTROL_SENT_TO_PRINTER = 0x00000006; // Used by port monitors to signal that a print job has been sent to the printer. This value SHOULD NOT be used remotely. + int JOB_CONTROL_LAST_PAGE_EJECTED = 0x00000007; // Used by language monitors to signal that the last page of a print job has been ejected from the printer. This value SHOULD NOT be used remotely. + int JOB_CONTROL_RETAIN = 0x00000008; // Keep the print job in the print queue after it prints. + int JOB_CONTROL_RELEASE = 0x00000009; // Release the print job, undoing the effect of a JOB_CONTROL_RETAIN action. + + boolean SetJob(WinNT.HANDLE hPrinter, int JobId, int Level, Pointer pJob, int Command); +} \ No newline at end of file diff --git a/old code/tray/src/qz/exception/InvalidRawImageException.java b/old code/tray/src/qz/exception/InvalidRawImageException.java new file mode 100755 index 0000000..b0982bc --- /dev/null +++ b/old code/tray/src/qz/exception/InvalidRawImageException.java @@ -0,0 +1,11 @@ +package qz.exception; + +public class InvalidRawImageException extends Exception { + public InvalidRawImageException(String msg) { + super(msg); + } + + public InvalidRawImageException(String msg, Throwable cause) { + super(msg, cause); + } +} diff --git a/old code/tray/src/qz/exception/MissingArgException.java b/old code/tray/src/qz/exception/MissingArgException.java new file mode 100755 index 0000000..8d946e1 --- /dev/null +++ b/old code/tray/src/qz/exception/MissingArgException.java @@ -0,0 +1,3 @@ +package qz.exception; + +public class MissingArgException extends Exception {} diff --git a/old code/tray/src/qz/exception/NullCommandException.java b/old code/tray/src/qz/exception/NullCommandException.java new file mode 100755 index 0000000..46314fd --- /dev/null +++ b/old code/tray/src/qz/exception/NullCommandException.java @@ -0,0 +1,10 @@ +package qz.exception; + +public class NullCommandException extends javax.print.PrintException { + public NullCommandException() { + super(); + } + public NullCommandException(String msg) { + super(msg); + } +} diff --git a/old code/tray/src/qz/exception/NullPrintServiceException.java b/old code/tray/src/qz/exception/NullPrintServiceException.java new file mode 100755 index 0000000..30253ef --- /dev/null +++ b/old code/tray/src/qz/exception/NullPrintServiceException.java @@ -0,0 +1,7 @@ +package qz.exception; + +public class NullPrintServiceException extends javax.print.PrintException { + public NullPrintServiceException(String msg) { + super(msg); + } +} diff --git a/old code/tray/src/qz/installer/Installer.java b/old code/tray/src/qz/installer/Installer.java new file mode 100755 index 0000000..041f933 --- /dev/null +++ b/old code/tray/src/qz/installer/Installer.java @@ -0,0 +1,413 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer; + +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.auth.Certificate; +import qz.build.provision.params.Phase; +import qz.installer.certificate.*; +import qz.installer.certificate.firefox.FirefoxCertificateInstaller; +import qz.installer.provision.ProvisionInstaller; +import qz.utils.FileUtilities; +import qz.utils.SystemUtilities; +import qz.ws.WebsocketPorts; + +import java.io.*; +import java.nio.file.*; +import java.security.cert.X509Certificate; +import java.util.*; + +import static qz.common.Constants.*; +import static qz.installer.certificate.KeyPairWrapper.Type.CA; +import static qz.utils.FileUtilities.*; + +/** + * Cross-platform wrapper for install steps + * - Used by CommandParser via command line + * - Used by PrintSocketServer at startup to ensure SSL is functioning + */ +public abstract class Installer { + protected static final Logger log = LogManager.getLogger(Installer.class); + + // Silence prompts within our control + public static boolean IS_SILENT = "1".equals(System.getenv(DATA_DIR + "_silent")); + public static String JRE_LOCATION = SystemUtilities.isMac() ? "Contents/PlugIns/Java.runtime/Contents/Home" : "runtime"; + + WebsocketPorts websocketPorts; + + public enum PrivilegeLevel { + USER, + SYSTEM + } + + public abstract Installer removeLegacyStartup(); + public abstract Installer addAppLauncher(); + public abstract Installer addStartupEntry(); + public abstract Installer addSystemSettings(); + public abstract Installer removeSystemSettings(); + public abstract void spawn(List args) throws Exception; + + public abstract void setDestination(String destination); + public abstract String getDestination(); + + private static Installer instance; + + public static Installer getInstance() { + if(instance == null) { + switch(SystemUtilities.getOs()) { + case WINDOWS: + instance = new WindowsInstaller(); + break; + case MAC: + instance = new MacInstaller(); + break; + default: + instance = new LinuxInstaller(); + } + } + return instance; + } + + public static void install(String destination, boolean silent) throws Exception { + IS_SILENT |= silent; // preserve environmental variable if possible + getInstance(); + if (destination != null) { + instance.setDestination(destination); + } + install(); + } + + public static boolean preinstall() { + getInstance(); + log.info("Fixing runtime permissions..."); + instance.setJrePermissions(SystemUtilities.getAppPath().toString()); + log.info("Stopping running instances..."); + return TaskKiller.killAll(); + } + + public static void install() throws Exception { + getInstance(); + log.info("Installing to {}", instance.getDestination()); + instance.removeLibs() + .removeProvisioning() + .deployApp() + .removeLegacyStartup() + .removeLegacyFiles() + .addSharedDirectory() + .addAppLauncher() + .addStartupEntry() + .invokeProvisioning(Phase.INSTALL) + .addSystemSettings(); + } + + public static void uninstall() { + log.info("Stopping running instances..."); + TaskKiller.killAll(); + getInstance(); + log.info("Uninstalling from {}", instance.getDestination()); + instance.removeSharedDirectory() + .removeSystemSettings() + .removeCerts() + .invokeProvisioning(Phase.UNINSTALL); + } + + public Installer deployApp() throws IOException { + Path src = SystemUtilities.getAppPath(); + Path dest = Paths.get(getDestination()); + + if(!Files.exists(dest)) { + Files.createDirectories(dest); + } + + // Delete the JDK blindly + FileUtils.deleteDirectory(dest.resolve(JRE_LOCATION).toFile()); + // Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011 + FileUtils.copyDirectory(src.toFile(), dest.toFile(), false); + FileUtilities.setPermissionsRecursively(dest, false); + // Fix permissions for provisioned files + FileUtilities.setExecutableRecursively(SystemUtilities.isMac() ? + dest.resolve("Contents/Resources").resolve(PROVISION_DIR) : + dest.resolve(PROVISION_DIR), false); + if(!SystemUtilities.isWindows()) { + setExecutable(SystemUtilities.isMac() ? "Contents/Resources/uninstall" : "uninstall"); + setExecutable(SystemUtilities.isMac() ? "Contents/MacOS/" + ABOUT_TITLE : PROPS_FILE); + return setJrePermissions(getDestination()); + } + return this; + } + + private Installer setJrePermissions(String dest) { + File jreLocation = new File(dest, JRE_LOCATION); + File jreBin = new File(jreLocation, "bin"); + File jreLib = new File(jreLocation, "lib"); + + // Set jre/bin/java and friends executable + File[] files = jreBin.listFiles(pathname -> !pathname.isDirectory()); + if(files != null) { + for(File file : files) { + file.setExecutable(true, false); + } + } + + // Set jspawnhelper executable + new File(jreLib, "jspawnhelper" + (SystemUtilities.isWindows() ? ".exe" : "")).setExecutable(true, false); + return this; + } + + private void setExecutable(String relativePath) { + new File(getDestination(), relativePath).setExecutable(true, false); + } + + /** + * Explicitly purge libs to notify system cache per https://github.com/qzind/tray/issues/662 + */ + public Installer removeLibs() { + String[] dirs = { "libs" }; + for (String dir : dirs) { + try { + FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir)); + } catch(IOException ignore) {} + } + return this; + } + + public Installer cleanupLegacyLogs(int rolloverCount) { + // Convert old < 2.2.3 log file format + Path logLocation = USER_DIR; + int oldIndex = 0; + int newIndex = 0; + File oldFile; + do { + // Old: debug.log.1 + oldFile = logLocation.resolve("debug.log." + ++oldIndex).toFile(); + if(oldFile.exists()) { + // New: debug.1.log + File newFile; + do { + newFile = logLocation.resolve("debug." + ++newIndex + ".log").toFile(); + } while(newFile.exists()); + + oldFile.renameTo(newFile); + log.info("Migrated log file {} to new location {}", oldFile, newFile); + } + } while(oldFile.exists() || oldIndex <= rolloverCount); + + return this; + } + + public Installer removeLegacyFiles() { + ArrayList dirs = new ArrayList<>(); + ArrayList files = new ArrayList<>(); + HashMap move = new HashMap<>(); + + // QZ Tray 2.0 files + dirs.add("demo/js/3rdparty"); + dirs.add("utils"); + dirs.add("auth"); + files.add("demo/js/qz-websocket.js"); + files.add("windows-icon.ico"); + + // QZ Tray 2.2.3-SNAPSHOT accidentally wrote certs in the wrong place + dirs.add("ssl"); + + // QZ Tray 2.1 files + if(SystemUtilities.isMac()) { + // Moved to macOS Application Bundle standard https://developer.apple.com/go/?id=bundle-structure + dirs.add("demo"); + dirs.add("libs"); + files.add(PROPS_FILE + ".jar"); + files.add("LICENSE.txt"); + files.add("uninstall"); + move.put(PROPS_FILE + ".properties", "Contents/Resources/" + PROPS_FILE + ".properties"); + } + + dirs.forEach(dir -> { + try { + FileUtils.deleteDirectory(new File(instance.getDestination() + File.separator + dir)); + } catch(IOException ignore) {} + }); + + files.forEach(file -> { + new File(instance.getDestination() + File.separator + file).delete(); + }); + + move.forEach((src, dest) -> { + try { + FileUtils.moveFile(new File(instance.getDestination() + File.separator + src), + new File(instance.getDestination() + File.separator + dest)); + } catch(IOException ignore) {} + }); + return this; + } + + public Installer addSharedDirectory() { + try { + Files.createDirectories(SHARED_DIR); + FileUtilities.setPermissionsRecursively(SHARED_DIR, true); + Path ssl = Paths.get(SHARED_DIR.toString(), "ssl"); + Files.createDirectories(ssl); + FileUtilities.setPermissionsRecursively(ssl, true); + + log.info("Created shared directory: {}", SHARED_DIR); + } catch(IOException e) { + log.warn("Could not create shared directory: {}", SHARED_DIR); + } + return this; + } + + public Installer removeSharedDirectory() { + try { + FileUtils.deleteDirectory(SHARED_DIR.toFile()); + log.info("Deleted shared directory: {}", SHARED_DIR); + } catch(IOException e) { + log.warn("Could not delete shared directory: {}", SHARED_DIR); + } + return this; + } + + /** + * Checks, and if needed generates an SSL for the system + */ + public CertificateManager certGen(boolean forceNew, String... hostNames) throws Exception { + CertificateManager certificateManager = new CertificateManager(forceNew, hostNames); + boolean needsInstall = certificateManager.needsInstall(); + try { + // Check that the CA cert is installed + X509Certificate caCert = certificateManager.getKeyPair(CA).getCert(); + NativeCertificateInstaller installer = NativeCertificateInstaller.getInstance(); + + if (forceNew || needsInstall) { + // Remove installed certs per request (usually the desktop installer, or failure to write properties) + // Skip if running from IDE, this may accidentally remove sandboxed certs + if(SystemUtilities.isJar()) { + List matchingCerts = installer.find(); + installer.remove(matchingCerts); + } + installer.install(caCert); + FirefoxCertificateInstaller.install(caCert, hostNames); + } else { + // Make sure the certificate is recognized by the system + if(caCert == null) { + log.info("CA cert is empty, skipping installation checks. This is normal for trusted/3rd-party SSL certificates."); + } else { + File tempCert = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION); + CertificateManager.writeCert(caCert, tempCert); // temp cert + if (!installer.verify(tempCert)) { + installer.install(caCert); + FirefoxCertificateInstaller.install(caCert, hostNames); + } + if(!tempCert.delete()) { + tempCert.deleteOnExit(); + } + } + } + } + catch(Exception e) { + log.error("Something went wrong obtaining the certificate. HTTPS will fail.", e); + } + + // Add provisioning steps that come after certgen + if(SystemUtilities.isAdmin()) { + invokeProvisioning(Phase.CERTGEN); + } + + return certificateManager; + } + + /** + * Remove matching certs from user|system, then Firefox + */ + public Installer removeCerts() { + // System certs + NativeCertificateInstaller instance = NativeCertificateInstaller.getInstance(); + instance.remove(instance.find()); + // Firefox certs + FirefoxCertificateInstaller.uninstall(); + return this; + } + + /** + * Add user-specific settings + * Note: See override usage for platform-specific tasks + */ + public Installer addUserSettings() { + // Check for whitelisted certificates in /whitelist/ + Path whiteList = SystemUtilities.getJarParentPath().resolve(WHITELIST_CERT_DIR); + if(Files.exists(whiteList) && Files.isDirectory(whiteList)) { + for(File file : whiteList.toFile().listFiles()) { + try { + Certificate cert = new Certificate(FileUtilities.readLocalFile(file.getPath())); + if (!cert.isSaved()) { + FileUtilities.addToCertList(ALLOW_FILE, file); + } + } catch(Exception e) { + log.warn("Could not add {} to {}", file, ALLOW_FILE, e); + } + } + } + return instance; + } + + public Installer invokeProvisioning(Phase phase) { + try { + Path provisionPath = SystemUtilities.isMac() ? + Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) : + Paths.get(getDestination()).resolve(PROVISION_DIR); + ProvisionInstaller provisionInstaller = new ProvisionInstaller(provisionPath); + provisionInstaller.invoke(phase); + + // Special case for custom websocket ports + if(phase == Phase.INSTALL) { + websocketPorts = WebsocketPorts.parseFromSteps(provisionInstaller.getSteps()); + } + } catch(Exception e) { + log.warn("An error occurred invoking provision \"phase\": \"{}\"", phase, e); + } + return this; + } + + public Installer removeProvisioning() { + try { + Path provisionPath = SystemUtilities.isMac() ? + Paths.get(getDestination()).resolve("Contents/Resources").resolve(PROVISION_DIR) : + Paths.get(getDestination()).resolve(PROVISION_DIR); + FileUtils.deleteDirectory(provisionPath.toFile()); + } catch(Exception e) { + log.warn("An error occurred removing provision directory", e); + } + return this; + } + + public static Properties persistProperties(File oldFile, Properties newProps) { + if(oldFile.exists()) { + Properties oldProps = new Properties(); + try(Reader reader = new FileReader(oldFile)) { + oldProps.load(reader); + for(String key : PERSIST_PROPS) { + if (oldProps.containsKey(key)) { + String value = oldProps.getProperty(key); + log.info("Preserving {}={} for install", key, value); + newProps.put(key, value); + } + } + } catch(IOException e) { + log.warn("Warning, an error occurred reading the old properties file {}", oldFile, e); + } + } + return newProps; + } + + public void spawn(String ... args) throws Exception { + spawn(new ArrayList(Arrays.asList(args))); + } +} diff --git a/old code/tray/src/qz/installer/LinuxInstaller.java b/old code/tray/src/qz/installer/LinuxInstaller.java new file mode 100755 index 0000000..cd3de6d --- /dev/null +++ b/old code/tray/src/qz/installer/LinuxInstaller.java @@ -0,0 +1,371 @@ +package qz.installer; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.FileUtilities; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; +import qz.utils.UnixUtilities; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.regex.Pattern; + +import static qz.common.Constants.*; + +public class LinuxInstaller extends Installer { + protected static final Logger log = LogManager.getLogger(LinuxInstaller.class); + + public static final String SHORTCUT_NAME = PROPS_FILE + ".desktop"; + public static final String STARTUP_DIR = "/etc/xdg/autostart/"; + public static final String STARTUP_LAUNCHER = STARTUP_DIR + SHORTCUT_NAME; + public static final String APP_DIR = "/usr/share/applications/"; + public static final String APP_LAUNCHER = APP_DIR + SHORTCUT_NAME; + public static final String UDEV_RULES = "/lib/udev/rules.d/99-udev-override.rules"; + public static final String[] CHROME_POLICY_DIRS = {"/etc/chromium/policies/managed", "/etc/opt/chrome/policies/managed" }; + public static final String CHROME_POLICY = "{ \"URLAllowlist\": [\"" + DATA_DIR + "://*\"] }"; + + private String destination = "/opt/" + PROPS_FILE; + private String sudoer; + + public LinuxInstaller() { + super(); + sudoer = getSudoer(); + } + + public void setDestination(String destination) { + this.destination = destination; + } + + public String getDestination() { + return destination; + } + + public Installer addAppLauncher() { + addLauncher(APP_LAUNCHER, false); + return this; + } + + public Installer addStartupEntry() { + addLauncher(STARTUP_LAUNCHER, true); + return this; + } + + private void addLauncher(String location, boolean isStartup) { + HashMap fieldMap = new HashMap<>(); + // Dynamic fields + fieldMap.put("%DESTINATION%", destination); + fieldMap.put("%LINUX_ICON%", String.format("%s.svg", PROPS_FILE)); + fieldMap.put("%COMMAND%", String.format("%s/%s", destination, PROPS_FILE)); + fieldMap.put("%PARAM%", isStartup ? "--honorautostart" : "%u"); + + File launcher = new File(location); + try { + FileUtilities.configureAssetFile("assets/linux-shortcut.desktop.in", launcher, fieldMap, LinuxInstaller.class); + launcher.setReadable(true, false); + launcher.setExecutable(true, false); + } catch(IOException e) { + log.warn("Unable to write {} file: {}", isStartup ? "startup":"launcher", location, e); + } + } + + public Installer removeLegacyStartup() { + log.info("Removing legacy autostart entries for all users matching {} or {}", ABOUT_TITLE, PROPS_FILE); + // assume users are in /home + String[] shortcutNames = {ABOUT_TITLE, PROPS_FILE}; + for(File file : new File("/home").listFiles()) { + if (file.isDirectory()) { + File userStart = new File(file.getPath() + "/.config/autostart"); + if (userStart.exists() && userStart.isDirectory()) { + for (String shortcutName : shortcutNames) { + File legacyStartup = new File(userStart.getPath() + File.separator + shortcutName + ".desktop"); + if(legacyStartup.exists()) { + legacyStartup.delete(); + } + } + } + } + } + return this; + } + + public Installer addSystemSettings() { + // Legacy Ubuntu versions only: Patch Unity to show the System Tray + if(UnixUtilities.isUbuntu()) { + ShellUtilities.execute("gsettings", "set", "com.canonical.Unity.Panel", "systray", "-whitelist", "\"['all']\""); + + if(ShellUtilities.execute("killall", "-w", "unity", "-panel")) { + ShellUtilities.execute("nohup", "unity", "-panel"); + } + + if(ShellUtilities.execute("killall", "-w", "unity", "-2d")) { + ShellUtilities.execute("nohup", "unity", "-2d"); + } + } + + // Chrome protocol handler + for (String policyDir : CHROME_POLICY_DIRS) { + log.info("Installing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json"); + try { + FileUtilities.setPermissionsParentally(Files.createDirectories(Paths.get(policyDir)), false); + } catch(IOException e) { + log.warn("An error occurred creating {}", policyDir); + } + + Path policy = Paths.get(policyDir, PROPS_FILE + ".json"); + try (BufferedWriter writer = new BufferedWriter(new FileWriter(policy.toFile()))){ + writer.write(CHROME_POLICY); + policy.toFile().setReadable(true, false); + } + catch(IOException e) { + log.warn("Unable to write chrome policy: {} ({}:launch will fail)", policy, DATA_DIR); + } + + } + + // USB permissions + try { + File udev = new File(UDEV_RULES); + if (udev.exists()) { + udev.delete(); + } + FileUtilities.configureAssetFile("assets/linux-udev.rules.in", new File(UDEV_RULES), new HashMap<>(), LinuxInstaller.class); + // udev rules should be -rw-r--r-- + udev.setReadable(true, false); + ShellUtilities.execute("udevadm", "control", "--reload-rules"); + } catch(IOException e) { + log.warn("Could not install udev rules, usb support may fail {}", UDEV_RULES, e); + } + + // Cleanup incorrectly placed files + File badFirefoxJs = new File("/usr/bin/defaults/pref/" + PROPS_FILE + ".js"); + File badFirefoxCfg = new File("/usr/bin/" + PROPS_FILE + ".cfg"); + + if(badFirefoxCfg.exists()) { + log.info("Removing incorrectly placed Firefox configuration {}, {}...", badFirefoxJs, badFirefoxCfg); + badFirefoxCfg.delete(); + new File("/usr/bin/defaults").delete(); + } + + // Cleanup incorrectly placed files + File badFirefoxPolicy = new File("/usr/bin/distribution/policies.json"); + if(badFirefoxPolicy.exists()) { + log.info("Removing incorrectly placed Firefox policy {}", badFirefoxPolicy); + badFirefoxPolicy.delete(); + // Delete the distribution folder too, as long as it's empty + File badPolicyFolder = badFirefoxPolicy.getParentFile(); + if(badPolicyFolder.isDirectory() && badPolicyFolder.listFiles().length == 0) { + badPolicyFolder.delete(); + } + } + + // Cleanup + log.info("Cleaning up any remaining files..."); + new File(destination + File.separator + "install").delete(); + return this; + } + + public Installer removeSystemSettings() { + // Chrome protocol handler + for (String policyDir : CHROME_POLICY_DIRS) { + log.info("Removing chrome protocol handler {}/{}...", policyDir, PROPS_FILE + ".json"); + Path policy = Paths.get(policyDir, PROPS_FILE + ".json"); + policy.toFile().delete(); + } + + // USB permissions + File udev = new File(UDEV_RULES); + if (udev.exists()) { + udev.delete(); + } + return this; + } + + // Environmental variables for spawning a task using sudo. Order is important. + static String[] SUDO_EXPORTS = {"USER", "HOME", "UPSTART_SESSION", "DISPLAY", "DBUS_SESSION_BUS_ADDRESS", "XDG_CURRENT_DESKTOP", "GNOME_DESKTOP_SESSION_ID" }; + + /** + * Spawns the process as the underlying regular user account, preserving the environment + */ + public void spawn(List args) throws Exception { + if(!SystemUtilities.isAdmin()) { + // Not admin, just run as the existing user + ShellUtilities.execute(args.toArray(new String[args.size()])); + return; + } + + // Get user's environment from dbus, etc + HashMap env = getUserEnv(sudoer); + if(env.size() == 0) { + throw new Exception("Unable to get dbus info; can't spawn instance"); + } + + // Prepare the environment + String[] envp = new String[env.size() + ShellUtilities.envp.length]; + int i = 0; + // Keep existing env + for(String keep : ShellUtilities.envp) { + envp[i++] = keep; + } + for(String key :env.keySet()) { + envp[i++] = String.format("%s=%s", key, env.get(key)); + } + + // Concat "sudo|su", sudoer, "nohup", args + ArrayList argsList = sudoCommand(sudoer, true, args); + + // Spawn + log.info("Executing: {}", Arrays.toString(argsList.toArray())); + Runtime.getRuntime().exec(argsList.toArray(new String[argsList.size()]), envp); + } + + /** + * Constructs a command to help running as another user using "sudo" or "su" + */ + public static ArrayList sudoCommand(String sudoer, boolean async, List cmds) { + ArrayList sudo = new ArrayList<>(); + if(StringUtils.isEmpty(sudoer) || !userExists(sudoer)) { + throw new UnsupportedOperationException(String.format("Parameter [sudoer: %s] is empty or the provided user was not found", sudoer)); + } + if(ShellUtilities.execute("which", "sudo") // check if sudo exists + || ShellUtilities.execute("sudo", "-u", sudoer, "-v")) { // check if user can login + // Pass directly into "sudo" + log.info("Guessing that this system prefers \"sudo\" over \"su\"."); + sudo.add("sudo"); + + // Add calling user + sudo.add("-E"); // preserve environment + sudo.add("-u"); + sudo.add(sudoer); + + // Add "background" task support + if(async) { + sudo.add("nohup"); + } + if(cmds != null && cmds.size() > 0) { + // Add additional commands + sudo.addAll(cmds); + } + } else { + // Build and escape for "su" + log.info("Guessing that this system prefers \"su\" over \"sudo\"."); + sudo.add("su"); + + // Add calling user + sudo.add(sudoer); + + sudo.add("-c"); + + // Add "background" task support + if(async) { + sudo.add("nohup"); + } + if(cmds != null && cmds.size() > 0) { + // Add additional commands + sudo.addAll(Arrays.asList(StringUtils.join(cmds, "\" \"") + "\"")); + } + } + return sudo; + } + + /** + * Gets the most likely non-root user account that the installer is running from + */ + private static String getSudoer() { + String sudoer = ShellUtilities.executeRaw("logname").trim(); + if(sudoer.isEmpty() || SystemUtilities.isSolaris()) { + sudoer = System.getenv("SUDO_USER"); + } + return sudoer; + } + + /** + * Uses two common POSIX techniques for testing if the provided user account exists + */ + private static boolean userExists(String user) { + return ShellUtilities.execute("id", "-u", user) || + ShellUtilities.execute("getent", "passwd", user); + } + + /** + * Attempts to extract user environment variables from the dbus process to + * allow starting a graphical application as the current user. + * + * If this fails, items such as the user's desktop theme may not be known to Java + * at runtime resulting in the Swing L&F instead of the Gtk L&F. + */ + private static HashMap getUserEnv(String matchingUser) { + if(!SystemUtilities.isAdmin()) { + throw new UnsupportedOperationException("Administrative access is required"); + } + + String[] dbusMatches = { "ibus-daemon.*--panel", "dbus-daemon.*--config-file="}; + + ArrayList pids = new ArrayList<>(); + for(String dbusMatch : dbusMatches) { + pids.addAll(Arrays.asList(ShellUtilities.executeRaw("pgrep", "-f", dbusMatch).split("\\r?\\n"))); + } + + HashMap env = new HashMap<>(); + HashMap tempEnv = new HashMap<>(); + ArrayList toExport = new ArrayList<>(Arrays.asList(SUDO_EXPORTS)); + for(String pid : pids) { + if(pid.isEmpty()) { + continue; + } + try { + String[] vars; + if(SystemUtilities.isSolaris()) { + // Use pargs -e $$ to get environment + log.info("Reading environment info from [pargs, -e, {}]", pid); + String pargs = ShellUtilities.executeRaw("pargs", "-e", pid); + vars = pargs.split("\\r?\\n"); + String delim = "]: "; + for(int i = 0; i < vars.length; i++) { + if(vars[i].contains(delim)) { + vars[i] = vars[i].substring(vars[i].indexOf(delim) + delim.length()).trim(); + } + } + } else { + // Assume /proc/$$/environ + String environ = String.format("/proc/%s/environ", pid); + String delim = Pattern.compile("\0").pattern(); + log.info("Reading environment info from {}", environ); + vars = new String(Files.readAllBytes(Paths.get(environ))).split(delim); + } + for(String var : vars) { + String[] parts = var.split("=", 2); + if(parts.length == 2) { + String key = parts[0].trim(); + String val = parts[1].trim(); + if(toExport.contains(key)) { + tempEnv.put(key, val); + } + } + } + } catch(Exception e) { + log.warn("An unexpected error occurred obtaining dbus info", e); + } + + // Only add vars for the current user + if(matchingUser.trim().equals(tempEnv.get("USER"))) { + env.putAll(tempEnv); + } else { + log.debug("Expected USER={} but got USER={}, skipping results for {}", matchingUser, tempEnv.get("USER"), pid); + } + + // Use gtk theme + if(env.containsKey("XDG_CURRENT_DESKTOP") && !env.containsKey("GNOME_DESKTOP_SESSION_ID")) { + if(env.get("XDG_CURRENT_DESKTOP").toLowerCase(Locale.ENGLISH).contains("gnome")) { + env.put("GNOME_DESKTOP_SESSION_ID", "this-is-deprecated"); + } + } + } + return env; + } + +} diff --git a/old code/tray/src/qz/installer/MacInstaller.java b/old code/tray/src/qz/installer/MacInstaller.java new file mode 100755 index 0000000..b191faf --- /dev/null +++ b/old code/tray/src/qz/installer/MacInstaller.java @@ -0,0 +1,125 @@ +package qz.installer; +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.FileUtilities; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.List; + +import static qz.common.Constants.*; + +public class MacInstaller extends Installer { + protected static final Logger log = LogManager.getLogger(MacInstaller.class); + private static final String PACKAGE_NAME = getPackageName(); + public static final String LAUNCH_AGENT_PATH = String.format("/Library/LaunchAgents/%s.plist", MacInstaller.PACKAGE_NAME); + private String destination = "/Applications/" + ABOUT_TITLE + ".app"; + + public Installer addAppLauncher() { + // not needed; registered when "QZ Tray.app" is copied + return this; + } + + public Installer addStartupEntry() { + File dest = new File(LAUNCH_AGENT_PATH); + HashMap fieldMap = new HashMap<>(); + // Dynamic fields + fieldMap.put("%PACKAGE_NAME%", PACKAGE_NAME); + fieldMap.put("%COMMAND%", String.format("%s/Contents/MacOS/%s", destination, ABOUT_TITLE)); + fieldMap.put("%PARAM%", "--honorautostart"); + + try { + FileUtilities.configureAssetFile("assets/mac-launchagent.plist.in", dest, fieldMap, MacInstaller.class); + // Disable service until reboot + if(SystemUtilities.isMac()) { + ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH); + } + } catch(IOException e) { + log.warn("Unable to write startup file: {}", dest, e); + } + + return this; + } + + public void setDestination(String destination) { + this.destination = destination; + } + + public String getDestination() { + return destination; + } + + public Installer addSystemSettings() { + // Chrome protocol handler + String plist = "/Library/Preferences/com.google.Chrome.plist"; + if(ShellUtilities.execute(new String[] { "/usr/bin/defaults", "write", plist }, new String[] {DATA_DIR + "://*" }).isEmpty()) { + ShellUtilities.execute("/usr/bin/defaults", "write", plist, "URLAllowlist", "-array-add", DATA_DIR +"://*"); + } + return this; + } + public Installer removeSystemSettings() { + // Remove startup entry + File dest = new File(LAUNCH_AGENT_PATH); + dest.delete(); + return this; + } + + /** + * Removes legacy (<= 2.0) startup entries + */ + public Installer removeLegacyStartup() { + log.info("Removing startup entries for all users matching " + ABOUT_TITLE); + String script = "tell application \"System Events\" to delete " + + "every login item where name is \"" + ABOUT_TITLE + "\"" + + " or name is \"" + PROPS_FILE + ".jar\""; + + // Run on background thread in case System Events is hung or slow to respond + final String finalScript = script; + new Thread(() -> { + ShellUtilities.executeAppleScript(finalScript); + }).run(); + return this; + } + + public static String getPackageName() { + String packageName; + String[] parts = ABOUT_URL.split("\\W"); + if (parts.length >= 2) { + // Parse io.qz.qz-print from Constants + packageName = String.format("%s.%s.%s", parts[parts.length - 1], parts[parts.length - 2], PROPS_FILE); + } else { + // Fallback on something sane + packageName = "local." + PROPS_FILE; + } + return packageName; + } + + public void spawn(List args) throws Exception { + if(SystemUtilities.isAdmin()) { + // macOS unconventionally uses "$USER" during its install process + String sudoer = System.getenv("USER"); + if(sudoer == null || sudoer.isEmpty() || sudoer.equals("root")) { + // Fallback, should only fire via Terminal + sudo + sudoer = ShellUtilities.executeRaw("logname").trim(); + } + // Start directly without waitFor(...), avoids deadlocking + Runtime.getRuntime().exec(new String[] { "su", sudoer, "-c", "\"" + StringUtils.join(args, "\" \"") + "\""}); + } else { + Runtime.getRuntime().exec(args.toArray(new String[args.size()])); + } + } +} diff --git a/old code/tray/src/qz/installer/TaskKiller.java b/old code/tray/src/qz/installer/TaskKiller.java new file mode 100755 index 0000000..d18ad88 --- /dev/null +++ b/old code/tray/src/qz/installer/TaskKiller.java @@ -0,0 +1,227 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2021 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.installer; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; +import qz.utils.WindowsUtilities; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashSet; + +import static qz.common.Constants.PROPS_FILE; + +public class TaskKiller { + protected static final Logger log = LogManager.getLogger(TaskKiller.class); + private static final String[] JAR_NAMES = { + PROPS_FILE + ".jar", + "qz.App", // v2.2.0... + "qz.ws.PrintSocketServer" // v2.0.0...v2.1.6 + }; + private static final String[] KILL_PID_CMD_POSIX = { "kill", "-9" }; + private static final String[] KILL_PID_CMD_WIN32 = { "taskkill.exe", "/F", "/PID" }; + private static final String[] KILL_PID_CMD = SystemUtilities.isWindows() ? KILL_PID_CMD_WIN32 : KILL_PID_CMD_POSIX; + + /** + * Kills all QZ Tray processes, being careful not to kill itself + */ + public static boolean killAll() { + boolean success = true; + + // Disable service until reboot + if(SystemUtilities.isMac()) { + ShellUtilities.execute("/bin/launchctl", "unload", MacInstaller.LAUNCH_AGENT_PATH); + } + + // Use jcmd to get all java processes + HashSet pids = findPidsJcmd(); + if(!SystemUtilities.isWindows()) { + // Fallback to pgrep, needed for macOS (See JDK-8319589, JDK-8197387) + pids.addAll(findPidsPgrep()); + } else if(WindowsUtilities.isSystemAccount()) { + // Fallback to powershell, needed for Windows + pids.addAll(findPidsPwsh()); + } + + // Careful not to kill ourselves ;) + pids.remove(SystemUtilities.getProcessId()); + + // Kill each PID + String[] killPid = new String[KILL_PID_CMD.length + 1]; + System.arraycopy(KILL_PID_CMD, 0, killPid, 0, KILL_PID_CMD.length); + for (Integer pid : pids) { + killPid[killPid.length - 1] = pid.toString(); + success = success && ShellUtilities.execute(killPid); + } + + return success; + } + + private static Path getJcmdPath() throws IOException { + Path jcmd; + if(SystemUtilities.isWindows()) { + jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd.exe"); + } else if (SystemUtilities.isMac()) { + jcmd = SystemUtilities.getJarParentPath().resolve("../PlugIns/Java.runtime/Contents/Home/bin/jcmd"); + } else { + jcmd = SystemUtilities.getJarParentPath().resolve("runtime/bin/jcmd"); + } + if(!jcmd.toFile().exists()) { + log.error("Could not find {}", jcmd); + throw new IOException("Could not find jcmd, we can't use it for detecting running instances"); + } + return jcmd; + } + + + static final String[] PWSH_QUERY = { "powershell.exe", "-Command", "\"(Get-CimInstance Win32_Process -Filter \\\"Name = 'java.exe' OR Name = 'javaw.exe'\\\").Where({$_.CommandLine -like '*%s*'}).ProcessId\"" }; + + /** + * Leverage powershell.exe when run as SYSTEM to workaround https://github.com/qzind/tray/issues/1360 + * TODO: Remove when jcmd is patched to work as SYSTEM account + */ + private static HashSet findPidsPwsh() { + HashSet foundPids = new HashSet<>(); + + for(String jarName : JAR_NAMES) { + String[] pwshQuery = PWSH_QUERY.clone(); + int lastIndex = pwshQuery.length - 1; + // Format the last element to contain the jarName + pwshQuery[lastIndex] = String.format(pwshQuery[lastIndex], jarName); + String stdout = ShellUtilities.executeRaw(pwshQuery); + String[] lines = stdout.split("\\s*\\r?\\n"); + for(String line : lines) { + if(line.trim().isEmpty()) { + // Don't try to process blank lines + continue; + } + + int pid = parsePid(line); + if (pid >= 0) { + foundPids.add(pid); + } else { + log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout); + } + } + } + + return foundPids; + } + + /** + * Use pgrep to fetch all PIDs to workaround https://github.com/openjdk/jdk/pull/25824 + * TODO: Remove when jcmd is patched to work properly on macOS + */ + private static HashSet findPidsPgrep() { + HashSet foundPids = new HashSet<>(); + + for(String jarName : JAR_NAMES) { + String stdout = ShellUtilities.executeRaw("pgrep", "-f", jarName); + String[] lines = stdout.split("\\s*\\r?\\n"); + for(String line : lines) { + if(line.trim().isEmpty()) { + // Don't try to process blank lines + continue; + } + + int pid = parsePid(line); + if (pid >= 0) { + foundPids.add(pid); + } else { + log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout); + } + } + } + + return foundPids; + } + + + /** + * Uses jcmd to fetch all PIDs that match this product + */ + private static HashSet findPidsJcmd() { + HashSet foundPids = new HashSet<>(); + + String stdout; + String[] lines; + try { + stdout = ShellUtilities.executeRaw(getJcmdPath().toString(), "-l"); + if(stdout == null) { + log.error("Error calling '{}' {}", getJcmdPath(), "-l"); + return foundPids; + } + lines = stdout.split("\\r?\\n"); + } catch(Exception e) { + log.error(e); + return foundPids; + } + + for(String line : lines) { + if (line.trim().isEmpty()) { + // Don't try to process blank lines + continue; + } + // e.g. "35446 C:\Program Files\QZ Tray\qz-tray.jar" + String[] parts = line.split(" ", 2); + int pid = parsePid(parts); + if (pid >= 0) { + String args = parseArgs(parts); + if (args == null) { + log.warn("Found PID value '{}' but no args to match. Full line: '{}', Full output: '{}'", pid, line, stdout); + continue; + } + for(String jarName : JAR_NAMES) { + if (args.contains(jarName)) { + foundPids.add(pid); + break; // continue parent loop + } + } + } else { + log.warn("Could not parse PID value. Full line: '{}', Full output: '{}'", line, stdout); + } + } + + return foundPids; + } + + // Returns the second index of a String[], trimmed + private static String parseArgs(String[] input) { + if(input != null) { + if(input.length == 2) { + return input[1].trim(); + } + } + return null; + } + + // Parses an int value form the first index of a String[], returning -1 if something went wrong + private static int parsePid(String[] input) { + if(input != null) { + if(input.length == 2) { + return parsePid(input[0]); + } + } + return -1; + } + + // Parses an int value form the provided string, returning -1 if something went wrong + private static int parsePid(String input) { + String pidString = input.trim(); + if(StringUtils.isNumeric(pidString)) { + return Integer.parseInt(pidString); + } + return -1; + } +} diff --git a/old code/tray/src/qz/installer/WindowsInstaller.java b/old code/tray/src/qz/installer/WindowsInstaller.java new file mode 100755 index 0000000..41e4f5f --- /dev/null +++ b/old code/tray/src/qz/installer/WindowsInstaller.java @@ -0,0 +1,208 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer; + +import com.sun.jna.platform.win32.*; +import mslinks.ShellLink; +import mslinks.ShellLinkException; +import mslinks.ShellLinkHelper; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; +import qz.utils.WindowsUtilities; +import qz.ws.PrintSocketServer; + +import javax.swing.*; + +import static qz.common.Constants.*; +import static qz.installer.WindowsSpecialFolders.*; +import static com.sun.jna.platform.win32.WinReg.*; + +import java.io.File; +import java.io.IOException; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + + +public class WindowsInstaller extends Installer { + protected static final Logger log = LogManager.getLogger(WindowsInstaller.class); + private String destination = getDefaultDestination(); + private String destinationExe = getDefaultDestination() + File.separator + PROPS_FILE + ".exe"; + + public void setDestination(String destination) { + this.destination = destination; + this.destinationExe = destination + File.separator + PROPS_FILE + ".exe"; + } + + /** + * Cycles through registry keys removing legacy (<= 2.0) startup entries + */ + public Installer removeLegacyStartup() { + log.info("Removing legacy startup entries for all users matching " + ABOUT_TITLE); + for (String user : Advapi32Util.registryGetKeys(HKEY_USERS)) { + WindowsUtilities.deleteRegValue(HKEY_USERS, user.trim() + "\\Software\\Microsoft\\Windows\\CurrentVersion\\Run", ABOUT_TITLE); + } + + try { + FileUtils.deleteQuietly(new File(STARTUP + File.separator + ABOUT_TITLE + ".lnk")); + } catch(Win32Exception ignore) {} + + return this; + } + + public Installer addAppLauncher() { + try { + // Delete old 2.0 launcher + FileUtils.deleteQuietly(new File(COMMON_START_MENU + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk")); + Path loc = Paths.get(COMMON_START_MENU.toString(), "Programs", ABOUT_TITLE); + loc.toFile().mkdirs(); + String lnk = loc + File.separator + ABOUT_TITLE + ".lnk"; + String exe = destination + File.separator + PROPS_FILE+ ".exe"; + log.info("Creating launcher \"{}\" -> \"{}\"", lnk, exe); + ShellLinkHelper.createLink(exe, lnk); + } catch(ShellLinkException | IOException | Win32Exception e) { + log.warn("Could not create launcher", e); + } + return this; + } + + public Installer addStartupEntry() { + try { + String lnk = WindowsSpecialFolders.COMMON_STARTUP + File.separator + ABOUT_TITLE + ".lnk"; + String exe = destination + File.separator + PROPS_FILE+ ".exe"; + log.info("Creating startup entry \"{}\" -> \"{}\"", lnk, exe); + ShellLink link = ShellLinkHelper.createLink(exe, lnk).getLink(); + link.setCMDArgs("--honorautostart"); // honors auto-start preferences + } catch(ShellLinkException | IOException | Win32Exception e) { + log.warn("Could not create startup launcher", e); + } + return this; + } + public Installer removeSystemSettings() { + // Cleanup registry + WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\" + ABOUT_TITLE); + WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, "Software\\" + ABOUT_TITLE); + WindowsUtilities.deleteRegKey(HKEY_LOCAL_MACHINE, DATA_DIR); + // Chrome protocol handler + WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR)); + // Deprecated Chrome protocol handler + WindowsUtilities.deleteRegData(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLWhitelist", String.format("%s://*", DATA_DIR)); + + // Cleanup launchers + for(WindowsSpecialFolders folder : new WindowsSpecialFolders[] { START_MENU, COMMON_START_MENU, DESKTOP, PUBLIC_DESKTOP, COMMON_STARTUP, RECENT }) { + try { + new File(folder + File.separator + ABOUT_TITLE + ".lnk").delete(); + // Since 2.1, start menus use subfolder + if (folder.equals(COMMON_START_MENU) || folder.equals(START_MENU)) { + FileUtils.deleteQuietly(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE + ".lnk")); + FileUtils.deleteDirectory(new File(folder + File.separator + "Programs" + File.separator + ABOUT_TITLE)); + } + } catch(InvalidPathException | IOException | Win32Exception ignore) {} + } + + // Cleanup firewall rules + ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE)); + return this; + } + + public Installer addSystemSettings() { + /** + * TODO: Upgrade JNA! + * 64-bit registry view is currently invoked by nsis (windows-installer.nsi.in) using SetRegView 64 + * However, newer version of JNA offer direct WinNT.KEY_WOW64_64KEY registry support, safeguarding + * against direct calls to "java -jar qz-tray.jar install|keygen|etc", which will be needed moving forward + * for support and troubleshooting. + */ + + // Mime-type support e.g. qz:launch + WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "", String.format("URL:%s Protocol", ABOUT_TITLE)); + WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, DATA_DIR, "URL Protocol", ""); + WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\DefaultIcon", DATA_DIR), "", String.format("\"%s\",1", destinationExe)); + WindowsUtilities.addRegValue(HKEY_CLASSES_ROOT, String.format("%s\\shell\\open\\command", DATA_DIR), "", String.format("\"%s\" \"%%1\"", destinationExe)); + + /// Uninstall info + String uninstallKey = String.format("Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\%s", ABOUT_TITLE); + WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, String.format("Software\\%s", ABOUT_TITLE), "", destination); + WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayName", String.format("%s %s", ABOUT_TITLE, VERSION)); + WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "Publisher", ABOUT_COMPANY); + WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "UninstallString", destination + File.separator + "uninstall.exe"); + WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayIcon", destinationExe); + WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "HelpLink", ABOUT_SUPPORT_URL ); + WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLUpdateInfo", ABOUT_DOWNLOAD_URL); + WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "URLInfoAbout", ABOUT_SUPPORT_URL); + WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "DisplayVersion", VERSION.toString()); + WindowsUtilities.addRegValue(HKEY_LOCAL_MACHINE, uninstallKey, "EstimatedSize", FileUtils.sizeOfDirectoryAsBigInteger(new File(destination)).intValue() / 1024); + + // Chrome protocol handler + WindowsUtilities.addNumberedRegValue(HKEY_LOCAL_MACHINE, "SOFTWARE\\Policies\\Google\\Chrome\\URLAllowlist", String.format("%s://*", DATA_DIR)); + + // Firewall rules + ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "delete", "rule", String.format("name=%s", ABOUT_TITLE)); + ShellUtilities.execute("netsh.exe", "advfirewall", "firewall", "add", "rule", String.format("name=%s", ABOUT_TITLE), + "dir=in", "action=allow", "profile=any", String.format("localport=%s", websocketPorts.allPortsAsString()), "localip=any", "protocol=tcp"); + return this; + } + + @Override + public Installer addUserSettings() { + // Whitelist loopback for IE/Edge + if(ShellUtilities.execute("CheckNetIsolation.exe", "LoopbackExempt", "-a", "-n=Microsoft.MicrosoftEdge_8wekyb3d8bbwe")) { + log.warn("Could not whitelist loopback connections for IE, Edge"); + } + + try { + // Intranet settings; uncheck "include sites not listed in other zones" + String key = "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings\\Zones\\1"; + String value = "Flags"; + if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) { + int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value); + // remove value using bitwise XOR + Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data ^ 16); + } + + // Legacy Edge loopback support + key = "Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\AppContainer\\Storage\\microsoft.microsoftedge_8wekyb3d8bbwe\\MicrosoftEdge\\ExperimentalFeatures"; + value = "AllowLocalhostLoopback"; + if (Advapi32Util.registryKeyExists(HKEY_CURRENT_USER, key) && Advapi32Util.registryValueExists(HKEY_CURRENT_USER, key, value)) { + int data = Advapi32Util.registryGetIntValue(HKEY_CURRENT_USER, key, value); + // remove value using bitwise OR + Advapi32Util.registrySetIntValue(HKEY_CURRENT_USER, key, value, data | 1); + } + } catch(Exception e) { + log.warn("An error occurred configuring the \"Local Intranet Zone\"; connections to \"localhost\" may fail", e); + } + return super.addUserSettings(); + } + + public static String getDefaultDestination() { + String path = System.getenv("ProgramW6432"); + if (path == null || path.trim().isEmpty()) { + path = System.getenv("ProgramFiles"); + } + return path + File.separator + ABOUT_TITLE; + } + + public String getDestination() { + return destination; + } + + public void spawn(List args) throws Exception { + if(SystemUtilities.isAdmin()) { + log.warn("Spawning as user isn't implemented; starting process with elevation instead"); + } + ShellUtilities.execute(args.toArray(new String[args.size()])); + } +} diff --git a/old code/tray/src/qz/installer/WindowsSpecialFolders.java b/old code/tray/src/qz/installer/WindowsSpecialFolders.java new file mode 100755 index 0000000..362c195 --- /dev/null +++ b/old code/tray/src/qz/installer/WindowsSpecialFolders.java @@ -0,0 +1,97 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer; + +import com.sun.jna.platform.win32.*; +import qz.utils.WindowsUtilities; + +/** + * Windows XP-compatible special folder's wrapper for JNA + * + */ +public enum WindowsSpecialFolders { + ADMIN_TOOLS(ShlObj.CSIDL_ADMINTOOLS, KnownFolders.FOLDERID_AdminTools), + STARTUP_ALT(ShlObj.CSIDL_ALTSTARTUP, KnownFolders.FOLDERID_Startup), + ROAMING_APPDATA(ShlObj.CSIDL_APPDATA, KnownFolders.FOLDERID_RoamingAppData), + RECYCLING_BIN(ShlObj.CSIDL_BITBUCKET, KnownFolders.FOLDERID_RecycleBinFolder), + CD_BURNING(ShlObj.CSIDL_CDBURN_AREA, KnownFolders.FOLDERID_CDBurning), + COMMON_ADMIN_TOOLS(ShlObj.CSIDL_COMMON_ADMINTOOLS, KnownFolders.FOLDERID_CommonAdminTools), + COMMON_STARTUP_ALT(ShlObj.CSIDL_COMMON_ALTSTARTUP, KnownFolders.FOLDERID_CommonStartup), + PROGRAM_DATA(ShlObj.CSIDL_COMMON_APPDATA, KnownFolders.FOLDERID_ProgramData), + PUBLIC_DESKTOP(ShlObj.CSIDL_COMMON_DESKTOPDIRECTORY, KnownFolders.FOLDERID_PublicDesktop), + PUBLIC_DOCUMENTS(ShlObj.CSIDL_COMMON_DOCUMENTS, KnownFolders.FOLDERID_PublicDocuments), + COMMON_FAVORITES(ShlObj.CSIDL_COMMON_FAVORITES, KnownFolders.FOLDERID_Favorites), + COMMON_MUSIC(ShlObj.CSIDL_COMMON_MUSIC, KnownFolders.FOLDERID_PublicMusic), + COMMON_OEM_LINKS(ShlObj.CSIDL_COMMON_OEM_LINKS, KnownFolders.FOLDERID_CommonOEMLinks), + COMMON_PICTURES(ShlObj.CSIDL_COMMON_PICTURES, KnownFolders.FOLDERID_PublicPictures), + COMMON_PROGRAMS(ShlObj.CSIDL_COMMON_PROGRAMS, KnownFolders.FOLDERID_CommonPrograms), + COMMON_START_MENU(ShlObj.CSIDL_COMMON_STARTMENU, KnownFolders.FOLDERID_CommonStartMenu), + COMMON_STARTUP(ShlObj.CSIDL_COMMON_STARTUP, KnownFolders.FOLDERID_CommonStartup), + COMMON_TEMPLATES(ShlObj.CSIDL_COMMON_TEMPLATES, KnownFolders.FOLDERID_CommonTemplates), + COMMON_VIDEO(ShlObj.CSIDL_COMMON_VIDEO, KnownFolders.FOLDERID_PublicVideos), + COMPUTERS_NEAR_ME(ShlObj.CSIDL_COMPUTERSNEARME, KnownFolders.FOLDERID_NetworkFolder), + CONNECTIONS_FOLDER(ShlObj.CSIDL_CONNECTIONS, KnownFolders.FOLDERID_ConnectionsFolder), + CONTROL_PANEL(ShlObj.CSIDL_CONTROLS, KnownFolders.FOLDERID_ControlPanelFolder), + COOKIES(ShlObj.CSIDL_COOKIES, KnownFolders.FOLDERID_Cookies), + DESKTOP_VIRTUAL(ShlObj.CSIDL_DESKTOP, KnownFolders.FOLDERID_Desktop), + DESKTOP(ShlObj.CSIDL_DESKTOPDIRECTORY, KnownFolders.FOLDERID_Desktop), + COMPUTER_FOLDER(ShlObj.CSIDL_DRIVES, KnownFolders.FOLDERID_ComputerFolder), + FAVORITES(ShlObj.CSIDL_FAVORITES, KnownFolders.FOLDERID_Favorites), + FONTS(ShlObj.CSIDL_FONTS, KnownFolders.FOLDERID_Fonts), + HISTORY(ShlObj.CSIDL_HISTORY, KnownFolders.FOLDERID_History), + INTERNET_FOLDER(ShlObj.CSIDL_INTERNET, KnownFolders.FOLDERID_InternetFolder), + INTERNET_CACHE(ShlObj.CSIDL_INTERNET_CACHE, KnownFolders.FOLDERID_InternetCache), + LOCAL_APPDATA(ShlObj.CSIDL_LOCAL_APPDATA, KnownFolders.FOLDERID_LocalAppData), + MY_DOCUMENTS(ShlObj.CSIDL_MYDOCUMENTS, KnownFolders.FOLDERID_Documents), + MY_MUSIC(ShlObj.CSIDL_MYMUSIC, KnownFolders.FOLDERID_Music), + MY_PICTURES(ShlObj.CSIDL_MYPICTURES, KnownFolders.FOLDERID_Pictures), + MY_VIDEOS(ShlObj.CSIDL_MYVIDEO, KnownFolders.FOLDERID_Videos), + NETWORK_NEIGHBORHOOD(ShlObj.CSIDL_NETHOOD, KnownFolders.FOLDERID_NetHood), + NETWORK_FOLDER(ShlObj.CSIDL_NETWORK, KnownFolders.FOLDERID_NetworkFolder), + PERSONAL_FOLDDER(ShlObj.CSIDL_PERSONAL, KnownFolders.FOLDERID_Documents), + PRINTERS(ShlObj.CSIDL_PRINTERS, KnownFolders.FOLDERID_PrintersFolder), + PRINTING_NEIGHBORHOODD(ShlObj.CSIDL_PRINTHOOD, KnownFolders.FOLDERID_PrintHood), + PROFILE_FOLDER(ShlObj.CSIDL_PROFILE, KnownFolders.FOLDERID_Profile), + PROGRAM_FILES(ShlObj.CSIDL_PROGRAM_FILES, KnownFolders.FOLDERID_ProgramFiles), + PROGRAM_FILESX86(ShlObj.CSIDL_PROGRAM_FILESX86, KnownFolders.FOLDERID_ProgramFilesX86), + PROGRAM_FILES_COMMON(ShlObj.CSIDL_PROGRAM_FILES_COMMON, KnownFolders.FOLDERID_ProgramFilesCommon), + PROGRAM_FILES_COMMONX86(ShlObj.CSIDL_PROGRAM_FILES_COMMONX86, KnownFolders.FOLDERID_ProgramFilesCommonX86), + PROGRAMS(ShlObj.CSIDL_PROGRAMS, KnownFolders.FOLDERID_Programs), + RECENT(ShlObj.CSIDL_RECENT, KnownFolders.FOLDERID_Recent), + RESOURCES(ShlObj.CSIDL_RESOURCES, KnownFolders.FOLDERID_ResourceDir), + RESOURCES_LOCALIZED(ShlObj.CSIDL_RESOURCES_LOCALIZED, KnownFolders.FOLDERID_LocalizedResourcesDir), + SEND_TO(ShlObj.CSIDL_SENDTO, KnownFolders.FOLDERID_SendTo), + START_MENU(ShlObj.CSIDL_STARTMENU, KnownFolders.FOLDERID_StartMenu), + STARTUP(ShlObj.CSIDL_STARTUP, KnownFolders.FOLDERID_Startup), + SYSTEM(ShlObj.CSIDL_SYSTEM, KnownFolders.FOLDERID_System), + SYSTEMX86(ShlObj.CSIDL_SYSTEMX86, KnownFolders.FOLDERID_SystemX86), + TEMPLATES(ShlObj.CSIDL_TEMPLATES, KnownFolders.FOLDERID_Templates), + WINDOWS(ShlObj.CSIDL_WINDOWS, KnownFolders.FOLDERID_Windows); + + private int csidl; + private Guid.GUID guid; + WindowsSpecialFolders(int csidl, Guid.GUID guid) { + this.csidl = csidl; + this.guid = guid; + } + + public String getPath() { + if(WindowsUtilities.isWindowsXP()) { + return Shell32Util.getSpecialFolderPath(csidl, false); + } + return Shell32Util.getKnownFolderPath(guid); + } + + @Override + public String toString() { + return getPath(); + } +} diff --git a/old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in b/old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in new file mode 100755 index 0000000..ae66835 --- /dev/null +++ b/old code/tray/src/qz/installer/assets/linux-shortcut.desktop.in @@ -0,0 +1,8 @@ +[Desktop Entry] +Type=Application +Name=%ABOUT_TITLE% +Exec="%COMMAND%" %PARAM% +Path=%DESTINATION% +Icon=%DESTINATION%/%LINUX_ICON% +MimeType=application/x-qz;x-scheme-handler/qz; +Terminal=false \ No newline at end of file diff --git a/old code/tray/src/qz/installer/assets/linux-udev.rules.in b/old code/tray/src/qz/installer/assets/linux-udev.rules.in new file mode 100755 index 0000000..506f274 --- /dev/null +++ b/old code/tray/src/qz/installer/assets/linux-udev.rules.in @@ -0,0 +1,2 @@ +# %ABOUT_TITLE% usb override settings +SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0666" diff --git a/old code/tray/src/qz/installer/assets/mac-launchagent.plist.in b/old code/tray/src/qz/installer/assets/mac-launchagent.plist.in new file mode 100755 index 0000000..69f214f --- /dev/null +++ b/old code/tray/src/qz/installer/assets/mac-launchagent.plist.in @@ -0,0 +1,18 @@ + + + + + Label%PACKAGE_NAME% + KeepAlive + + SuccessfulExit + AfterInitialDemand + + RunAtLoad + ProgramArguments + + %COMMAND% + %PARAM% + + + \ No newline at end of file diff --git a/old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java b/old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java new file mode 100755 index 0000000..88d11f4 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/CertificateChainBuilder.java @@ -0,0 +1,147 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate; + +import java.io.IOException; +import java.math.BigInteger; +import java.security.*; +import java.util.Calendar; +import java.util.Locale; + +import org.bouncycastle.asn1.*; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.X500NameBuilder; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.*; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.X509v3CertificateBuilder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509ExtensionUtils; +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.operator.ContentSigner; +import org.bouncycastle.operator.OperatorException; +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder; +import qz.common.Constants; +import qz.utils.SystemUtilities; + +import static qz.installer.certificate.KeyPairWrapper.Type.*; + +public class CertificateChainBuilder { + public static final String[] DEFAULT_HOSTNAMES = {"localhost", "localhost.qz.io" }; + + private static int KEY_SIZE = 2048; + public static int CA_CERT_AGE = 7305; // 20 years + public static int SSL_CERT_AGE = 825; // Per https://support.apple.com/HT210176 + + private String[] hostNames; + + public CertificateChainBuilder(String ... hostNames) { + Security.addProvider(new BouncyCastleProvider()); + if(hostNames.length > 0) { + this.hostNames = hostNames; + } else { + this.hostNames = DEFAULT_HOSTNAMES; + } + } + + public KeyPairWrapper createCaCert() throws IOException, GeneralSecurityException, OperatorException { + KeyPair keyPair = createRsaKey(); + + X509v3CertificateBuilder builder = createX509Cert(keyPair, CA_CERT_AGE, hostNames); + + builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(1)) + .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.keyCertSign + KeyUsage.cRLSign)) + .addExtension(Extension.subjectKeyIdentifier, false, new JcaX509ExtensionUtils().createSubjectKeyIdentifier(keyPair.getPublic())); + + // Signing + ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(keyPair.getPrivate()); + X509CertificateHolder certHolder = builder.build(sign); + + // Convert to java-friendly format + return new KeyPairWrapper(CA, keyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder)); + } + + public KeyPairWrapper createSslCert(KeyPairWrapper caKeyPairWrapper) throws IOException, GeneralSecurityException, OperatorException { + KeyPair sslKeyPair = createRsaKey(); + X509v3CertificateBuilder builder = createX509Cert(sslKeyPair, SSL_CERT_AGE, hostNames); + + JcaX509ExtensionUtils utils = new JcaX509ExtensionUtils(); + + builder.addExtension(Extension.authorityKeyIdentifier, false, utils.createAuthorityKeyIdentifier(caKeyPairWrapper.getCert())) + .addExtension(Extension.basicConstraints, true, new BasicConstraints(false)) + .addExtension(Extension.keyUsage, true, new KeyUsage(KeyUsage.digitalSignature + KeyUsage.keyEncipherment)) + .addExtension(Extension.extendedKeyUsage, false, new ExtendedKeyUsage(new KeyPurposeId[]{KeyPurposeId.id_kp_serverAuth, KeyPurposeId.id_kp_clientAuth})) + .addExtension(Extension.subjectAlternativeName, false, buildSan(hostNames)) + .addExtension(Extension.subjectKeyIdentifier, false, utils.createSubjectKeyIdentifier(sslKeyPair.getPublic())); + + // Signing + ContentSigner sign = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC").build(caKeyPairWrapper.getKey()); + X509CertificateHolder certHolder = builder.build(sign); + + // Convert to java-friendly format + return new KeyPairWrapper(SSL, sslKeyPair, new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder)); + } + + private static KeyPair createRsaKey() throws GeneralSecurityException { + KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA", "BC"); + keyPairGenerator.initialize(KEY_SIZE, new SecureRandom()); + return keyPairGenerator.generateKeyPair(); + } + + private static X509v3CertificateBuilder createX509Cert(KeyPair keyPair, int age, String ... hostNames) { + String cn = hostNames.length > 0? hostNames[0]:DEFAULT_HOSTNAMES[0]; + X500Name name = new X500NameBuilder() + .addRDN(BCStyle.C, Constants.ABOUT_COUNTRY) + .addRDN(BCStyle.ST, Constants.ABOUT_STATE) + .addRDN(BCStyle.L, Constants.ABOUT_CITY) + .addRDN(BCStyle.O, Constants.ABOUT_COMPANY) + .addRDN(BCStyle.OU, Constants.ABOUT_COMPANY) + .addRDN(BCStyle.EmailAddress, Constants.ABOUT_EMAIL) + .addRDN(BCStyle.CN, cn) + .build(); + BigInteger serial = BigInteger.valueOf(System.currentTimeMillis()); + Calendar notBefore = Calendar.getInstance(Locale.ENGLISH); + Calendar notAfter = Calendar.getInstance(Locale.ENGLISH); + notBefore.add(Calendar.DAY_OF_YEAR, -1); + notAfter.add(Calendar.DAY_OF_YEAR, age - 1); + + SystemUtilities.swapLocale(); + X509v3CertificateBuilder x509builder = new JcaX509v3CertificateBuilder(name, serial, notBefore.getTime(), notAfter.getTime(), name, keyPair.getPublic()); + SystemUtilities.restoreLocale(); + return x509builder; + } + + /** + * Builds subjectAlternativeName extension; iterates and detects IPv4 or hostname + */ + private static GeneralNames buildSan(String ... hostNames) { + GeneralName[] gn = new GeneralName[hostNames.length]; + for (int i = 0; i < hostNames.length; i++) { + int gnType = isIp(hostNames[i]) ? GeneralName.iPAddress : GeneralName.dNSName; + gn[i] = new GeneralName(gnType, hostNames[i]); + } + return GeneralNames.getInstance(new DERSequence(gn)); + } + + private static boolean isIp(String ip) { + try { + String[] split = ip.split("\\."); + if (split.length != 4) return false; + for (int i = 0; i < 4; ++i) { + int p = Integer.parseInt(split[i]); + if (p > 255 || p < 0) return false; + } + return true; + } catch (Exception ignore) {} + return false; + } +} \ No newline at end of file diff --git a/old code/tray/src/qz/installer/certificate/CertificateManager.java b/old code/tray/src/qz/installer/certificate/CertificateManager.java new file mode 100755 index 0000000..ebf0445 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/CertificateManager.java @@ -0,0 +1,478 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate; + +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.asn1.x500.AttributeTypeAndValue; +import org.bouncycastle.asn1.x500.RDN; +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMKeyPair; +import org.bouncycastle.openssl.PEMParser; +import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.bouncycastle.operator.OperatorException; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.installer.Installer; +import qz.utils.ArgValue; +import qz.utils.FileUtilities; +import qz.utils.MacUtilities; +import qz.utils.SystemUtilities; + +import java.io.*; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.security.*; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.*; + +import static qz.utils.FileUtilities.*; +import static qz.installer.certificate.KeyPairWrapper.Type.*; + +/** + * Stores and maintains reading and writing of certificate related files + */ +public class CertificateManager { + static List SAVE_LOCATIONS = new ArrayList<>(); + static { + // Workaround for JDK-8266929 + // See also https://github.com/qzind/tray/issues/814 + SystemUtilities.clearAlgorithms(); + + // Skip shared location if running from IDE or build directory + // Prevents corrupting the version installed per https://github.com/qzind/tray/issues/1200 + if(SystemUtilities.isJar() && SystemUtilities.isInstalled()) { + // Skip install location if running from sandbox (must remain sealed) + if(!SystemUtilities.isMac() || !MacUtilities.isSandboxed()) { + SAVE_LOCATIONS.add(SystemUtilities.getJarParentPath()); + } + SAVE_LOCATIONS.add(SHARED_DIR); + } + SAVE_LOCATIONS.add(USER_DIR); + } + private static final Logger log = LogManager.getLogger(CertificateManager.class); + + public static String DEFAULT_KEYSTORE_FORMAT = "PKCS12"; + public static String DEFAULT_KEYSTORE_EXTENSION = ".p12"; + public static String DEFAULT_CERTIFICATE_EXTENSION = ".crt"; + private static int DEFAULT_PASSWORD_BITS = 100; + + private boolean needsInstall; + private SslContextFactory.Server sslContextFactory; + private KeyPairWrapper sslKeyPair; + private KeyPairWrapper caKeyPair; + + private Properties properties; + private char[] password; + + /** + * For internal certs + */ + public CertificateManager(boolean forceNew, String ... hostNames) throws IOException, GeneralSecurityException, OperatorException { + Security.addProvider(new BouncyCastleProvider()); + sslKeyPair = new KeyPairWrapper(SSL); + caKeyPair = new KeyPairWrapper(CA); + + if (!forceNew) { + // order is important: ssl, ca + properties = loadProperties(sslKeyPair, caKeyPair); + } + + if(properties == null) { + log.warn("Warning, SSL properties won't be loaded from disk... we'll try to create them..."); + + CertificateChainBuilder cb = new CertificateChainBuilder(hostNames); + caKeyPair = cb.createCaCert(); + sslKeyPair = cb.createSslCert(caKeyPair); + + // Create CA + properties = createKeyStore(CA) + .writeCert(CA) + .writeKeystore(null, CA); + + // Create SSL + properties = createKeyStore(SSL) + .writeCert(SSL) + .writeKeystore(properties, SSL); + + // Save properties + saveProperties(); + } + } + + /** + * For trusted PEM-formatted certs + */ + public CertificateManager(File trustedPemKey, File trustedPemCert) throws Exception { + Security.addProvider(new BouncyCastleProvider()); + needsInstall = false; + sslKeyPair = new KeyPairWrapper(SSL); + + // Assumes ssl/privkey.pem, ssl/fullchain.pem + properties = createTrustedKeystore(trustedPemKey, trustedPemCert) + .writeKeystore(properties, SSL); + + // Save properties + saveProperties(); + } + + /** + * For trusted PKCS12-formatted certs + */ + public CertificateManager(File pkcs12File, char[] password) throws Exception { + Security.addProvider(new BouncyCastleProvider()); + needsInstall = false; + sslKeyPair = new KeyPairWrapper(SSL); + + // Assumes direct pkcs12 import + this.password = password; + sslKeyPair.init(pkcs12File, password); + + // Save it back, but to a location we can find + properties = writeKeystore(null, SSL); + + // Save properties + saveProperties(); + } + + public void renewCertChain(String ... hostNames) throws Exception { + CertificateChainBuilder cb = new CertificateChainBuilder(hostNames); + sslKeyPair = cb.createSslCert(caKeyPair); + createKeyStore(SSL).writeKeystore(properties, SSL); + reloadSslContextFactory(); + } + + public KeyPairWrapper getSslKeyPair() { + return sslKeyPair; + } + + public KeyPairWrapper getCaKeyPair() { + return caKeyPair; + } + + public KeyPairWrapper getKeyPair(KeyPairWrapper.Type type) { + switch(type) { + case SSL: + return sslKeyPair; + case CA: + default: + return caKeyPair; + } + } + + public KeyPairWrapper getKeyPair(String alias) { + for(KeyPairWrapper.Type type : KeyPairWrapper.Type.values()) { + if (KeyPairWrapper.getAlias(type).equalsIgnoreCase(alias)) { + return getKeyPair(type); + } + } + return getKeyPair(KeyPairWrapper.Type.CA); + } + + public Properties getProperties() { + return properties; + } + + private char[] getPassword() { + if (password == null) { + if(caKeyPair != null && caKeyPair.getPassword() != null) { + // Reuse existing + password = caKeyPair.getPassword(); + } else { + // Create new + BigInteger bi = new BigInteger(DEFAULT_PASSWORD_BITS, new SecureRandom()); + password = bi.toString(16).toCharArray(); + log.info("Created a random {} bit password: {}", DEFAULT_PASSWORD_BITS, new String(password)); + } + } + return password; + } + + public SslContextFactory.Server configureSslContextFactory() { + sslContextFactory = new SslContextFactory.Server(); + sslContextFactory.setKeyStore(sslKeyPair.getKeyStore()); + sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString()); + sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString()); + return sslContextFactory; + } + + public void reloadSslContextFactory() throws Exception { + if(isSslActive()) { + sslContextFactory.reload(sslContextFactory -> { + sslContextFactory.setKeyStore(sslKeyPair.getKeyStore()); + sslContextFactory.setKeyStorePassword(sslKeyPair.getPasswordString()); + sslContextFactory.setKeyManagerPassword(sslKeyPair.getPasswordString()); + }); + } else { + log.warn("SSL isn't active, can't reload"); + } + } + + public boolean isSslActive() { + return sslContextFactory != null; + } + + public boolean needsInstall() { + return needsInstall; + } + + public CertificateManager createKeyStore(KeyPairWrapper.Type type) throws IOException, GeneralSecurityException { + KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair; + KeyStore keyStore = KeyStore.getInstance(DEFAULT_KEYSTORE_FORMAT); + keyStore.load(null, password); + + List chain = new ArrayList<>(); + chain.add(keyPair.getCert()); + + // Add ca to ssl cert chain + if (keyPair.getType() == SSL) { + chain.add(caKeyPair.getCert()); + } + keyStore.setEntry(caKeyPair.getAlias(), new KeyStore.TrustedCertificateEntry(caKeyPair.getCert()), null); + keyStore.setKeyEntry(keyPair.getAlias(), keyPair.getKey(), getPassword(), chain.toArray(new X509Certificate[chain.size()])); + keyPair.init(keyStore, getPassword()); + return this; + } + + public CertificateManager createTrustedKeystore(File p12Store, String password) throws Exception { + sslKeyPair = new KeyPairWrapper(SSL); + sslKeyPair.init(p12Store, password.toCharArray()); + return this; + } + + public CertificateManager createTrustedKeystore(File pemKey, File pemCert) throws Exception { + sslKeyPair = new KeyPairWrapper(SSL); + + // Private Key + PEMParser pem = new PEMParser(new FileReader(pemKey)); + Object parsedObject = pem.readObject(); + + PrivateKeyInfo privateKeyInfo = parsedObject instanceof PEMKeyPair ? ((PEMKeyPair)parsedObject).getPrivateKeyInfo() : (PrivateKeyInfo)parsedObject; + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyInfo.getEncoded()); + KeyFactory factory = KeyFactory.getInstance("RSA"); + PrivateKey key = factory.generatePrivate(privateKeySpec); + + List certs = new ArrayList<>(); + X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject(); + if(certHolder != null) { + certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder)); + } + + // Certificate + pem = new PEMParser(new FileReader(pemCert)); + while((certHolder = (X509CertificateHolder)pem.readObject()) != null) { + certs.add(new JcaX509CertificateConverter().setProvider("BC").getCertificate(certHolder)); + } + + // Keystore + KeyStore ks = KeyStore.getInstance("PKCS12"); + ks.load(null); + + for (int i = 0; i < certs.size(); i++) { + ks.setCertificateEntry(sslKeyPair.getAlias() + "_" + i, certs.get(i)); + } + + KeyStore keyStore = KeyStore.getInstance("PKCS12"); + keyStore.load(null); + keyStore.setKeyEntry(sslKeyPair.getAlias(), key, getPassword(), certs.toArray(new X509Certificate[certs.size()])); + + sslKeyPair.init(keyStore, getPassword()); + return this; + } + + public static void writeCert(X509Certificate data, File dest) throws IOException { + // PEMWriter doesn't always clear the file, explicitly delete it, see issue #796 + if(dest.exists()) { + dest.delete(); + } + JcaMiscPEMGenerator cert = new JcaMiscPEMGenerator(data); + JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(dest.toPath(), StandardOpenOption.CREATE))); + writer.writeObject(cert.generate()); + writer.close(); + FileUtilities.inheritParentPermissions(dest.toPath()); + log.info("Wrote Cert: \"{}\"", dest); + } + + public CertificateManager writeCert(KeyPairWrapper.Type type) throws IOException { + KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair; + File certFile = new File(getWritableLocation("ssl"), keyPair.getAlias() + DEFAULT_CERTIFICATE_EXTENSION); + + writeCert(keyPair.getCert(), certFile); + FileUtilities.inheritParentPermissions(certFile.toPath()); + if(keyPair.getType() == CA) { + needsInstall = true; + } + return this; + } + + public Properties writeKeystore(Properties props, KeyPairWrapper.Type type) throws GeneralSecurityException, IOException { + File sslDir = getWritableLocation("ssl"); + KeyPairWrapper keyPair = type == CA ? caKeyPair : sslKeyPair; + + File keyFile = new File(sslDir, keyPair.getAlias() + DEFAULT_KEYSTORE_EXTENSION); + keyPair.getKeyStore().store(Files.newOutputStream(keyFile.toPath(), StandardOpenOption.CREATE), getPassword()); + FileUtilities.inheritParentPermissions(keyFile.toPath()); + log.info("Wrote {} Key: \"{}\"", DEFAULT_KEYSTORE_FORMAT, keyFile); + + if (props == null) { + props = new Properties(); + } + props.putIfAbsent(String.format("%s.keystore", keyPair.propsPrefix()), keyFile.toString()); + props.putIfAbsent(String.format("%s.storepass", keyPair.propsPrefix()), new String(getPassword())); + props.putIfAbsent(String.format("%s.alias", keyPair.propsPrefix()), keyPair.getAlias()); + + if (keyPair.getType() == SSL) { + props.putIfAbsent(String.format("%s.host", keyPair.propsPrefix()), ArgValue.SECURITY_WSS_HOST.getDefaultVal()); + } + + + return props; + } + + public static File getWritableLocation(String ... suffixes) throws IOException { + // Get an array of preferred directories + ArrayList locs = new ArrayList<>(); + + if (suffixes.length == 0) { + locs.addAll(SAVE_LOCATIONS); + // Last, fallback on a directory we won't ever see again :/ + locs.add(TEMP_DIR); + } else { + // Same as above, but with suffixes added (usually "ssl"), skipping the install location + for(Path saveLocation : SAVE_LOCATIONS) { + if(!saveLocation.equals(SystemUtilities.getJarParentPath())) { + locs.add(Paths.get(saveLocation.toString(), suffixes)); + } + } + // Last, fallback on a directory we won't ever see again :/ + locs.add(Paths.get(TEMP_DIR.toString(), suffixes)); + } + + // Find a suitable write location + File path; + for(Path loc : locs) { + if (loc == null) continue; + boolean isPreferred = locs.indexOf(loc) == 0; + path = loc.toFile(); + path.mkdirs(); + if (path.canWrite()) { + log.debug("Writing to {}", loc); + if(!isPreferred) { + log.warn("Warning, {} isn't the preferred write location, but we'll use it anyway", loc); + } + return path; + } else { + log.debug("Can't write to {}, trying the next...", loc); + } + } + throw new IOException("Can't find a suitable write location. SSL will fail."); + } + + public static Properties loadProperties(KeyPairWrapper... keyPairs) { + log.info("Try to find SSL properties file..."); + + + Properties props = null; + for(Path loc : SAVE_LOCATIONS) { + if (loc == null) continue; + try { + for(KeyPairWrapper keyPair : keyPairs) { + props = loadKeyPair(keyPair, loc, props); + } + // We've loaded without Exception, return + log.info("Found {}/{}.properties", loc, Constants.PROPS_FILE); + return props; + } catch(Exception ignore) { + log.warn("Properties couldn't be loaded at {}, trying fallback...", loc, ignore); + } + } + log.info("Could not get SSL properties from file."); + return null; + } + + public static Properties loadKeyPair(KeyPairWrapper keyPair, Path parent, Properties existing) throws Exception { + Properties props; + + if (existing == null) { + FileInputStream fis = null; + try { + props = new Properties(); + props.load(fis = new FileInputStream(new File(parent.toFile(), Constants.PROPS_FILE + ".properties"))); + } finally { + if(fis != null) fis.close(); + } + } else { + props = existing; + } + + String ks = props.getProperty(String.format("%s.keystore", keyPair.propsPrefix())); + String pw = props.getProperty(String.format("%s.storepass", keyPair.propsPrefix()), ""); + + if(ks == null || ks.trim().isEmpty()) { + if(keyPair.getType() == SSL) { + throw new IOException("Missing wss.keystore entry"); + } else { + // CA is only needed for internal certs, return + return props; + } + } + File ksFile = Paths.get(ks).isAbsolute()? new File(ks):new File(parent.toFile(), ks); + if (ksFile.exists()) { + keyPair.init(ksFile, pw.toCharArray()); + return props; + } + return null; + } + + private void saveProperties() throws IOException { + File propsFile = new File(getWritableLocation(), Constants.PROPS_FILE + ".properties"); + Installer.persistProperties(propsFile, properties); // checks for props from previous install + properties.store(new FileOutputStream(propsFile), null); + FileUtilities.inheritParentPermissions(propsFile.toPath()); + log.info("Successfully created SSL properties file: {}", propsFile); + } + + public static boolean emailMatches(X509Certificate cert) { + return emailMatches(cert, false); + } + + public static boolean emailMatches(X509Certificate cert, boolean quiet) { + try { + X500Name x500name = new JcaX509CertificateHolder(cert).getSubject(); + RDN[] emailNames = x500name.getRDNs(BCStyle.E); + for(RDN emailName : emailNames) { + AttributeTypeAndValue first = emailName.getFirst(); + if (first != null && first.getValue() != null && Constants.ABOUT_EMAIL.equals(first.getValue().toString())) { + if(!quiet) { + log.info("Email address {} found, assuming CertProvider is {}", Constants.ABOUT_EMAIL, ExpiryTask.CertProvider.INTERNAL); + } + return true; + } + } + } + catch(Exception ignore) {} + if(!quiet) { + log.info("Email address {} was not found. Assuming the certificate is manually installed, we won't try to renew it.", Constants.ABOUT_EMAIL); + } + return false; + } +} diff --git a/old code/tray/src/qz/installer/certificate/ExpiryTask.java b/old code/tray/src/qz/installer/certificate/ExpiryTask.java new file mode 100755 index 0000000..34c86eb --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/ExpiryTask.java @@ -0,0 +1,295 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate; + +import org.bouncycastle.asn1.x500.X500Name; +import org.bouncycastle.asn1.x500.style.BCStyle; +import org.bouncycastle.asn1.x509.GeneralName; +import org.bouncycastle.cert.jcajce.JcaX509CertificateHolder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.*; + +import static qz.utils.FileUtilities.*; + +public class ExpiryTask extends TimerTask { + private static final Logger log = LogManager.getLogger(CertificateManager.class); + public static final int DEFAULT_INITIAL_DELAY = 60 * 1000; // 1 minute + public static final int DEFAULT_CHECK_FREQUENCY = 3600 * 1000; // 1 hour + private static final int DEFAULT_GRACE_PERIOD_DAYS = 5; + private enum ExpiryState {VALID, EXPIRING, EXPIRED, MANAGED} + + public enum CertProvider { + INTERNAL(Constants.ABOUT_COMPANY + ".*"), + LETS_ENCRYPT("Let's Encrypt.*"), + CA_CERT_ORG("CA Cert Signing.*"), + UNKNOWN; + String[] patterns; + CertProvider(String ... regexPattern) { + this.patterns = regexPattern; + } + } + + private Timer timer; + private CertificateManager certificateManager; + private String[] hostNames; + private CertProvider certProvider; + + public ExpiryTask(CertificateManager certificateManager) { + super(); + this.certificateManager = certificateManager; + this.hostNames = parseHostNames(); + this.certProvider = findCertProvider(); + } + + @Override + public void run() { + // Check for expiration + ExpiryState state = getExpiry(certificateManager.getSslKeyPair().getCert()); + switch(state) { + case EXPIRING: + case EXPIRED: + log.info("Certificate ExpiryState {}, renewing/reloading...", state); + switch(certProvider) { + case INTERNAL: + if(renewInternalCert()) { + getExpiry(); + } + break; + case CA_CERT_ORG: + case LETS_ENCRYPT: + if(renewExternalCert(certProvider)) { + getExpiry(); + } + break; + case UNKNOWN: + default: + log.warn("Certificate can't be renewed/reloaded; ExpiryState: {}, CertProvider: {}", state, certProvider); + } + case VALID: + default: + } + + } + + public boolean renewInternalCert() { + try { + log.info("Requesting a new SSL certificate from {} ...", certificateManager.getCaKeyPair().getAlias()); + certificateManager.renewCertChain(hostNames); + log.info("New SSL certificate created. Reloading SslContextFactory..."); + certificateManager.reloadSslContextFactory(); + log.info("Reloaded SSL successfully."); + return true; + } + catch(Exception e) { + log.error("Could not reload SSL certificate", e); + } + return false; + } + + public ExpiryState getExpiry() { + return getExpiry(certificateManager.getSslKeyPair().getCert()); + } + + /** + * Returns true if the SSL certificate is generated by QZ Tray and expires inside the GRACE_PERIOD. + * GRACE_PERIOD is preferred for scheduling the renewals in advance, such as non-peak hours + */ + public static ExpiryState getExpiry(X509Certificate cert) { + // Invalid + if (cert == null) { + log.error("Can't check for expiration, certificate is missing."); + return ExpiryState.EXPIRED; + } + + Date expireDate = cert.getNotAfter(); + Calendar now = Calendar.getInstance(Locale.ENGLISH); + Calendar expires = Calendar.getInstance(Locale.ENGLISH); + expires.setTime(expireDate); + + // Expired + if (now.after(expires)) { + log.info("SSL certificate has expired {}. It must be renewed immediately.", SystemUtilities.toISO(expireDate)); + return ExpiryState.EXPIRED; + } + + // Expiring + expires.add(Calendar.DAY_OF_YEAR, -DEFAULT_GRACE_PERIOD_DAYS); + if (now.after(expires)) { + log.info("SSL certificate will expire in less than {} days: {}", DEFAULT_GRACE_PERIOD_DAYS, SystemUtilities.toISO(expireDate)); + return ExpiryState.EXPIRING; + } + + // Valid + int days = (int)Math.round((expireDate.getTime() - new Date().getTime()) / (double)86400000); + log.info("SSL certificate is still valid for {} more days: {}. We'll make a new one automatically when needed.", days, SystemUtilities.toISO(expireDate)); + return ExpiryState.VALID; + } + + public void schedule() { + schedule(DEFAULT_INITIAL_DELAY, DEFAULT_CHECK_FREQUENCY); + } + + public void schedule(int delayMillis, int freqMillis) { + if(timer != null) { + timer.cancel(); + timer.purge(); + } + timer = new Timer(); + timer.scheduleAtFixedRate(this, delayMillis, freqMillis); + } + + public String[] parseHostNames() { + return parseHostNames(certificateManager.getSslKeyPair().getCert()); + } + + public CertProvider findCertProvider() { + return findCertProvider(certificateManager.getSslKeyPair().getCert()); + } + + public static CertProvider findCertProvider(X509Certificate cert) { + // Internal certs use CN=localhost, trust email instead + if (CertificateManager.emailMatches(cert)) { + return CertProvider.INTERNAL; + } + + String providerDN; + + // check registered patterns to classify certificate + if(cert.getIssuerDN() != null && (providerDN = cert.getIssuerDN().getName()) != null) { + String cn = null; + try { + // parse issuer's DN + LdapName ldapName = new LdapName(providerDN); + for(Rdn rdn : ldapName.getRdns()) { + if(rdn.getType().equalsIgnoreCase("CN")) { + cn = (String)rdn.getValue(); + break; + } + } + + // compare cn to our pattern + if(cn != null) { + for(CertProvider provider : CertProvider.values()) { + for(String pattern : provider.patterns) { + if (cn.matches(pattern)) { + log.warn("Cert issuer detected as {}", provider.name()); + return provider; + } + } + } + } + } catch(InvalidNameException ignore) {} + } + + log.warn("A valid issuer couldn't be found, we won't know how to renew this cert when it expires"); + return CertProvider.UNKNOWN; + } + + public static String[] parseHostNames(X509Certificate cert) { + // Cache the SAN hosts for recreation + List hostNameList = new ArrayList<>(); + try { + Collection> altNames = cert.getSubjectAlternativeNames(); + if (altNames != null) { + for(List altName : altNames) { + if(altName.size()< 1) continue; + switch((Integer)altName.get(0)) { + case GeneralName.dNSName: + case GeneralName.iPAddress: + Object data = altName.get(1); + if (data instanceof String) { + hostNameList.add(((String)data)); + } + break; + default: + } + } + } else { + log.error("getSubjectAlternativeNames is null?"); + } + log.debug("Parsed hostNames: {}", String.join(", ", hostNameList)); + } catch(CertificateException e) { + log.warn("Can't parse hostNames from this cert. Cert renewals will contain default values instead"); + } + return hostNameList.toArray(new String[hostNameList.size()]); + } + + public boolean renewExternalCert(CertProvider externalProvider) { + switch(externalProvider) { + case LETS_ENCRYPT: + return renewLetsEncryptCert(externalProvider); + case CA_CERT_ORG: + default: + log.error("Cert renewal for {} is not implemented", externalProvider); + } + + return false; + } + + private boolean renewLetsEncryptCert(CertProvider externalProvider) { + try { + File storagePath = CertificateManager.getWritableLocation("ssl"); + + // cerbot is much simpler than acme, let's use it + Path root = Paths.get(SHARED_DIR.toString(), "letsencrypt", "config"); + log.info("Attempting to renew {}. Assuming certs are installed in {}...", externalProvider, root); + List cmds = new ArrayList(Arrays.asList("certbot", "--force-renewal", "certonly")); + + cmds.add("--standalone"); + + cmds.add("--config-dir"); + String config = Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "config").toString(); + cmds.add(config); + + cmds.add("--logs-dir"); + cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt", "logs").toString()); + + cmds.add("--work-dir"); + cmds.add(Paths.get(SHARED_DIR.toString(), "ssl", "letsencrypt").toString()); + + // append dns names + for(String hostName : hostNames) { + cmds.add("-d"); + cmds.add(hostName); + } + + if (ShellUtilities.execute(cmds.toArray(new String[cmds.size()]))) { + // Assume the cert is stored in a folder called "letsencrypt/config/live/" + Path keyPath = Paths.get(config, "live", hostNames[0], "privkey.pem"); + Path certPath = Paths.get(config, "live", hostNames[0], "fullchain.pem"); // fullchain required + certificateManager.createTrustedKeystore(keyPath.toFile(), certPath.toFile()); + log.info("Files imported, converted and saved. Reloading SslContextFactory..."); + certificateManager.reloadSslContextFactory(); + log.info("Reloaded SSL successfully."); + return true; + } else { + log.warn("Something went wrong renewing the LetsEncrypt certificate. Please run the certbot command manually to learn more."); + } + } catch(Exception e) { + log.error("Error renewing/reloading LetsEncrypt cert", e); + } + return false; + } + +} diff --git a/old code/tray/src/qz/installer/certificate/KeyPairWrapper.java b/old code/tray/src/qz/installer/certificate/KeyPairWrapper.java new file mode 100755 index 0000000..da7d947 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/KeyPairWrapper.java @@ -0,0 +1,130 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate; + +import qz.common.Constants; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Enumeration; + +/** + * Wrap handling of X509Certificate, PrivateKey and KeyStore conversion + */ +public class KeyPairWrapper { + public enum Type {CA, SSL} + + private Type type; + private PrivateKey key; + private char[] password; + private X509Certificate cert; + private KeyStore keyStore; // for SSL + + public KeyPairWrapper(Type type) { + this.type = type; + } + + public KeyPairWrapper(Type type, KeyPair keyPair, X509Certificate cert) { + this.type = type; + this.key = keyPair.getPrivate(); + this.cert = cert; + } + + /** + * Load from disk + */ + public void init(File keyFile, char[] password) throws IOException, GeneralSecurityException { + KeyStore keyStore = KeyStore.getInstance(keyFile.getName().endsWith(".jks") ? "JKS" : "PKCS12"); + keyStore.load(new FileInputStream(keyFile), password); + init(keyStore, password); + } + + /** + * Load from memory + */ + public void init(KeyStore keyStore, char[] password) throws GeneralSecurityException { + this.keyStore = keyStore; + KeyStore.ProtectionParameter param = new KeyStore.PasswordProtection(password); + KeyStore.PrivateKeyEntry entry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(getAlias(), param); + // the entry we assume is always wrong for pkcs12 imports, search for it instead + if(entry == null) { + Enumeration enumerator = keyStore.aliases(); + while(enumerator.hasMoreElements()) { + String alias = enumerator.nextElement(); + if(keyStore.isKeyEntry(alias)) { + this.password = password; + this.key = ((KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, param)).getPrivateKey(); + this.cert = (X509Certificate)keyStore.getCertificate(alias); + return; + } + } + throw new GeneralSecurityException("Could not initialize the KeyStore for internal use"); + } + + this.password = password; + this.key = entry.getPrivateKey(); + this.cert = (X509Certificate)keyStore.getCertificate(getAlias()); + } + + public X509Certificate getCert() { + return cert; + } + + public PrivateKey getKey() { + return key; + } + + public String getPasswordString() { + return new String(password); + } + + public char[] getPassword() { + return password; + } + + public static String getAlias(Type type) { + switch(type) { + case SSL: + return Constants.PROPS_FILE; // "qz-tray" + case CA: + default: + return "root-ca"; + } + } + + public String getAlias() { + return getAlias(getType()); + } + + public String propsPrefix() { + switch(type) { + case SSL: + return "wss"; + case CA: + default: + return "ca"; + } + } + + public Type getType() { + return type; + } + + public KeyStore getKeyStore() { + return keyStore; + } +} \ No newline at end of file diff --git a/old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java b/old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java new file mode 100755 index 0000000..029fea9 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/LinuxCertificateInstaller.java @@ -0,0 +1,365 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate; + +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.x509.Extension; +import org.bouncycastle.asn1.x509.SubjectKeyIdentifier; +import org.bouncycastle.util.encoders.Base64; +import qz.auth.X509Constants; +import qz.common.Constants; +import qz.installer.Installer; +import qz.utils.ByteUtilities; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; +import qz.utils.UnixUtilities; + +import javax.swing.*; +import java.awt.*; +import java.io.*; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; + +import static qz.installer.Installer.PrivilegeLevel.*; + +/** + * @author Tres Finocchiaro + */ +public class LinuxCertificateInstaller extends NativeCertificateInstaller { + private static final Logger log = LogManager.getLogger(LinuxCertificateInstaller.class); + private static final String CA_CERTIFICATES = "/usr/local/share/ca-certificates/"; + private static final String CA_CERTIFICATE_NAME = Constants.PROPS_FILE + "-root.crt"; // e.g. qz-tray-root.crt + private static final String PK11_KIT_ID = "pkcs11:id="; + + private static String[] NSSDB_URLS = { + // Conventional cert store + "sql:" + System.getenv("HOME") + "/.pki/nssdb/", + + // Snap-specific cert stores + "sql:" + System.getenv("HOME") + "/snap/chromium/current/.pki/nssdb/", + "sql:" + System.getenv("HOME") + "/snap/brave/current/.pki/nssdb/", + "sql:" + System.getenv("HOME") + "/snap/opera/current/.pki/nssdb/", + "sql:" + System.getenv("HOME") + "/snap/opera-beta/current/.pki/nssdb/" + }; + + private Installer.PrivilegeLevel certType; + + public LinuxCertificateInstaller(Installer.PrivilegeLevel certType) { + setInstallType(certType); + findCertutil(); + } + + public Installer.PrivilegeLevel getInstallType() { + return certType; + } + + public void setInstallType(Installer.PrivilegeLevel certType) { + this.certType = certType; + if (this.certType == SYSTEM) { + log.warn("Command \"certutil\" (required for certain browsers) needs to run as USER. We'll try again on launch."); + } + } + + public boolean remove(List idList) { + boolean success = true; + if(certType == SYSTEM) { + boolean first = distrustUsingUpdateCaCertificates(idList); + boolean second = distrustUsingTrustAnchor(idList); + success = first || second; + } else { + for(String nickname : idList) { + for(String nssdb : NSSDB_URLS) { + success = success && ShellUtilities.execute("certutil", "-d", nssdb, "-D", "-n", nickname); + } + } + } + return success; + } + + public List find() { + ArrayList nicknames = new ArrayList<>(); + if(certType == SYSTEM) { + nicknames = findUsingTrustAnchor(); + nicknames.addAll(findUsingUsingUpdateCaCert()); + } else { + try { + for(String nssdb : NSSDB_URLS) { + Process p = Runtime.getRuntime().exec(new String[] {"certutil", "-d", nssdb, "-L"}); + BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream())); + String line; + while((line = in.readLine()) != null) { + if (line.startsWith(Constants.ABOUT_COMPANY + " ")) { + nicknames.add(Constants.ABOUT_COMPANY); + break; // Stop reading input; nicknames can't appear more than once + } + } + in.close(); + } + } + catch(IOException e) { + log.warn("Could not get certificate nicknames", e); + } + } + return nicknames; + } + + public boolean verify(File ignore) { return true; } // no easy way to validate a cert, assume it's installed + + public boolean add(File certFile) { + boolean success = true; + + if(certType == SYSTEM) { + // Attempt two common methods for installing the SSL certificate + File systemCertFile; + boolean first = (systemCertFile = trustUsingUpdateCaCertificates(certFile)) != null; + boolean second = trustUsingTrustAnchor(systemCertFile, certFile); + success = first || second; + } else if(certType == USER) { + // Install certificate to local profile using "certutil" + for(String nssdb : NSSDB_URLS) { + String[] parts = nssdb.split(":", 2); + if (parts.length > 1) { + File folder = new File(parts[1]); + // If .pki/nssdb doesn't exist yet, don't create it! Per https://github.com/qzind/tray/issues/1003 + if(folder.exists() && folder.isDirectory()) { + if (!ShellUtilities.execute("certutil", "-d", nssdb, "-A", "-t", "TC", "-n", Constants.ABOUT_COMPANY, "-i", certFile.getPath())) { + log.warn("Something went wrong creating {}. HTTPS will fail on certain browsers which depend on it.", nssdb); + success = false; + } + } + } + } + } + + return success; + } + + private boolean findCertutil() { + boolean installed = ShellUtilities.execute("which", "certutil"); + if (!installed) { + if (certType == SYSTEM && promptCertutil()) { + if(UnixUtilities.isUbuntu() || UnixUtilities.isDebian()) { + installed = ShellUtilities.execute("apt-get", "install", "-y", "libnss3-tools"); + } else if(UnixUtilities.isFedora()) { + installed = ShellUtilities.execute("dnf", "install", "-y", "nss-tools"); + } + } + } + if(!installed) { + log.warn("A critical component, \"certutil\" wasn't found and cannot be installed automatically. HTTPS will fail on certain browsers which depend on it."); + } + return installed; + } + + private boolean promptCertutil() { + // Assume silent or headless installs want certutil + if(Installer.IS_SILENT || GraphicsEnvironment.isHeadless()) { + return true; + } + try { + SystemUtilities.setSystemLookAndFeel(true); + return JOptionPane.YES_OPTION == JOptionPane.showConfirmDialog(null, "A critical component, \"certutil\" wasn't found. Attempt to fetch it now?"); + } catch(Throwable ignore) {} + return true; + } + + /** + * Common technique for installing system-wide certificates on Debian-based systems (Ubuntu, etc.) + * + * This technique is only known to work for select browsers, such as Epiphany. Browsers such as + * Firefox and Chromium require different techniques. + * + * @return Full path to the destination file if successful, otherwise null + */ + private File trustUsingUpdateCaCertificates(File certFile) { + if(hasUpdateCaCertificatesCommand()) { + File destFile = new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME); + log.debug("Copying SYSTEM SSL certificate {} to {}", certFile.getPath(), destFile.getPath()); + try { + if (new File(CA_CERTIFICATES).isDirectory()) { + // Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011 + FileUtils.copyFile(certFile, destFile, false); + if (destFile.isFile()) { + // Attempt "update-ca-certificates" (Debian) + if (!ShellUtilities.execute("update-ca-certificates")) { + log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate."); + } else { + return destFile; + } + } + } else { + log.warn("{} is not a valid directory, skipping", CA_CERTIFICATES); + } + } + catch(IOException e) { + log.warn("Error copying SYSTEM SSL certificate file", e); + } + } else { + log.warn("Skipping SYSTEM SSL certificate install using \"update-ca-certificates\", command missing or invalid"); + } + return null; + } + + /** + * Common technique for installing system-wide certificates on Fedora-based systems + * + * Uses first existing non-null file provided + */ + private boolean trustUsingTrustAnchor(File ... certFiles) { + if (hasTrustAnchorCommand()) { + for(File certFile : certFiles) { + if (certFile == null || !certFile.exists()) { + continue; + } + // Install certificate to system using "trust anchor" (Fedora) + if (ShellUtilities.execute("trust", "anchor", "--store", certFile.getPath())) { + return true; + } else { + log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate."); + } + } + } else { + log.warn("Skipping SYSTEM SSL certificate install using \"trust anchor\", command missing or invalid"); + } + return false; + } + + private boolean distrustUsingUpdateCaCertificates(List paths) { + if(hasUpdateCaCertificatesCommand()) { + boolean deleted = false; + for(String path : paths) { + // Process files only; not "trust anchor" URIs + if(!path.startsWith(PK11_KIT_ID)) { + File certFile = new File(path); + if (certFile.isFile() && certFile.delete()) { + deleted = true; + } else { + log.warn("SYSTEM SSL certificate {} does not exist, skipping", certFile.getPath()); + } + } + } + // Attempt "update-ca-certificates" (Debian) + if(deleted) { + if (ShellUtilities.execute("update-ca-certificates")) { + return true; + } else { + log.warn("Something went wrong calling \"update-ca-certificates\" for the SYSTEM SSL certificate."); + } + } + } else { + log.warn("Skipping SYSTEM SSL certificate removal using \"update-ca-certificates\", command missing or invalid"); + } + return false; + } + + private boolean distrustUsingTrustAnchor(List idList) { + if(hasTrustAnchorCommand()) { + for(String id : idList) { + // only remove by id + if (id.startsWith(PK11_KIT_ID) && !ShellUtilities.execute("trust", "anchor", "--remove", id)) { + log.warn("Something went wrong calling \"trust anchor\" for the SYSTEM SSL certificate."); + } + } + } else { + log.warn("Skipping SYSTEM SSL certificate removal using \"trust anchor\", command missing or invalid"); + } + return false; + } + + /** + * Check for the presence of a QZ certificate in known locations (e.g. /usr/local/share/ca-certificates/ + * and return the path if found + */ + private ArrayList findUsingUsingUpdateCaCert() { + ArrayList found = new ArrayList<>(); + File[] systemCertFiles = { new File(CA_CERTIFICATES, CA_CERTIFICATE_NAME) }; + for(File file : systemCertFiles) { + if(file.isFile()) { + found.add(file.getPath()); + } + } + return found; + } + + /** + * Find QZ installed certificates in the "trust anchor" by searching by email. + * + * The "trust" utility identifies certificates as URIs: + * Example: + * pkcs11:id=%7C%5D%02%84%13%D4%CC%8A%9B%81%CE%17%1C%2E%29%1E%9C%48%63%42;type=cert + * ... which is an encoded version of the cert's SubjectKeyIdentifier field + * To identify a match: + * 1. Extract all trusted certificates and look for a familiar email address + * 2. If found, construct and store a "trust" compatible URI as the nickname + */ + private ArrayList findUsingTrustAnchor() { + ArrayList uris = new ArrayList<>(); + File tempFile = null; + try { + // Temporary location for system certificates + tempFile = File.createTempFile("trust-extract-for-qz-", ".pem"); + // Delete before use: "trust extract" requires an empty file + tempFile.delete(); + if(ShellUtilities.execute("trust", "extract", "--format", "pem-bundle", tempFile.getPath())) { + BufferedReader reader = new BufferedReader(new FileReader(tempFile)); + String line; + StringBuilder base64 = new StringBuilder(); + while ((line = reader.readLine()) != null) { + if(line.startsWith(X509Constants.BEGIN_CERT)) { + // Beginning of a new certificate + base64.setLength(0); + } else if(line.startsWith(X509Constants.END_CERT)) { + // End of the existing certificate + byte[] certBytes = Base64.decode(base64.toString()); + CertificateFactory factory = CertificateFactory.getInstance("X.509"); + X509Certificate cert = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(certBytes)); + if(CertificateManager.emailMatches(cert, true)) { + byte[] extensionValue = cert.getExtensionValue(Extension.subjectKeyIdentifier.getId()); + byte[] octets = DEROctetString.getInstance(extensionValue).getOctets(); + SubjectKeyIdentifier subjectKeyIdentifier = SubjectKeyIdentifier.getInstance(octets); + byte[] keyIdentifier = subjectKeyIdentifier.getKeyIdentifier(); + String hex = ByteUtilities.bytesToHex(keyIdentifier, true); + String uri = PK11_KIT_ID + hex.replaceAll("(.{2})", "%$1") + ";type=cert"; + log.info("Found matching cert: {}", uri); + + uris.add(uri); + } + } else { + base64.append(line); + } + } + + reader.close(); + } + } catch(IOException | CertificateException e) { + log.warn("An error occurred finding preexisting \"trust anchor\" certificates", e); + } finally { + if(tempFile != null && !tempFile.delete()) { + tempFile.deleteOnExit(); + } + } + return uris; + } + + private boolean hasUpdateCaCertificatesCommand() { + return ShellUtilities.execute("which", "update-ca-certificates"); + } + + private boolean hasTrustAnchorCommand() { + return ShellUtilities.execute("trust", "anchor", "--help"); + } +} diff --git a/old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java b/old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java new file mode 100755 index 0000000..b06584d --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/MacCertificateInstaller.java @@ -0,0 +1,91 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.installer.Installer; +import qz.utils.ShellUtilities; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.List; + +public class MacCertificateInstaller extends NativeCertificateInstaller { + private static final Logger log = LogManager.getLogger(MacCertificateInstaller.class); + + public static final String USER_STORE = System.getProperty("user.home") + "/Library/Keychains/login.keychain"; // aka login.keychain-db + public static final String SYSTEM_STORE = "/Library/Keychains/System.keychain"; + private String certStore; + + public MacCertificateInstaller(Installer.PrivilegeLevel certType) { + setInstallType(certType); + } + + public boolean add(File certFile) { + if (certStore.equals(USER_STORE)) { + // This will prompt the user + return ShellUtilities.execute("security", "add-trusted-cert", "-r", "trustRoot", "-k", certStore, certFile.getPath()); + } else { + return ShellUtilities.execute("security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", certStore, certFile.getPath()); + } + } + + public boolean remove(List idList) { + boolean success = true; + for (String certId : idList) { + success = success && ShellUtilities.execute("security", "delete-certificate", "-Z", certId, certStore); + } + return success; + } + + public List find() { + ArrayList hashList = new ArrayList<>(); + try { + Process p = Runtime.getRuntime().exec(new String[] {"security", "find-certificate", "-a", "-e", Constants.ABOUT_EMAIL, "-Z", certStore}); + BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream())); + String line; + while ((line = in.readLine()) != null) { + if (line.contains("SHA-1") && line.contains(":")) { + hashList.add(line.split(":", 2)[1].trim()); + } + } + in.close(); + } catch(IOException e) { + log.warn("Could not get certificate list", e); + } + return hashList; + } + + public boolean verify(File certFile) { + return ShellUtilities.execute( "security", "verify-cert", "-c", certFile.getPath()); + } + + public void setInstallType(Installer.PrivilegeLevel type) { + if (type == Installer.PrivilegeLevel.USER) { + certStore = USER_STORE; + } else { + certStore = SYSTEM_STORE; + } + } + + public Installer.PrivilegeLevel getInstallType() { + if (certStore == USER_STORE) { + return Installer.PrivilegeLevel.USER; + } else { + return Installer.PrivilegeLevel.SYSTEM; + } + } +} diff --git a/old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java b/old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java new file mode 100755 index 0000000..cbe3178 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/NativeCertificateInstaller.java @@ -0,0 +1,105 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate; + +import org.bouncycastle.openssl.jcajce.JcaMiscPEMGenerator; +import org.bouncycastle.openssl.jcajce.JcaPEMWriter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.installer.Installer; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.security.cert.X509Certificate; +import java.util.List; + +public abstract class NativeCertificateInstaller { + private static final Logger log = LogManager.getLogger(NativeCertificateInstaller.class); + protected static NativeCertificateInstaller instance; + + public static NativeCertificateInstaller getInstance() { + return getInstance(SystemUtilities.isAdmin() ? Installer.PrivilegeLevel.SYSTEM : Installer.PrivilegeLevel.USER); + } + public static NativeCertificateInstaller getInstance(Installer.PrivilegeLevel type) { + if (instance == null) { + switch(SystemUtilities.getOs()) { + case WINDOWS: + instance = new WindowsCertificateInstaller(type); + break; + case MAC: + instance = new MacCertificateInstaller(type); + break; + case LINUX: + default: + instance = new LinuxCertificateInstaller(type); + } + } + return instance; + } + + /** + * Install a certificate from memory + */ + public boolean install(X509Certificate cert) { + File certFile = null; + try { + certFile = File.createTempFile(KeyPairWrapper.getAlias(KeyPairWrapper.Type.CA) + "-", CertificateManager.DEFAULT_CERTIFICATE_EXTENSION); + JcaMiscPEMGenerator generator = new JcaMiscPEMGenerator(cert); + JcaPEMWriter writer = new JcaPEMWriter(new OutputStreamWriter(Files.newOutputStream(certFile.toPath(), StandardOpenOption.CREATE))); + writer.writeObject(generator.generate()); + writer.close(); + + return install(certFile); + } catch(IOException e) { + log.warn("Could not install cert from temp file", e); + } finally { + if(certFile != null && !certFile.delete()) { + certFile.deleteOnExit(); + } + } + return false; + } + + /** + * Install a certificate from disk + */ + public boolean install(File certFile) { + String helper = instance.getClass().getSimpleName(); + String store = instance.getInstallType().name(); + if(SystemUtilities.isJar()) { + if (remove(find())) { + log.info("Certificate removed from {} store using {}", store, helper); + } else { + log.warn("Could not remove certificate from {} store using {}", store, helper); + } + } else { + log.info("Skipping {} store certificate removal, IDE detected.", store, helper); + } + if (add(certFile)) { + log.info("Certificate added to {} store using {}", store, helper); + return true; + } else { + log.warn("Could not install certificate to {} store using {}", store, helper); + } + return false; + } + + public abstract boolean add(File certFile); + public abstract boolean remove(List idList); + public abstract List find(); + public abstract boolean verify(File certFile); + public abstract void setInstallType(Installer.PrivilegeLevel certType); + public abstract Installer.PrivilegeLevel getInstallType(); +} diff --git a/old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java b/old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java new file mode 100755 index 0000000..ba879cb --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/WindowsCertificateInstaller.java @@ -0,0 +1,236 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate; + +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.Structure; +import com.sun.jna.platform.win32.Kernel32Util; +import com.sun.jna.platform.win32.WinNT; +import com.sun.jna.win32.StdCallLibrary; +import com.sun.jna.win32.W32APIOptions; +import org.bouncycastle.cert.X509CertificateHolder; +import org.bouncycastle.openssl.PEMParser; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.installer.Installer; + +import java.io.*; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; + +public class WindowsCertificateInstaller extends NativeCertificateInstaller { + private static final Logger log = LogManager.getLogger(WindowsCertificateInstaller.class); + private WinCrypt.HCERTSTORE store; + private byte[] certBytes; + private Installer.PrivilegeLevel certType; + + public WindowsCertificateInstaller(Installer.PrivilegeLevel certType) { + setInstallType(certType); + } + + public boolean add(File certFile) { + log.info("Writing certificate {} to {} store using Crypt32...", certFile, certType); + try { + + byte[] bytes = getCertBytes(certFile); + Pointer pointer = new Memory(bytes.length); + pointer.write(0, bytes, 0, bytes.length); + + boolean success = Crypt32.INSTANCE.CertAddEncodedCertificateToStore( + openStore(), + WinCrypt.X509_ASN_ENCODING, + pointer, + bytes.length, + Crypt32.CERT_STORE_ADD_REPLACE_EXISTING, + null + ); + if(!success) { + log.warn(Kernel32Util.formatMessage(Native.getLastError())); + } + + closeStore(); + + return success; + } catch(IOException e) { + log.warn("An error occurred installing the certificate", e); + } finally { + certBytes = null; + } + return false; + } + + private byte[] getCertBytes(File certFile) throws IOException { + if(certBytes == null) { + PEMParser pem = new PEMParser(new FileReader(certFile)); + X509CertificateHolder certHolder = (X509CertificateHolder)pem.readObject(); + certBytes = certHolder.getEncoded(); + } + return certBytes; + } + + private WinCrypt.HCERTSTORE openStore() { + if(store == null) { + store = openStore(certType); + } + return store; + } + + private void closeStore() { + if(store != null && closeStore(store)) { + store = null; + } else { + log.warn("Unable to close {} cert store", certType); + } + } + + private static WinCrypt.HCERTSTORE openStore(Installer.PrivilegeLevel certType) { + log.info("Opening {} store using Crypt32...", certType); + + WinCrypt.HCERTSTORE store = Crypt32.INSTANCE.CertOpenStore( + Crypt32.CERT_STORE_PROV_SYSTEM, + 0, + null, + certType == Installer.PrivilegeLevel.USER ? Crypt32.CERT_SYSTEM_STORE_CURRENT_USER : Crypt32.CERT_SYSTEM_STORE_LOCAL_MACHINE, + "ROOT" + ); + if(store == null) { + log.warn(Kernel32Util.formatMessage(Native.getLastError())); + } + return store; + } + + private static boolean closeStore(WinCrypt.HCERTSTORE certStore) { + boolean isClosed = Crypt32.INSTANCE.CertCloseStore( + certStore, 0 + ); + if(!isClosed) { + log.warn(Kernel32Util.formatMessage(Native.getLastError())); + } + return isClosed; + } + + public boolean remove(List ignore) { + boolean success = true; + + WinCrypt.CERT_CONTEXT hCertContext; + WinCrypt.CERT_CONTEXT pPrevCertContext = null; + while(true) { + hCertContext = Crypt32.INSTANCE.CertFindCertificateInStore( + openStore(), + WinCrypt.X509_ASN_ENCODING, + 0, + Crypt32.CERT_FIND_SUBJECT_STR, + Constants.ABOUT_EMAIL, + pPrevCertContext); + + if(hCertContext == null) { + break; + } + + pPrevCertContext = Crypt32.INSTANCE.CertDuplicateCertificateContext(hCertContext); + + if(success = (success && Crypt32.INSTANCE.CertDeleteCertificateFromStore(hCertContext))) { + log.info("Successfully deleted certificate matching {}", Constants.ABOUT_EMAIL); + } else { + log.info("Could not delete certificate: {}", Kernel32Util.formatMessage(Native.getLastError())); + } + } + + closeStore(); + return success; + } + + public List find() { + return null; + } + + public void setInstallType(Installer.PrivilegeLevel type) { + this.certType = type; + } + + public Installer.PrivilegeLevel getInstallType() { + return certType; + } + + public boolean verify(File certFile) { + try { + MessageDigest md = MessageDigest.getInstance("SHA-1"); + md.update(getCertBytes(certFile)); + WinCrypt.DATA_BLOB thumbPrint = new WinCrypt.DATA_BLOB(md.digest()); + WinNT.HANDLE cert = Crypt32.INSTANCE.CertFindCertificateInStore( + openStore(), + WinCrypt.X509_ASN_ENCODING, + 0, + Crypt32.CERT_FIND_SHA1_HASH, + thumbPrint, + null); + + return cert != null; + } catch(IOException | NoSuchAlgorithmException e) { + log.warn("An error occurred verifying the cert is installed: {}", certFile, e); + } + return false; + } + + /** + * The JNA's Crypt32 instance oversimplifies store handling, preventing user stores from being used + */ + interface Crypt32 extends StdCallLibrary { + int CERT_SYSTEM_STORE_CURRENT_USER = 65536; + int CERT_SYSTEM_STORE_LOCAL_MACHINE = 131072; + int CERT_STORE_PROV_SYSTEM = 10; + int CERT_STORE_ADD_REPLACE_EXISTING = 3; + int CERT_FIND_SUBJECT_STR = 524295; + int CERT_FIND_SHA1_HASH = 65536; + + Crypt32 INSTANCE = Native.load("Crypt32", Crypt32.class, W32APIOptions.DEFAULT_OPTIONS); + + WinCrypt.HCERTSTORE CertOpenStore(int lpszStoreProvider, int dwMsgAndCertEncodingType, Pointer hCryptProv, int dwFlags, String pvPara); + boolean CertCloseStore(WinCrypt.HCERTSTORE hCertStore, int dwFlags); + boolean CertAddEncodedCertificateToStore(WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, Pointer pbCertEncoded, int cbCertEncoded, int dwAddDisposition, Pointer ppCertContext); + WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, String pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext); + WinCrypt.CERT_CONTEXT CertFindCertificateInStore (WinCrypt.HCERTSTORE hCertStore, int dwCertEncodingType, int dwFindFlags, int dwFindType, Structure pvFindPara, WinCrypt.CERT_CONTEXT pPrevCertContext); + boolean CertDeleteCertificateFromStore(WinCrypt.CERT_CONTEXT pCertContext); + boolean CertFreeCertificateContext(WinCrypt.CERT_CONTEXT pCertContext); + WinCrypt.CERT_CONTEXT CertDuplicateCertificateContext(WinCrypt.CERT_CONTEXT pCertContext); + } + + // Polyfill from JNA5+ + @SuppressWarnings("UnusedDeclaration") //Library class + public static class WinCrypt { + public static int X509_ASN_ENCODING = 0x00000001; + public static class HCERTSTORE extends WinNT.HANDLE { + public HCERTSTORE() {} + public HCERTSTORE(Pointer p) { + super(p); + } + } + public static class CERT_CONTEXT extends WinNT.HANDLE { + public CERT_CONTEXT() {} + public CERT_CONTEXT(Pointer p) { + super(p); + } + } + public static class DATA_BLOB extends com.sun.jna.platform.win32.WinCrypt.DATA_BLOB { + // Wrap the constructor for code readability + public DATA_BLOB() { + super(); + } + public DATA_BLOB(byte[] data) { + super(data); + } + } + } +} diff --git a/old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java b/old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java new file mode 100755 index 0000000..016cd9a --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/WindowsCertificateInstallerCli.java @@ -0,0 +1,136 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.installer.Installer; +import qz.utils.ShellUtilities; +import qz.utils.WindowsUtilities; + +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Command Line technique for installing certificates on Windows + * Fallback class for when JNA is not available (e.g. Windows on ARM) + */ +@SuppressWarnings("UnusedDeclaration") //Library class +public class WindowsCertificateInstallerCli extends NativeCertificateInstaller { + private static final Logger log = LogManager.getLogger(WindowsCertificateInstallerCli.class); + private Installer.PrivilegeLevel certType; + + public WindowsCertificateInstallerCli(Installer.PrivilegeLevel certType) { + setInstallType(certType); + } + + public boolean add(File certFile) { + if (WindowsUtilities.isWindowsXP()) return false; + if (certType == Installer.PrivilegeLevel.USER) { + // This will prompt the user + return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "-user", "Root", certFile.getPath()); + } else { + return ShellUtilities.execute("certutil.exe", "-addstore", "-f", "Root", certFile.getPath()); + } + } + + public boolean remove(List idList) { + if (WindowsUtilities.isWindowsXP()) return false; + boolean success = true; + for (String certId : idList) { + if (certType == Installer.PrivilegeLevel.USER) { + success = success && ShellUtilities.execute("certutil.exe", "-delstore", "-user", "Root", certId); + } else { + success = success && ShellUtilities.execute("certutil.exe", "-delstore", "Root", certId); + } + } + return success; + } + + /** + * Returns a list of serials, if found + */ + public List find() { + ArrayList serialList = new ArrayList<>(); + try { + Process p; + if (certType == Installer.PrivilegeLevel.USER) { + p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "-user", "Root"}); + } else { + p = Runtime.getRuntime().exec(new String[] {"certutil.exe", "-store", "Root"}); + } + BufferedReader in = new BufferedReader(new InputStreamReader(p.getInputStream())); + String line; + while ((line = in.readLine()) != null) { + if (line.contains("================")) { + // First line is serial + String serial = parseNextLine(in); + if (serial != null) { + // Second line is issuer + String issuer = parseNextLine(in); + if (issuer.contains("OU=" + Constants.ABOUT_COMPANY)) { + serialList.add(serial); + } + } + } + } + in.close(); + } catch(Exception e) { + log.info("Unable to find a Trusted Root Certificate matching \"OU={}\"", Constants.ABOUT_COMPANY); + } + return serialList; + } + + public boolean verify(File certFile) { + return verifyCert(certFile); + } + + public static boolean verifyCert(File certFile) { + // -user also will check the root store + String dwErrorStatus = ShellUtilities.execute( new String[] {"certutil", "-user", "-verify", certFile.getPath() }, new String[] { "dwErrorStatus=" }, false, false); + if(!dwErrorStatus.isEmpty()) { + String[] parts = dwErrorStatus.split("[\r\n\\s]+"); + for(String part : parts) { + if(part.startsWith("dwErrorStatus=")) { + log.info("Certificate validity says {}", part); + String[] status = part.split("=", 2); + if (status.length == 2) { + return status[1].trim().equals("0"); + } + } + } + } + log.warn("Unable to determine certificate validity, you'll be prompted on startup"); + return false; + } + + public void setInstallType(Installer.PrivilegeLevel type) { + this.certType = type; + } + + public Installer.PrivilegeLevel getInstallType() { + return certType; + } + + private static String parseNextLine(BufferedReader reader) throws IOException { + String data = reader.readLine(); + if (data != null) { + String[] split = data.split(":", 2); + if (split.length == 2) { + return split[1].trim(); + } + } + return null; + } + +} diff --git a/old code/tray/src/qz/installer/certificate/firefox/ConflictingPolicyException.java b/old code/tray/src/qz/installer/certificate/firefox/ConflictingPolicyException.java new file mode 100755 index 0000000..24de2f6 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/firefox/ConflictingPolicyException.java @@ -0,0 +1,7 @@ +package qz.installer.certificate.firefox; + +class ConflictingPolicyException extends Exception { + ConflictingPolicyException(String message) { + super(message); + } +} diff --git a/old code/tray/src/qz/installer/certificate/firefox/FirefoxCertificateInstaller.java b/old code/tray/src/qz/installer/certificate/firefox/FirefoxCertificateInstaller.java new file mode 100755 index 0000000..1f06ed5 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/firefox/FirefoxCertificateInstaller.java @@ -0,0 +1,282 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate.firefox; + +import com.github.zafarkhaja.semver.Version; +import com.sun.jna.platform.win32.WinReg; +import org.codehaus.jettison.json.JSONException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.installer.Installer; +import qz.installer.certificate.CertificateManager; +import qz.installer.certificate.firefox.locator.AppAlias; +import qz.installer.certificate.firefox.locator.AppInfo; +import qz.installer.certificate.firefox.locator.AppLocator; +import qz.utils.JsonWriter; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; +import qz.utils.WindowsUtilities; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.cert.CertificateEncodingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Base64; + +/** + * Installs the Firefox Policy file via Enterprise Policy, Distribution Policy file or AutoConfig, depending on OS & version + */ +public class FirefoxCertificateInstaller { + protected static final Logger log = LogManager.getLogger(FirefoxCertificateInstaller.class); + + /** + * Versions are for Mozilla's official Firefox release. + * 3rd-party/clones may adopt Enterprise Policy support under + * different version numbers, adapt as needed. + */ + private static final Version WINDOWS_POLICY_VERSION = Version.valueOf("62.0.0"); + private static final Version MAC_POLICY_VERSION = Version.valueOf("63.0.0"); + private static final Version LINUX_POLICY_VERSION = Version.valueOf("65.0.0"); + public static final Version FIREFOX_RESTART_VERSION = Version.valueOf("60.0.0"); + + public static final String LINUX_GLOBAL_POLICY_LOCATION = "/etc/firefox/policies/policies.json"; + public static final String LINUX_SNAP_CERT_LOCATION = "/etc/firefox/policies/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION; // See https://github.com/mozilla/policy-templates/issues/936 + public static final String LINUX_GLOBAL_CERT_LOCATION = "/usr/lib/mozilla/certificates/" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION; + private static String DISTRIBUTION_ENTERPRISE_ROOT_POLICY = "{ \"policies\": { \"Certificates\": { \"ImportEnterpriseRoots\": true } } }"; + private static String DISTRIBUTION_INSTALL_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"" + Constants.PROPS_FILE + CertificateManager.DEFAULT_CERTIFICATE_EXTENSION + "\", \"" + LINUX_SNAP_CERT_LOCATION + "\" ] } } }"; + private static String DISTRIBUTION_REMOVE_CERT_POLICY = "{ \"policies\": { \"Certificates\": { \"Install\": [ \"/opt/" + Constants.PROPS_FILE + "/auth/root-ca.crt\"] } } }"; + + public static final String DISTRIBUTION_POLICY_LOCATION = "distribution/policies.json"; + public static final String DISTRIBUTION_MAC_POLICY_LOCATION = "Contents/Resources/" + DISTRIBUTION_POLICY_LOCATION; + + public static final String POLICY_AUDIT_MESSAGE = "Enterprise policy installed by " + Constants.ABOUT_TITLE + " on " + SystemUtilities.timeStamp(); + + public static void install(X509Certificate cert, String ... hostNames) { + // Blindly install Firefox enterprise policies to the system (macOS, Windows) + ArrayList enterpriseFailed = new ArrayList<>(); + for(AppAlias.Alias alias : AppAlias.FIREFOX.getAliases()) { + boolean success = false; + try { + if(alias.isEnterpriseReady() && !hasEnterprisePolicy(alias, false)) { + log.info("Installing Firefox enterprise certificate policy for {}", alias); + success = installEnterprisePolicy(alias, false); + } + } catch(ConflictingPolicyException e) { + log.warn("Conflict found installing {} enterprise cert support. We'll fallback on the distribution policy instead", alias.getName(), e); + } + if(!success) { + enterpriseFailed.add(alias); + } + } + + // Search for installed instances + ArrayList foundApps = AppLocator.getInstance().locate(AppAlias.FIREFOX); + ArrayList processPaths = null; + + for(AppInfo appInfo : foundApps) { + boolean success = false; + if (honorsPolicy(appInfo)) { + if((SystemUtilities.isWindows()|| SystemUtilities.isMac()) && !enterpriseFailed.contains(appInfo.getAlias())) { + // Enterprise policy was already installed + success = true; + } else { + log.info("Installing Firefox distribution policy for {}", appInfo); + success = installDistributionPolicy(appInfo, cert); + } + } else { + log.info("Installing Firefox auto-config script for {}", appInfo); + try { + String certData = Base64.getEncoder().encodeToString(cert.getEncoded()); + success = LegacyFirefoxCertificateInstaller.installAutoConfigScript(appInfo, certData, hostNames); + } + catch(CertificateEncodingException e) { + log.warn("Unable to install auto-config script for {}", appInfo, e); + } + } + if(success) { + issueRestartWarning(processPaths = AppLocator.getRunningPaths(foundApps, processPaths), appInfo); + } + } + } + + public static void uninstall() { + ArrayList appList = AppLocator.getInstance().locate(AppAlias.FIREFOX); + for(AppInfo appInfo : appList) { + if(honorsPolicy(appInfo)) { + if(SystemUtilities.isWindows() || SystemUtilities.isMac()) { + log.info("Skipping uninstall of Firefox enterprise root certificate policy for {}", appInfo); + } else { + try { + File policy = appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toFile(); + if(policy.exists()) { + JsonWriter.write(appInfo.getPath().resolve(DISTRIBUTION_POLICY_LOCATION).toString(), DISTRIBUTION_INSTALL_CERT_POLICY, false, true); + } + } catch(IOException | JSONException e) { + log.warn("Unable to remove Firefox policy for {}", appInfo, e); + } + } + } else { + log.info("Uninstalling Firefox auto-config script for {}", appInfo); + LegacyFirefoxCertificateInstaller.uninstallAutoConfigScript(appInfo); + } + } + } + + public static boolean honorsPolicy(AppInfo appInfo) { + if (appInfo.getVersion() == null) { + log.warn("Firefox-compatible browser found {}, but no version information is available", appInfo); + return false; + } + if(SystemUtilities.isWindows()) { + return appInfo.getVersion().greaterThanOrEqualTo(WINDOWS_POLICY_VERSION); + } else if (SystemUtilities.isMac()) { + return appInfo.getVersion().greaterThanOrEqualTo(MAC_POLICY_VERSION); + } else { + return appInfo.getVersion().greaterThanOrEqualTo(LINUX_POLICY_VERSION); + } + } + + /** + * Returns true if an alternative Firefox policy (e.g. registry, plist user or system) is installed + */ + private static boolean hasEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) throws ConflictingPolicyException { + if(SystemUtilities.isWindows()) { + String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true)); + Integer foundPolicy = WindowsUtilities.getRegInt(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots"); + if(foundPolicy != null) { + return foundPolicy == 1; + } + } else if(SystemUtilities.isMac()) { + String policyLocation = "/Library/Preferences/"; + if(userOnly) { + policyLocation = System.getProperty("user.home") + policyLocation; + } + String policesEnabled = ShellUtilities.executeRaw(new String[] { "defaults", "read", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled"}, true); + String foundPolicy = ShellUtilities.executeRaw(new String[] {"defaults", "read", policyLocation + alias.getBundleId(), "Certificates"}, true); + if(!policesEnabled.isEmpty() && !foundPolicy.isEmpty()) { + // Policies exist, decide how to proceed + if(policesEnabled.trim().equals("1") && foundPolicy.contains("ImportEnterpriseRoots = 1;")) { + return true; + } + throw new ConflictingPolicyException(String.format("%s enterprise policy conflict at %s: %s", alias.getName(), policyLocation + alias.getBundleId(), foundPolicy)); + } + } else { + // Linux alternate policy not yet supported + } + return false; + } + + /** + * Install policy to distribution/policies.json + */ + public static boolean installDistributionPolicy(AppInfo app, X509Certificate cert) { + Path jsonPath = app.getPath().resolve(SystemUtilities.isMac() ? DISTRIBUTION_MAC_POLICY_LOCATION:DISTRIBUTION_POLICY_LOCATION); + String jsonPolicy = SystemUtilities.isWindows() || SystemUtilities.isMac() ? DISTRIBUTION_ENTERPRISE_ROOT_POLICY:DISTRIBUTION_INSTALL_CERT_POLICY; + + // Special handling for snaps + if(app.getPath().toString().startsWith("/snap")) { + log.info("Snap detected, installing policy file to global location instead: {}", LINUX_GLOBAL_POLICY_LOCATION); + jsonPath = Paths.get(LINUX_GLOBAL_POLICY_LOCATION); + } + + try { + if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) { + // Linux lacks the concept of "enterprise roots", we'll write it to a known location instead + writeCertFile(cert, LINUX_SNAP_CERT_LOCATION); // so that the snap can read from it + writeCertFile(cert, LINUX_GLOBAL_CERT_LOCATION); // default location for non-snaps + } + + File jsonFile = jsonPath.toFile(); + + // Make sure we can traverse and read + File distribution = jsonFile.getParentFile(); + distribution.mkdirs(); + distribution.setReadable(true, false); + distribution.setExecutable(true, false); + + if(jsonPolicy.equals(DISTRIBUTION_INSTALL_CERT_POLICY)) { + // Delete previous policy + JsonWriter.write(jsonPath.toString(), DISTRIBUTION_REMOVE_CERT_POLICY, false, true); + } + + JsonWriter.write(jsonPath.toString(), jsonPolicy, false, false); + + // Make sure ew can read + jsonFile.setReadable(true, false); + return true; + } catch(JSONException | IOException e) { + log.warn("Could not install distribution policy {} to {}", jsonPolicy, jsonPath.toString(), e); + } + return false; + } + + public static boolean installEnterprisePolicy(AppAlias.Alias alias, boolean userOnly) { + if(SystemUtilities.isWindows()) { + String key = String.format("Software\\Policies\\%s\\%s\\Certificates", alias.getVendor(), alias.getName(true));; + WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "Comment", POLICY_AUDIT_MESSAGE); + return WindowsUtilities.addRegValue(userOnly ? WinReg.HKEY_CURRENT_USER : WinReg.HKEY_LOCAL_MACHINE, key, "ImportEnterpriseRoots", 1); + } else if(SystemUtilities.isMac()) { + String policyLocation = "/Library/Preferences/"; + if(userOnly) { + policyLocation = System.getProperty("user.home") + policyLocation; + } + return ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "EnterprisePoliciesEnabled", "-bool", "TRUE"}, true) && + ShellUtilities.execute(new String[] {"defaults", "write", policyLocation + alias.getBundleId(), "Certificates", "-dict", "ImportEnterpriseRoots", "-bool", "TRUE", + "Comment", "-string", POLICY_AUDIT_MESSAGE}, true); + } + return false; + } + + public static boolean issueRestartWarning(ArrayList runningPaths, AppInfo appInfo) { + boolean firefoxIsRunning = runningPaths.contains(appInfo.getExePath()); + + // Edge case for detecting if snap is running, since we can't compare the exact path easily + for(Path runningPath : runningPaths) { + if(runningPath.startsWith("/snap/")) { + firefoxIsRunning = true; + } + } + + if (firefoxIsRunning) { + if (appInfo.getVersion().greaterThanOrEqualTo(FirefoxCertificateInstaller.FIREFOX_RESTART_VERSION)) { + try { + Installer.getInstance().spawn(appInfo.getExePath().toString(), "-private", "about:restartrequired"); + return true; + } + catch(Exception ignore) {} + } else { + log.warn("{} must be restarted manually for changes to take effect", appInfo); + } + } + return false; + } + + private static void writeCertFile(X509Certificate cert, String location) throws IOException { + File certFile = new File(location); + + // Make sure we can traverse and read + File certs = new File(location).getParentFile(); + certs.mkdirs(); + certs.setReadable(true, false); + certs.setExecutable(true, false); + File mozilla = certs.getParentFile(); + mozilla.setReadable(true, false); + mozilla.setExecutable(true, false); + + // Make sure we can read + CertificateManager.writeCert(cert, certFile); + certFile.setReadable(true, false); + } +} diff --git a/old code/tray/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java b/old code/tray/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java new file mode 100755 index 0000000..33a698a --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/firefox/LegacyFirefoxCertificateInstaller.java @@ -0,0 +1,150 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate.firefox; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.installer.certificate.CertificateChainBuilder; +import qz.installer.certificate.firefox.locator.AppInfo; +import qz.utils.FileUtilities; +import qz.utils.SystemUtilities; + +import java.io.*; +import java.nio.file.Path; +import java.security.cert.CertificateEncodingException; +import java.util.*; + +/** + * Legacy Firefox Certificate installer + * + * For old Firefox-compatible browsers still in the wild such as Firefox 52 ESR, SeaMonkey, WaterFox, etc. + */ +public class LegacyFirefoxCertificateInstaller { + private static final Logger log = LogManager.getLogger(CertificateChainBuilder.class); + + private static final String CFG_TEMPLATE = "assets/firefox-autoconfig.js.in"; + private static final String CFG_FILE = Constants.PROPS_FILE + ".cfg"; + private static final String PREFS_FILE = Constants.PROPS_FILE + ".js"; + private static final String PREFS_DIR = "defaults/pref"; + private static final String MAC_PREFIX = "Contents/Resources"; + + public static boolean installAutoConfigScript(AppInfo appInfo, String certData, String ... hostNames) { + try { + if(appInfo.getPath().toString().equals("/usr/bin")) { + throw new Exception("Preventing install to root location"); + } + writePrefsFile(appInfo); + writeParsedConfig(appInfo, certData, false, hostNames); + return true; + } catch(Exception e) { + log.warn("Error installing auto-config support for {}", appInfo, e); + } + return false; + } + + public static boolean uninstallAutoConfigScript(AppInfo appInfo) { + try { + writeParsedConfig(appInfo, "", true); + return true; + } catch(Exception e) { + log.warn("Error uninstalling auto-config support for {}", appInfo, e); + } + return false; + } + + public static File tryWrite(AppInfo appInfo, boolean mkdirs, String ... paths) throws IOException { + Path dir = appInfo.getPath(); + if (SystemUtilities.isMac()) { + dir = dir.resolve(MAC_PREFIX); + } + for (String path : paths) { + dir = dir.resolve(path); + } + File file = dir.toFile(); + + if(mkdirs) file.mkdirs(); + if(file.exists() && file.isDirectory() && file.canWrite()) { + return file; + } + + throw new IOException(String.format("Directory does not exist or is not writable: %s", file)); + } + + public static void deleteFile(File parent, String ... paths) { + if(parent != null) { + String toDelete = parent.getPath(); + for (String path : paths) { + toDelete += File.separator + path; + } + File deleteFile = new File(toDelete); + if (!deleteFile.exists()) { + } else if (new File(toDelete).delete()) { + log.info("Deleted old file: {}", toDelete); + } else { + log.warn("Could not delete old file: {}", toDelete); + } + } + } + + public static void writePrefsFile(AppInfo app) throws Exception { + File prefsDir = tryWrite(app, true, PREFS_DIR); + deleteFile(prefsDir, "firefox-prefs.js"); // cleanup old version + + // first check that there aren't other prefs files + String pref = "general.config.filename"; + for (File file : prefsDir.listFiles()) { + try { + BufferedReader reader = new BufferedReader(new FileReader(file)); + String line; + while((line = reader.readLine()) != null) { + if(line.contains(pref) && !line.contains(CFG_FILE)) { + throw new Exception(String.format("Browser already has %s defined in %s:\n %s", pref, file, line)); + } + } + } catch(IOException ignore) {} + } + + // write out the new prefs file + File prefsFile = new File(prefsDir, PREFS_FILE); + BufferedWriter writer = new BufferedWriter(new FileWriter(prefsFile)); + String[] data = { + String.format("pref('%s', '%s');", pref, CFG_FILE), + "pref('general.config.obscure_value', 0);" + }; + for (String line : data) { + writer.write(line + "\n"); + } + writer.close(); + prefsFile.setReadable(true, false); + } + + private static void writeParsedConfig(AppInfo appInfo, String certData, boolean uninstall, String ... hostNames) throws IOException, CertificateEncodingException{ + if (hostNames.length == 0) hostNames = CertificateChainBuilder.DEFAULT_HOSTNAMES; + + File cfgDir = tryWrite(appInfo, false); + deleteFile(cfgDir, "firefox-config.cfg"); // cleanup old version + File dest = new File(cfgDir.getPath(), CFG_FILE); + + HashMap fieldMap = new HashMap<>(); + // Dynamic fields + fieldMap.put("%CERT_DATA%", certData); + fieldMap.put("%COMMON_NAME%", hostNames[0]); + fieldMap.put("%TIMESTAMP%", uninstall ? "-1" : "" + new Date().getTime()); + fieldMap.put("%APP_PATH%", SystemUtilities.isMac() ? SystemUtilities.getAppPath() != null ? SystemUtilities.getAppPath().toString() : "" : ""); + fieldMap.put("%UNINSTALL%", "" + uninstall); + + FileUtilities.configureAssetFile(CFG_TEMPLATE, dest, fieldMap, LegacyFirefoxCertificateInstaller.class); + dest.setReadable(true, false); + } + + +} diff --git a/old code/tray/src/qz/installer/certificate/firefox/assets/firefox-autoconfig.js.in b/old code/tray/src/qz/installer/certificate/firefox/assets/firefox-autoconfig.js.in new file mode 100755 index 0000000..5366a96 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/firefox/assets/firefox-autoconfig.js.in @@ -0,0 +1,117 @@ +// +// Firefox AutoConfig Certificate Installer for Legacy Firefox versions +// This is part of the QZ Tray application +// +var serviceObserver = { + observe: function observe(aSubject, aTopic, aData) { + // Get NSS certdb object + var certdb = getCertDB(); + + if (needsUninstall()) { + deleteCertificate(); + unregisterProtocol(); + } else if (needsCert()) { + deleteCertificate(); + installCertificate(); + registerProtocol(); + } + + // Compares the timestamp embedded in this script against that stored in the browser's about:config + function needsCert() { + try { + return getPref("%PROPS_FILE%.installer.timestamp") != "%TIMESTAMP%"; + } catch(notfound) {} + return true; + } + + // Installs the embedded base64 certificate into the browser + function installCertificate() { + certdb.addCertFromBase64(getCertData(), "C,C,C", "%COMMON_NAME% - %ABOUT_COMPANY%"); + pref("%PROPS_FILE%.installer.timestamp", "%TIMESTAMP%"); + } + + // Deletes the certificate, if it exists + function deleteCertificate() { + var certs = certdb.getCerts(); + var enumerator = certs.getEnumerator(); + while (enumerator.hasMoreElements()) { + var cert = enumerator.getNext().QueryInterface(Components.interfaces.nsIX509Cert); + if (cert.containsEmailAddress("%ABOUT_EMAIL%")) { + try { + certdb.deleteCertificate(cert); + } catch (ignore) {} + } + } + pref("%PROPS_FILE%.installer.timestamp", "-1"); + } + + // Register the specified protocol to open with the specified application + function registerProtocol() { + // Only register if platform needs it (e.g. macOS) + var trayApp = "%APP_PATH%"; + if (!trayApp) { return; } + try { + var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService); + var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService); + + var file = Components.classes["@mozilla.org/file/local;1"].createInstance(Components.interfaces.nsIFile); + file.initWithPath(trayApp); + + var lhandler = Components.classes["@mozilla.org/uriloader/local-handler-app;1"].createInstance(Components.interfaces.nsILocalHandlerApp); + lhandler.executable = file; + lhandler.name = "%PROPS_FILE%"; + + var protocol = pservice.getProtocolHandlerInfo("%DATA_DIR%"); + protocol.preferredApplicationHandler = lhandler; + protocol.preferredAction = 2; // useHelperApp + protocol.alwaysAskBeforeHandling = false; + hservice.store(protocol); + } catch(ignore) {} + } + + // De-register the specified protocol from opening with the specified application + function unregisterProtocol() { + // Only register if platform needs it (e.g. macOS) + var trayApp = "%APP_PATH%"; + if (!trayApp) { return; } + try { + var hservice = Components.classes["@mozilla.org/uriloader/handler-service;1"].getService(Components.interfaces.nsIHandlerService); + var pservice = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].getService(Components.interfaces.nsIExternalProtocolService); + hservice.remove(pservice.getProtocolHandlerInfo("%DATA_DIR%")); + } catch(ignore) {} + } + + // Get certdb object + function getCertDB() { + // Import certificate using NSS certdb API (http://tinyurl.com/x509certdb) + var id = "@mozilla.org/security/x509certdb;1"; + var db1 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB); + var db2 = db1; + try { + db2 = Components.classes[id].getService(Components.interfaces.nsIX509CertDB2); + } catch(ignore) {} + return db2; + } + + // The certificate to import (automatically generated by desktop installer) + function getCertData() { + return "%CERT_DATA%"; + } + + // Whether or not an uninstall should occur, flagged by the installer/uninstaller + function needsUninstall() { + try { + if (getPref("%PROPS_FILE%.installer.timestamp") == "-1") { + return false; + } + } + catch(notfound) { + return false; + } + return %UNINSTALL%; + } + } +}; + +Components.utils.import("resource://gre/modules/Services.jsm"); +Services.obs.addObserver(serviceObserver, "profile-after-change", false); \ No newline at end of file diff --git a/old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java b/old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java new file mode 100755 index 0000000..d689e26 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/firefox/locator/AppAlias.java @@ -0,0 +1,91 @@ +package qz.installer.certificate.firefox.locator; + +import java.util.Locale; + +public enum AppAlias { + // Tor Browser intentionally excluded; Tor's proxy blocks localhost connections + FIREFOX( + new Alias("Mozilla", "Mozilla Firefox", "org.mozilla.firefox", true), + new Alias("Mozilla", "Firefox Developer Edition", "org.mozilla.firefoxdeveloperedition", true), + new Alias("Mozilla", "Firefox Nightly", "org.mozilla.nightly", true), + new Alias("Mozilla", "SeaMonkey", "org.mozilla.seamonkey", false), + new Alias("Waterfox", "Waterfox", "net.waterfox.waterfoxcurrent", true), + new Alias("Waterfox", "Waterfox Classic", "org.waterfoxproject.waterfox classic", false), + new Alias("Mozilla", "Pale Moon", "org.mozilla.palemoon", false), + // IceCat is technically enterprise ready, but not officially distributed for macOS, Windows + new Alias("Mozilla", "IceCat", "org.gnu.icecat", false) + ); + Alias[] aliases; + AppAlias(Alias... aliases) { + this.aliases = aliases; + } + + public Alias[] getAliases() { + return aliases; + } + + public static Alias findAlias(AppAlias appAlias, String appName, boolean stripVendor) { + if (appName != null) { + for (Alias alias : appAlias.aliases) { + if (appName.toLowerCase(Locale.ENGLISH).matches(alias.getName(stripVendor).toLowerCase(Locale.ENGLISH))) { + return alias; + } + } + } + return null; + } + + public static class Alias { + private String vendor; + private String name; + private String bundleId; + private boolean enterpriseReady; + private String posix; + + public Alias(String vendor, String name, String bundleId, boolean enterpriseReady) { + this.name = name; + this.vendor = vendor; + this.bundleId = bundleId; + this.enterpriseReady = enterpriseReady; + this.posix = getName(true).replaceAll(" ", "").toLowerCase(Locale.ENGLISH); + } + + public String getVendor() { + return vendor; + } + + public String getName() { + return name; + } + + /** + * Remove vendor prefix if exists + */ + public String getName(boolean stripVendor) { + if(stripVendor && "Mozilla".equals(vendor) && name.startsWith(vendor)) { + return name.substring(vendor.length()).trim(); + } + return name; + } + + public String getBundleId() { + return bundleId; + } + + public String getPosix() { + return posix; + } + + /** + * Returns whether or not the app is known to recognizes enterprise policies, such as GPO + */ + public boolean isEnterpriseReady() { + return enterpriseReady; + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java b/old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java new file mode 100755 index 0000000..9d76f40 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/firefox/locator/AppInfo.java @@ -0,0 +1,100 @@ +package qz.installer.certificate.firefox.locator; + +import com.github.zafarkhaja.semver.Version; + +import java.nio.file.Path; +import qz.installer.certificate.firefox.locator.AppAlias.Alias; + +/** + * Container class for installed app information + */ +public class AppInfo { + private AppAlias.Alias alias; + private Path path; + private Path exePath; + private Version version; + + public AppInfo(Alias alias, Path exePath, String version) { + this.alias = alias; + this.path = exePath.getParent(); + this.exePath = exePath; + this.version = parseVersion(version); + } + + public AppInfo(Alias alias, Path path, Path exePath, String version) { + this.alias = alias; + this.path = path; + this.exePath = exePath; + this.version = parseVersion(version); + } + + public AppInfo(Alias alias, Path exePath) { + this.alias = alias; + this.path = exePath.getParent(); + this.exePath = exePath; + } + + public Alias getAlias() { + return alias; + } + + public String getName(boolean stripVendor) { + return alias.getName(stripVendor); + } + + public Path getExePath() { + return exePath; + } + + public Path getPath() { + return path; + } + + public void setPath(Path path) { + this.path = path; + } + + public Version getVersion() { + return version; + } + + public void setVersion(String version) { + this.version = parseVersion(version); + } + + public void setVersion(Version version) { + this.version = version; + } + + private static Version parseVersion(String version) { + try { + // Ensure < 3 octets (e.g. "56.0") doesn't failing + while(version.split("\\.").length < 3) { + version = version + ".0"; + } + return Version.valueOf(version); + } catch(Exception ignore1) { + // Catch poor formatting (e.g. "97.0a1"), try to use major version only + if(version.split("\\.").length > 0) { + try { + String[] tryFix = version.split("\\."); + return Version.valueOf(tryFix[0] + ".0.0-unknown"); + } catch(Exception ignore2) {} + } + } + return null; + } + + @Override + public boolean equals(Object o) { + if(o instanceof AppInfo && o != null && path != null) { + return path.equals(((AppInfo)o).getPath()); + } + return false; + } + + @Override + public String toString() { + return alias + " " + path; + } +} diff --git a/old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java b/old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java new file mode 100755 index 0000000..94c40d1 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/firefox/locator/AppLocator.java @@ -0,0 +1,87 @@ +package qz.installer.certificate.firefox.locator; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; + +public abstract class AppLocator { + protected static final Logger log = LogManager.getLogger(AppLocator.class); + + private static AppLocator INSTANCE = getPlatformSpecificAppLocator(); + + public abstract ArrayList locate(AppAlias appAlias); + public abstract ArrayList getPidPaths(ArrayList pids); + + @SuppressWarnings("unused") + public ArrayList getPids(String ... processNames) { + return getPids(new ArrayList<>(Arrays.asList(processNames))); + } + + /** + * Linux, Mac + */ + public ArrayList getPids(ArrayList processNames) { + String[] response; + ArrayList pidList = new ArrayList<>(); + + if(processNames.contains("firefox") && !(SystemUtilities.isWindows() || SystemUtilities.isMac())) { + processNames.add("MainThread"); // Workaround Firefox 79 https://github.com/qzind/tray/issues/701 + processNames.add("GeckoMain"); // Workaround Firefox 94 https://bugzilla.mozilla.org/show_bug.cgi?id=1742606 + } + + if (processNames.size() == 0) return pidList; + + // Quoting handled by the command processor (e.g. pgrep -x "myapp|my app" is perfectly valid) + String data = ShellUtilities.executeRaw("pgrep", "-x", String.join("|", processNames)); + + //Splitting an empty string results in a 1 element array, this is not what we want + if (!data.isEmpty()) { + response = data.split("\\s*\\r?\\n"); + Collections.addAll(pidList, response); + } + + return pidList; + } + + public static ArrayList getRunningPaths(ArrayList appList) { + return getRunningPaths(appList, null); + } + + /** + * Gets the path to the running executables matching on AppInfo.getExePath + * This is resource intensive; if a non-null cache is provided, it will return that instead + */ + public static ArrayList getRunningPaths(ArrayList appList, ArrayList cache) { + if(cache == null) { + ArrayList appNames = new ArrayList<>(); + for(AppInfo app : appList) { + String exeName = app.getExePath().getFileName().toString(); + if (!appNames.contains(exeName)) appNames.add(exeName); + } + cache = INSTANCE.getPidPaths(INSTANCE.getPids(appNames)); + } + + return cache; + } + + public static AppLocator getInstance() { + return INSTANCE; + } + + private static AppLocator getPlatformSpecificAppLocator() { + switch(SystemUtilities.getOs()) { + case WINDOWS: + return new WindowsAppLocator(); + case MAC: + return new MacAppLocator(); + default: + return new LinuxAppLocator(); + } + } +} diff --git a/old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java b/old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java new file mode 100755 index 0000000..24a212a --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/firefox/locator/LinuxAppLocator.java @@ -0,0 +1,159 @@ +package qz.installer.certificate.firefox.locator; + +import org.apache.commons.io.FilenameUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; +import qz.utils.UnixUtilities; + +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; + +public class LinuxAppLocator extends AppLocator { + private static final Logger log = LogManager.getLogger(LinuxAppLocator.class); + + public ArrayList locate(AppAlias appAlias) { + ArrayList appList = new ArrayList<>(); + + // Workaround for calling "firefox --version" as sudo + String[] env = appendPaths("HOME=/tmp"); + + // Search for matching executable in all path values + aliasLoop: + for(AppAlias.Alias alias : appAlias.aliases) { + // Add non-standard app search locations (e.g. Fedora) + for (String dirname : appendPaths(alias.getPosix(), "/usr/lib/$/bin", "/usr/lib64/$/bin", "/usr/lib/$", "/usr/lib64/$")) { + Path path = Paths.get(dirname, alias.getPosix()); + if (Files.isRegularFile(path) && Files.isExecutable(path)) { + log.info("Found {} {}: {}, investigating...", alias.getVendor(), alias.getName(true), path); + try { + File file = path.toFile().getCanonicalFile(); // fix symlinks + if(file.getPath().endsWith("/snap")) { + // Ubuntu 22.04+ ships Firefox as a snap + // Snaps are read-only and are symlinks back to /usr/bin/snap + // Reset the executable back to /snap/bin/firefox to get proper version information + file = path.toFile(); + } + if(file.getPath().endsWith(".sh")) { + // Legacy Ubuntu likes to use .../firefox/firefox.sh, return .../firefox/firefox instead + log.info("Found an '.sh' file: {}, removing file extension: {}", file, file = new File(FilenameUtils.removeExtension(file.getPath()))); + } + String contentType = Files.probeContentType(file.toPath()); + if(contentType == null) { + // Fallback to commandline per https://bugs.openjdk.org/browse/JDK-8188228 + contentType = ShellUtilities.executeRaw("file", "--mime-type", "--brief", file.getPath()).trim(); + } + if(contentType != null && contentType.endsWith("/x-shellscript")) { + if(UnixUtilities.isFedora()) { + // Firefox's script is full of variables and not parsable, fallback to /usr/lib64/$, etc + log.info("Found shell script at {}, but we're on Fedora, so we'll look in some known locations instead.", file.getPath()); + continue; + } + // Debian and Arch like to place a stub script directly in /usr/bin/ + // TODO: Split into a function; possibly recurse on search paths + log.info("{} bin was expected but script found... Reading...", appAlias.name()); + BufferedReader reader = new BufferedReader(new FileReader(file)); + String line; + while((line = reader.readLine()) != null) { + if(line.startsWith("exec") && line.contains(alias.getPosix())) { + String[] parts = line.split(" "); + // Get the app name after "exec" + if (parts.length > 1) { + log.info("Found a familiar line '{}', using '{}'", line, parts[1]); + Path p = Paths.get(parts[1]); + String exec = parts[1]; + // Handle edge-case for esr release + if(!p.isAbsolute()) { + // Script doesn't contain the full path, go deeper + exec = Paths.get(dirname, exec).toFile().getCanonicalPath(); + log.info("Calculated full bin path {}", exec); + } + // Make sure it actually exists + if(!(file = new File(exec)).exists()) { + log.warn("Sorry, we couldn't detect the real path of {}. Skipping...", appAlias.name()); + continue aliasLoop; + } + break; + } + } + } + reader.close(); + } else { + log.info("Assuming {} {} is installed: {}", alias.getVendor(), alias.getName(true), file); + } + AppInfo appInfo = new AppInfo(alias, file.toPath()); + if(file.getPath().startsWith("/snap/")) { + // Ubuntu 22.04+ uses snaps, fallback to a sane "path" value + String snapPath = file.getPath(); // e.g. /snap/bin/firefox + snapPath = snapPath.replaceFirst("/bin/", "/"); + snapPath += "/current"; + appInfo.setPath(Paths.get(snapPath)); + } + + appList.add(appInfo); + + // Call "--version" on executable to obtain version information + Process p = Runtime.getRuntime().exec(new String[] {file.getPath(), "--version" }, env); + BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream())); + String version = reader.readLine(); + reader.close(); + if (version != null) { + log.info("We obtained version info: {}, but we'll need to parse it", version); + if(version.contains(" ")) { + String[] split = version.split(" "); + String parsed = split[split.length - 1]; + String stripped = parsed.replaceAll("[^\\d.]", ""); + appInfo.setVersion(stripped); + if(!parsed.equals(stripped)) { + // Add the meta data back (e.g. "esr") + appInfo.getVersion().setBuildMetadata(parsed.replaceAll("[\\d.]", "")); + } + } else { + appInfo.setVersion(version.trim()); + } + } + break; + } catch(Exception e) { + log.warn("Something went wrong getting app info for {} {}", alias.getVendor(), alias.getName(true), e); + } + } + } + } + + return appList; + } + + @Override + public ArrayList getPidPaths(ArrayList pids) { + ArrayList pathList = new ArrayList<>(); + + for(String pid : pids) { + try { + pathList.add(Paths.get("/proc/", pid, !SystemUtilities.isSolaris() ? "/exe" : "/path/a.out").toRealPath()); + } catch(IOException e) { + log.warn("Process {} vanished", pid); + } + } + + return pathList; + } + + /** + * Returns a PATH value with provided paths appended, replacing "$" with POSIX app name + * Useful for strange Firefox install locations (e.g. Fedora) + * + * Usage: appendPaths("firefox", "/usr/lib64"); + * + */ + private static String[] appendPaths(String posix, String ... prefixes) { + String newPath = System.getenv("PATH"); + for (String prefix : prefixes) { + newPath = newPath + File.pathSeparator + prefix.replaceAll("\\$", posix); + } + return newPath.split(File.pathSeparator); + } +} diff --git a/old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java b/old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java new file mode 100755 index 0000000..434d8be --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/firefox/locator/MacAppLocator.java @@ -0,0 +1,168 @@ +package qz.installer.certificate.firefox.locator; + +import com.sun.jna.Library; +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.ptr.IntByReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; +import qz.utils.ShellUtilities; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; + +public class MacAppLocator extends AppLocator{ + protected static final Logger log = LogManager.getLogger(MacAppLocator.class); + + private static String[] BLACKLISTED_PATHS = new String[]{"/Volumes/", "/.Trash/", "/Applications (Parallels)/" }; + + /** + * Helper class for finding key/value siblings from the DDM + */ + private enum SiblingNode { + NAME("_name"), + PATH("path"), + VERSION("version"); + + private String key; + private boolean wants; + + SiblingNode(String key) { + this.key = key; + this.wants = false; + } + + private boolean isKey(Node node) { + if (node.getNodeName().equals("key") && node.getTextContent().equals(key)) { + return true; + } + return false; + } + } + + @Override + public ArrayList locate(AppAlias appAlias) { + ArrayList appList = new ArrayList<>(); + Document doc; + + try { + // system_profile benchmarks about 30% better than lsregister + Process p = Runtime.getRuntime().exec(new String[] {"system_profiler", "SPApplicationsDataType", "-xml"}, ShellUtilities.envp); + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + // don't let the fail parsing per https://github.com/qzind/tray/issues/809 + dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); + doc = dbf.newDocumentBuilder().parse(p.getInputStream()); + } catch(IOException | ParserConfigurationException | SAXException e) { + log.warn("Could not retrieve app listing for {}", appAlias.name(), e); + return appList; + } + doc.normalizeDocument(); + + NodeList nodeList = doc.getElementsByTagName("dict"); + for (int i = 0; i < nodeList.getLength(); i++) { + NodeList dict = nodeList.item(i).getChildNodes(); + HashMap foundApp = new HashMap<>(); + for (int j = 0; j < dict.getLength(); j++) { + Node node = dict.item(j); + if (node.getNodeType() == Node.ELEMENT_NODE) { + for (SiblingNode sibling : SiblingNode.values()) { + if (sibling.wants) { + foundApp.put(sibling, node.getTextContent()); + sibling.wants = false; + break; + } else if(sibling.isKey(node)) { + sibling.wants = true; + break; + } + } + } + } + AppAlias.Alias alias; + if((alias = AppAlias.findAlias(appAlias, foundApp.get(SiblingNode.NAME), true)) != null) { + appList.add(new AppInfo(alias, Paths.get(foundApp.get(SiblingNode.PATH)), + getExePath(foundApp.get(SiblingNode.PATH)), foundApp.get(SiblingNode.VERSION) + )); + } + } + + // Remove blacklisted paths + Iterator appInfoIterator = appList.iterator(); + while(appInfoIterator.hasNext()) { + AppInfo appInfo = appInfoIterator.next(); + for(String listEntry : BLACKLISTED_PATHS) { + if (appInfo.getPath() != null && appInfo.getPath().toString().contains(listEntry)) { + appInfoIterator.remove(); + } + } + } + return appList; + } + + @Override + public ArrayList getPidPaths(ArrayList pids) { + ArrayList processPaths = new ArrayList(); + for (String pid : pids) { + Pointer buf = new Memory(SystemB.PROC_PIDPATHINFO_MAXSIZE); + SystemB.INSTANCE.proc_pidpath(Integer.parseInt(pid), buf, SystemB.PROC_PIDPATHINFO_MAXSIZE); + processPaths.add(Paths.get(buf.getString(0).trim())); + } + return processPaths; + } + + /** + * Calculate executable path by parsing Contents/Info.plist + */ + private static Path getExePath(String appPath) { + Path path = Paths.get(appPath).toAbsolutePath().normalize(); + Path plist = path.resolve("Contents/Info.plist"); + Document doc; + try { + if(!plist.toFile().exists()) { + log.warn("Could not locate plist file for {}: {}", appPath, plist); + return null; + } + // Convert potentially binary plist files to XML + Process p = Runtime.getRuntime().exec(new String[] {"plutil", "-convert", "xml1", plist.toString(), "-o", "-"}, ShellUtilities.envp); + doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(p.getInputStream()); + } catch(IOException | ParserConfigurationException | SAXException e) { + log.warn("Could not parse plist file for {}: {}", appPath, appPath, e); + return null; + } + doc.normalizeDocument(); + + boolean upNext = false; + NodeList nodeList = doc.getElementsByTagName("dict"); + for (int i = 0; i < nodeList.getLength(); i++) { + NodeList dict = nodeList.item(i).getChildNodes(); + for(int j = 0; j < dict.getLength(); j++) { + Node node = dict.item(j); + if ("key".equals(node.getNodeName()) && node.getTextContent().equals("CFBundleExecutable")) { + upNext = true; + } else if (upNext && "string".equals(node.getNodeName())) { + return path.resolve("Contents/MacOS/" + node.getTextContent()); + } + } + } + return null; + } + + private interface SystemB extends Library { + SystemB INSTANCE = Native.load("System", SystemB.class); + int PROC_ALL_PIDS = 1; + int PROC_PIDPATHINFO_MAXSIZE = 1024 * 4; + int sysctlbyname(String name, Pointer oldp, IntByReference oldlenp, Pointer newp, int newlen); + int proc_listpids(int type, int typeinfo, int[] buffer, int buffersize); + int proc_pidpath(int pid, Pointer buffer, int buffersize); + } +} diff --git a/old code/tray/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java b/old code/tray/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java new file mode 100755 index 0000000..a362462 --- /dev/null +++ b/old code/tray/src/qz/installer/certificate/firefox/locator/WindowsAppLocator.java @@ -0,0 +1,142 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.certificate.firefox.locator; + +import com.sun.jna.Memory; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import com.sun.jna.platform.win32.Kernel32; +import com.sun.jna.platform.win32.Psapi; +import com.sun.jna.platform.win32.Tlhelp32; +import com.sun.jna.platform.win32.WinNT; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.installer.certificate.firefox.locator.AppAlias.Alias; +import qz.utils.WindowsUtilities; + + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Locale; + +import static com.sun.jna.platform.win32.WinReg.HKEY_LOCAL_MACHINE; + +public class WindowsAppLocator extends AppLocator{ + protected static final Logger log = LogManager.getLogger(MacAppLocator.class); + + private static String REG_TEMPLATE = "Software\\%s%s\\%s%s"; + + @Override + public ArrayList locate(AppAlias appAlias) { + ArrayList appList = new ArrayList<>(); + for (Alias alias : appAlias.aliases) { + if (alias.getVendor() != null) { + String[] suffixes = new String[]{ "", " ESR"}; + String[] prefixes = new String[]{ "", "WOW6432Node\\"}; + for (String suffix : suffixes) { + for (String prefix : prefixes) { + String key = String.format(REG_TEMPLATE, prefix, alias.getVendor(), alias.getName(), suffix); + AppInfo appInfo = getAppInfo(alias, key, suffix); + if (appInfo != null && !appList.contains(appInfo)) { + appList.add(appInfo); + } + } + } + } + } + return appList; + } + + @Override + public ArrayList getPids(ArrayList processNames) { + ArrayList pidList = new ArrayList<>(); + + if (processNames.isEmpty()) return pidList; + + Tlhelp32.PROCESSENTRY32 pe32 = new Tlhelp32.PROCESSENTRY32(); + pe32.dwSize = new WinNT.DWORD(pe32.size()); + + // Fetch a snapshot of all processes + WinNT.HANDLE hSnapshot = Kernel32.INSTANCE.CreateToolhelp32Snapshot(Tlhelp32.TH32CS_SNAPPROCESS, new WinNT.DWORD(0)); + if (hSnapshot.equals(WinNT.INVALID_HANDLE_VALUE)) { + log.warn("Process snapshot has invalid handle"); + return pidList; + } + + if (Kernel32.INSTANCE.Process32First(hSnapshot, pe32)) { + do { + String processName = Native.toString(pe32.szExeFile); + if(processNames.contains(processName.toLowerCase(Locale.ENGLISH))) { + pidList.add(pe32.th32ProcessID.toString()); + } + } while (Kernel32.INSTANCE.Process32Next(hSnapshot, pe32)); + } + + Kernel32.INSTANCE.CloseHandle(hSnapshot); + return pidList; + } + + + @Override + public ArrayList getPidPaths(ArrayList pids) { + ArrayList pathList = new ArrayList<>(); + + for(String pid : pids) { + WinNT.HANDLE hProcess = Kernel32.INSTANCE.OpenProcess(WinNT.PROCESS_QUERY_INFORMATION | WinNT.PROCESS_VM_READ, false, Integer.parseInt(pid)); + if (hProcess == null) { + log.warn("Handle for PID {} is missing, skipping.", pid); + continue; + } + + int bufferSize = WinNT.MAX_PATH; + Pointer buffer = new Memory(bufferSize * Native.WCHAR_SIZE); + + if (Psapi.INSTANCE.GetModuleFileNameEx(hProcess, null, buffer, bufferSize) == 0) { + log.warn("Full path to PID {} is empty, skipping.", pid); + Kernel32.INSTANCE.CloseHandle(hProcess); + continue; + } + + Kernel32.INSTANCE.CloseHandle(hProcess); + pathList.add(Paths.get(Native.WCHAR_SIZE == 1 ? + buffer.getString(0) : + buffer.getWideString(0))); + } + return pathList; + } + + /** + * Use a proprietary Firefox-only technique for getting "PathToExe" registry value + */ + private static AppInfo getAppInfo(Alias alias, String key, String suffix) { + String version = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "CurrentVersion"); + if (version != null) { + version = version.split(" ")[0]; // chop off (x86 ...) + if (!suffix.isEmpty()) { + if (key.endsWith(suffix)) { + key = key.substring(0, key.length() - suffix.length()); + } + version = version + suffix; + } + String exePath = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key + " " + version + "\\bin", "PathToExe"); + + if (exePath != null) { + // SemVer: Replace spaces in suffixes with dashes + version = version.replaceAll(" ", "-"); + return new AppInfo(alias, Paths.get(exePath), version); + } else { + log.warn("Couldn't locate \"PathToExe\" for \"{}\" in \"{}\", skipping", alias.getName(), key); + } + } + return null; + } +} diff --git a/old code/tray/src/qz/installer/provision/ProvisionInstaller.java b/old code/tray/src/qz/installer/provision/ProvisionInstaller.java new file mode 100755 index 0000000..74c64d7 --- /dev/null +++ b/old code/tray/src/qz/installer/provision/ProvisionInstaller.java @@ -0,0 +1,161 @@ +package qz.installer.provision; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import qz.build.provision.Step; +import qz.build.provision.params.Os; +import qz.build.provision.params.Phase; +import qz.build.provision.params.types.Script; +import qz.build.provision.params.types.Software; +import qz.common.Constants; +import qz.installer.provision.invoker.*; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.ArrayList; + +import static qz.common.Constants.*; +import static qz.utils.FileUtilities.*; + +public class ProvisionInstaller { + protected static final Logger log = LogManager.getLogger(ProvisionInstaller.class); + private ArrayList steps; + + static { + // Populate variables for scripting environment + ShellUtilities.addEnvp("APP_TITLE", ABOUT_TITLE, + "APP_VERSION", VERSION, + "APP_ABBREV", PROPS_FILE, + "APP_VENDOR", ABOUT_COMPANY, + "APP_VENDOR_ABBREV", DATA_DIR, + "APP_ARCH", SystemUtilities.getArch(), + "APP_OS", SystemUtilities.getOs(), + "APP_DIR", SystemUtilities.getAppPath(), + "APP_USER_DIR", USER_DIR, + "APP_SHARED_DIR", SHARED_DIR); + } + + public ProvisionInstaller(Path relativePath) throws IOException, JSONException { + this(relativePath, relativePath.resolve(Constants.PROVISION_FILE).toFile()); + } + + public ProvisionInstaller(Path relativePath, File jsonFile) throws IOException, JSONException { + if(!jsonFile.exists()) { + log.info("Provision file not found '{}', skipping", jsonFile); + this.steps = new ArrayList<>(); + return; + } + this.steps = parse(FileUtils.readFileToString(jsonFile, StandardCharsets.UTF_8), relativePath); + } + + /** + * Package private for internal testing only + * Assumes files located in ./resources/ subdirectory + */ + ProvisionInstaller(Class relativeClass, InputStream in) throws IOException, JSONException { + this(relativeClass, IOUtils.toString(in, StandardCharsets.UTF_8)); + } + + /** + * Package private for internal testing only + * Assumes files located in ./resources/ subdirectory + */ + ProvisionInstaller(Class relativeClass, String jsonData) throws JSONException { + this.steps = parse(jsonData, relativeClass); + } + + public void invoke(Phase phase) { + for(Step step : this.steps) { + if(phase == null || step.getPhase() == phase) { + try { + invokeStep(step); + } + catch(Exception e) { + log.error("[PROVISION] Provisioning step failed '{}'", step, e); + } + } + } + } + + public void invoke() { + invoke(null); + } + + private static ArrayList parse(String jsonData, Object relativeObject) throws JSONException { + return parse(new JSONArray(jsonData), relativeObject); + } + + private boolean invokeStep(Step step) throws Exception { + if(Os.matchesHost(step.getOs())) { + log.info("[PROVISION] Invoking step '{}'", step.toString()); + } else { + log.info("[PROVISION] Skipping step for different OS '{}'", step.toString()); + return false; + } + + Invokable invoker; + switch(step.getType()) { + case CA: + invoker = new CaInvoker(step, PropertyInvoker.getProperties(step)); + break; + case CERT: + invoker = new CertInvoker(step); + break; + case CONF: + invoker = new ConfInvoker(step); + break; + case SCRIPT: + invoker = new ScriptInvoker(step); + break; + case SOFTWARE: + invoker = new SoftwareInvoker(step); + break; + case REMOVER: + invoker = new RemoverInvoker(step); + break; + case RESOURCE: + invoker = new ResourceInvoker(step); + break; + case PREFERENCE: + invoker = new PropertyInvoker(step, PropertyInvoker.getPreferences(step)); + break; + case PROPERTY: + invoker = new PropertyInvoker(step, PropertyInvoker.getProperties(step)); + break; + default: + throw new UnsupportedOperationException("Type " + step.getType() + " is not yet supported."); + } + return invoker.invoke(); + } + + public ArrayList getSteps() { + return steps; + } + + private static ArrayList parse(JSONArray jsonArray, Object relativeObject) throws JSONException { + ArrayList steps = new ArrayList<>(); + for(int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonStep = jsonArray.getJSONObject(i); + try { + steps.add(Step.parse(jsonStep, relativeObject)); + } catch(Exception e) { + log.warn("[PROVISION] Unable to add step '{}'", jsonStep, e); + } + } + return steps; + } + + public static boolean shouldBeExecutable(Path path) { + return Script.parse(path) != null || Software.parse(path) != Software.UNKNOWN; + } +} diff --git a/old code/tray/src/qz/installer/provision/invoker/CaInvoker.java b/old code/tray/src/qz/installer/provision/invoker/CaInvoker.java new file mode 100755 index 0000000..2158217 --- /dev/null +++ b/old code/tray/src/qz/installer/provision/invoker/CaInvoker.java @@ -0,0 +1,49 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.common.PropertyHelper; +import qz.utils.ArgValue; +import qz.utils.FileUtilities; + +import java.io.File; +import java.io.IOException; + +/** + * Combines ResourceInvoker and PropertyInvoker to deploy a file and set a property to its deployed path + */ +public class CaInvoker extends InvokableResource { + Step step; + PropertyHelper properties; + + public CaInvoker(Step step, PropertyHelper properties) { + this.step = step; + this.properties = properties; + } + + @Override + public boolean invoke() throws IOException { + // First, write our cert file + File caCert = dataToFile(step); + if(caCert == null) { + return false; + } + + // Next, handle our property step + Step propsStep = step.clone(); + + // If the property already exists, snag it + String key = ArgValue.AUTHCERT_OVERRIDE.getMatch(); + String value = caCert.getPath(); + if (properties.containsKey(key)) { + value = properties.getProperty(key) + FileUtilities.FILE_SEPARATOR + value; + } + + propsStep.setData(String.format("%s=%s", key, value)); + + if (new PropertyInvoker(propsStep, properties).invoke()) { + return true; + } + + return false; + } +} diff --git a/old code/tray/src/qz/installer/provision/invoker/CertInvoker.java b/old code/tray/src/qz/installer/provision/invoker/CertInvoker.java new file mode 100755 index 0000000..8e7b32e --- /dev/null +++ b/old code/tray/src/qz/installer/provision/invoker/CertInvoker.java @@ -0,0 +1,26 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.common.Constants; +import qz.utils.FileUtilities; + +import java.io.File; + +import static qz.utils.ArgParser.ExitStatus.*; + +public class CertInvoker extends InvokableResource { + private Step step; + + public CertInvoker(Step step) { + this.step = step; + } + + @Override + public boolean invoke() throws Exception { + File cert = dataToFile(step); + if(cert == null) { + return false; + } + return FileUtilities.addToCertList(Constants.ALLOW_FILE, cert) == SUCCESS; + } +} diff --git a/old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java b/old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java new file mode 100755 index 0000000..b574e43 --- /dev/null +++ b/old code/tray/src/qz/installer/provision/invoker/ConfInvoker.java @@ -0,0 +1,46 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.common.PropertyHelper; +import qz.utils.SystemUtilities; + +import java.util.AbstractMap; + +public class ConfInvoker extends PropertyInvoker { + public ConfInvoker(Step step) { + super(step, new PropertyHelper(calculateConfPath(step))); + } + + public static String calculateConfPath(Step step) { + String relativePath = step.getArgs().get(0); + if(SystemUtilities.isMac()) { + return SystemUtilities.getJarParentPath(). + resolve("../PlugIns/Java.runtime/Contents/Home/conf"). + resolve(relativePath). + normalize() + .toString(); + } else { + return SystemUtilities.getJarParentPath() + .resolve("runtime/conf") + .resolve(relativePath) + .normalize() + .toString(); + } + } + + @Override + public boolean invoke() { + Step step = getStep(); + // Java uses the same "|" delimiter as we do, only parse one property at a time + AbstractMap.SimpleEntry pair = parsePropertyPair(step, step.getData()); + if (!pair.getValue().isEmpty()) { + properties.setProperty(pair); + if (properties.save()) { + log.info("Successfully provisioned '1' '{}'", step.getType()); + return true; + } + log.error("An error occurred saving properties '{}' to file", step.getData()); + } + return false; + } +} diff --git a/old code/tray/src/qz/installer/provision/invoker/Invokable.java b/old code/tray/src/qz/installer/provision/invoker/Invokable.java new file mode 100755 index 0000000..508e489 --- /dev/null +++ b/old code/tray/src/qz/installer/provision/invoker/Invokable.java @@ -0,0 +1,10 @@ +package qz.installer.provision.invoker; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public interface Invokable { + Logger log = LogManager.getLogger(Invokable.class); + + boolean invoke() throws Exception; +} diff --git a/old code/tray/src/qz/installer/provision/invoker/InvokableResource.java b/old code/tray/src/qz/installer/provision/invoker/InvokableResource.java new file mode 100755 index 0000000..5a76d54 --- /dev/null +++ b/old code/tray/src/qz/installer/provision/invoker/InvokableResource.java @@ -0,0 +1,63 @@ +package qz.installer.provision.invoker; + +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.build.provision.Step; +import qz.build.provision.params.Type; +import qz.common.Constants; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +public abstract class InvokableResource implements Invokable { + static final Logger log = LogManager.getLogger(InvokableResource.class); + + public static File dataToFile(Step step) throws IOException { + Path resourcePath = Paths.get(step.getData()); + if(resourcePath.isAbsolute() || step.usingPath()) { + return pathResourceToFile(step); + } + if(step.usingClass()) { + return classResourceToFile(step); + } + return null; + } + + /** + * Resolves the resource directly from file + */ + private static File pathResourceToFile(Step step) { + String resourcePath = step.getData(); + Path dataPath = Paths.get(resourcePath); + return dataPath.isAbsolute() ? dataPath.toFile() : step.getRelativePath().resolve(resourcePath).toFile(); + } + + /** + * Copies resource from JAR to a temp file for use in installation + */ + private static File classResourceToFile(Step step) throws IOException { + // Resource may be inside the jar + InputStream in = step.getRelativeClass().getResourceAsStream("resources/" + step.getData()); + if(in == null) { + log.warn("Resource '{}' is missing, skipping step", step.getData()); + return null; + } + String suffix = "_" + Paths.get(step.getData()).getFileName().toString(); + File destination = File.createTempFile(Constants.DATA_DIR + "_provision_", suffix); + Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING); + IOUtils.closeQuietly(in); + + // Set scripts executable + if(step.getType() == Type.SCRIPT && !SystemUtilities.isWindows()) { + destination.setExecutable(true, false); + } + return destination; + } +} diff --git a/old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java b/old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java new file mode 100755 index 0000000..707bfd2 --- /dev/null +++ b/old code/tray/src/qz/installer/provision/invoker/PropertyInvoker.java @@ -0,0 +1,99 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.common.Constants; +import qz.common.PropertyHelper; +import qz.utils.FileUtilities; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.util.AbstractMap; +import java.util.HashMap; +import java.util.Map; + +public class PropertyInvoker implements Invokable { + private Step step; + PropertyHelper properties; + + public PropertyInvoker(Step step, PropertyHelper properties) { + this.step = step; + this.properties = properties; + } + + public boolean invoke() { + HashMap pairs = parsePropertyPairs(step); + if (!pairs.isEmpty()) { + for(Map.Entry pair : pairs.entrySet()) { + properties.setProperty(pair); + } + if (properties.save()) { + log.info("Successfully provisioned '{}' '{}'", pairs.size(), step.getType()); + return true; + } + log.error("An error occurred saving properties '{}' to file", step.getData()); + } + return false; + } + + public static PropertyHelper getProperties(Step step) { + File propertiesFile; + if(step.getRelativePath() != null) { + // Assume qz-tray.properties is one directory up from provision folder + // required to prevent installing to payload + propertiesFile = step.getRelativePath().getParent().resolve(Constants.PROPS_FILE + ".properties").toFile(); + } else { + // If relative path isn't set, fallback to the jar's parent path + propertiesFile = SystemUtilities.getJarParentPath(".").resolve(Constants.PROPS_FILE + ".properties").toFile(); + } + log.info("Provisioning '{}' to properties file: '{}'", step.getData(), propertiesFile); + return new PropertyHelper(propertiesFile); + } + + public static PropertyHelper getPreferences(Step step) { + return new PropertyHelper(FileUtilities.USER_DIR + File.separator + Constants.PREFS_FILE + ".properties"); + } + + public static HashMap parsePropertyPairs(Step step) { + HashMap pairs = new HashMap<>(); + if(step.getData() != null && !step.getData().trim().isEmpty()) { + String[] props = step.getData().split("\\|"); + for(String prop : props) { + AbstractMap.SimpleEntry pair = parsePropertyPair(step, prop); + if (pair != null) { + if(pairs.get(pair.getKey()) != null) { + log.warn("Property {} already exists, replacing [before: {}, after: {}] ", + pair.getKey(), pairs.get(pair.getKey()), pair.getValue()); + } + pairs.put(pair.getKey(), pair.getValue()); + } + } + } else { + log.error("Skipping Step '{}', Data is null or empty", step.getType()); + } + return pairs; + } + + + public static AbstractMap.SimpleEntry parsePropertyPair(Step step, String prop) { + if(prop.contains("=")) { + String[] pair = prop.split("=", 2); + if (!pair[0].trim().isEmpty()) { + if (!pair[1].trim().isEmpty()) { + return new AbstractMap.SimpleEntry<>(pair[0], pair[1]); + } else { + log.warn("Skipping '{}' '{}', property value is malformed", step.getType(), prop); + } + } else { + log.warn("Skipping '{}' '{}', property name is malformed", step.getType(), prop); + } + } else { + log.warn("Skipping '{}' '{}', property is malformed", step.getType(), prop); + } + + return null; + } + + public Step getStep() { + return step; + } +} diff --git a/old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java b/old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java new file mode 100755 index 0000000..3b8271a --- /dev/null +++ b/old code/tray/src/qz/installer/provision/invoker/RemoverInvoker.java @@ -0,0 +1,100 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.build.provision.params.Os; +import qz.build.provision.params.types.Remover; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; + +public class RemoverInvoker extends InvokableResource { + private Step step; + private String aboutTitle; // e.g. "QZ Tray" + private String propsFile; // e.g. "qz-tray" + private String dataDir; // e.g. "qz" + + + public RemoverInvoker(Step step) { + this.step = step; + Remover remover = Remover.parse(step.getData()); + if(remover == Remover.CUSTOM) { + // Fields are comma delimited in the data field + parseCustomFromData(step.getData()); + } else { + aboutTitle = remover.getAboutTitle(); + propsFile = remover.getPropsFile(); + dataDir = remover.getDataDir(); + } + } + + @Override + public boolean invoke() throws Exception { + ArrayList command = getRemoveCommand(); + if(command.size() == 0) { + log.info("An existing installation of '{}' was not found. Skipping.", aboutTitle); + return true; + } + boolean success = ShellUtilities.execute(command.toArray(new String[command.size()])); + if(!success) { + log.error("An error occurred invoking [{}]", step.getData()); + } + return success; + } + + public void parseCustomFromData(String data) { + String[] parts = data.split(","); + aboutTitle = parts[0].trim(); + propsFile = parts[1].trim(); + dataDir = parts[2].trim(); + } + + /** + * Returns the installer command (including the installer itself and if needed, arguments) to + * invoke the installer file + */ + public ArrayList getRemoveCommand() { + ArrayList removeCmd = new ArrayList<>(); + Os os = SystemUtilities.getOs(); + switch(os) { + case WINDOWS: + Path win = Paths.get(System.getenv("PROGRAMFILES")) + .resolve(aboutTitle) + .resolve("uninstall.exe"); + + if(win.toFile().exists()) { + removeCmd.add(win.toString()); + removeCmd.add("/S"); + break; + } + case MAC: + Path legacy = Paths.get("/Applications") + .resolve(aboutTitle + ".app") + .resolve("Contents") + .resolve("uninstall"); + + Path mac = Paths.get("/Applications") + .resolve(aboutTitle + ".app") + .resolve("Contents") + .resolve("Resources") + .resolve("uninstall"); + + if(legacy.toFile().exists()) { + removeCmd.add(legacy.toString()); + } else if(mac.toFile().exists()) { + removeCmd.add(mac.toString()); + } + break; + default: + Path linux = Paths.get("/opt") + .resolve(propsFile) + .resolve("uninstall"); + if(linux.toFile().exists()) { + removeCmd.add(linux.toString()); + } + } + return removeCmd; + } +} diff --git a/old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java b/old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java new file mode 100755 index 0000000..3602608 --- /dev/null +++ b/old code/tray/src/qz/installer/provision/invoker/ResourceInvoker.java @@ -0,0 +1,19 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; + +/** + * Stub class for deploying an otherwise "action-less" resource, only to be used by other tasks + */ +public class ResourceInvoker extends InvokableResource { + private Step step; + + public ResourceInvoker(Step step) { + this.step = step; + } + + @Override + public boolean invoke() throws Exception { + return dataToFile(step) != null; + } +} diff --git a/old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java b/old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java new file mode 100755 index 0000000..bbd9d9a --- /dev/null +++ b/old code/tray/src/qz/installer/provision/invoker/ScriptInvoker.java @@ -0,0 +1,77 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.build.provision.params.Os; +import qz.build.provision.params.types.Script; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.util.ArrayList; + +public class ScriptInvoker extends InvokableResource { + private Step step; + + public ScriptInvoker(Step step) { + this.step = step; + } + + @Override + public boolean invoke() throws Exception { + File script = dataToFile(step); + if(script == null) { + return false; + } + Script engine = Script.parse(step.getData()); + ArrayList command = getInterpreter(engine); + if(command.isEmpty() && SystemUtilities.isWindows()) { + log.warn("No interpreter found for {}, skipping", step.getData()); + return false; + } + command.add(script.toString()); + boolean success = ShellUtilities.execute(command.toArray(new String[command.size()])); + if(!success) { + log.error("An error occurred invoking [{}]", step.getData()); + } + return success; + } + + + /** + * Returns the interpreter command (and if needed, arguments) to invoke the script file + * + * An empty array will fall back to Unix "shebang" notation, e.g. #!/usr/bin/python3 + * which will allow the OS to select the correct interpreter for the given file + * + * No special attention is given to "shebang", behavior may differ between OSs + */ + private static ArrayList getInterpreter(Script engine) { + ArrayList interpreter = new ArrayList<>(); + Os osType = SystemUtilities.getOs(); + switch(engine) { + case PS1: + if(osType == Os.WINDOWS) { + interpreter.add("powershell.exe"); + } else if(osType == Os.MAC) { + interpreter.add("/usr/local/bin/pwsh"); + } else { + interpreter.add("pwsh"); + } + interpreter.add("-File"); + break; + case PY: + interpreter.add(osType == Os.WINDOWS ? "python3.exe" : "python3"); + break; + case BAT: + interpreter.add(osType == Os.WINDOWS ? "cmd.exe" : "wineconsole"); + break; + case RB: + interpreter.add(osType == Os.WINDOWS ? "ruby.exe" : "ruby"); + break; + case SH: + default: + // Allow the environment to parse it from the shebang at invocation time + } + return interpreter; + } +} diff --git a/old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java b/old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java new file mode 100755 index 0000000..c446208 --- /dev/null +++ b/old code/tray/src/qz/installer/provision/invoker/SoftwareInvoker.java @@ -0,0 +1,87 @@ +package qz.installer.provision.invoker; + +import qz.build.provision.Step; +import qz.build.provision.params.Os; +import qz.build.provision.params.types.Software; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public class SoftwareInvoker extends InvokableResource { + private Step step; + + public SoftwareInvoker(Step step) { + this.step = step; + } + + @Override + public boolean invoke() throws Exception { + File payload = dataToFile(step); + if(payload == null) { + return false; + } + Software installer = Software.parse(step.getData()); + ArrayList command = getInstallCommand(installer, step.getArgs(), payload); + boolean success = ShellUtilities.execute(command.toArray(new String[command.size()]), payload.getParentFile()); + if(!success) { + log.error("An error occurred invoking [{}]", step.getData()); + } + return success; + } + + /** + * Returns the installer command (including the installer itself and if needed, arguments) to + * invoke the installer file + */ + public ArrayList getInstallCommand(Software installer, List args, File payload) { + ArrayList interpreter = new ArrayList<>(); + Os os = SystemUtilities.getOs(); + switch(installer) { + case EXE: + if(!SystemUtilities.isWindows()) { + interpreter.add("wine"); + } + // Executable on its own + interpreter.add(payload.toString()); + interpreter.addAll(args); // Assume exe args come after payload + break; + case MSI: + interpreter.add(os == Os.WINDOWS ? "msiexec.exe" : "msiexec"); + interpreter.add("/i"); // Assume standard install + interpreter.add(payload.toString()); + interpreter.addAll(args); // Assume msiexec args come after payload + break; + case PKG: + if(os == Os.MAC) { + interpreter.add("installer"); + interpreter.addAll(args); // Assume installer args come before payload + interpreter.add("-package"); + interpreter.add(payload.toString()); + interpreter.add("-target"); + interpreter.add("/"); // Assume we don't want this on a removable volume + } else { + throw new UnsupportedOperationException("PKG is not yet supported on this platform"); + } + break; + case DMG: + // DMG requires "hdiutil attach", but the mount point is unknown + throw new UnsupportedOperationException("DMG is not yet supported"); + case RUN: + if(SystemUtilities.isWindows()) { + interpreter.add("bash"); + interpreter.add("-c"); + } + interpreter.add(payload.toString()); + interpreter.addAll(args); // Assume run args come after payload + // Executable on its own + break; + default: + // We'll try to parse it from the shebang just before invocation time + } + return interpreter; + } + +} diff --git a/old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java b/old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java new file mode 100755 index 0000000..e76d2cc --- /dev/null +++ b/old code/tray/src/qz/installer/shortcut/LinuxShortcutCreator.java @@ -0,0 +1,44 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.shortcut; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.installer.LinuxInstaller; + +/** + * @author Tres Finocchiaro + */ +class LinuxShortcutCreator extends ShortcutCreator { + + private static final Logger log = LogManager.getLogger(LinuxShortcutCreator.class); + private static String DESKTOP = System.getProperty("user.home") + "/Desktop/"; + + public boolean canAutoStart() { + return Files.exists(Paths.get(LinuxInstaller.STARTUP_DIR, LinuxInstaller.SHORTCUT_NAME)); + } + public void createDesktopShortcut() { + copyShortcut(LinuxInstaller.APP_LAUNCHER, DESKTOP); + } + + private static void copyShortcut(String source, String target) { + try { + Files.copy(Paths.get(source), Paths.get(target)); + } catch(IOException e) { + log.warn("Error creating shortcut {}", target, e); + } + } +} + diff --git a/old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java b/old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java new file mode 100755 index 0000000..582e582 --- /dev/null +++ b/old code/tray/src/qz/installer/shortcut/MacShortcutCreator.java @@ -0,0 +1,100 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.installer.shortcut; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; +import qz.common.Constants; +import qz.utils.MacUtilities; +import qz.utils.SystemUtilities; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * @author Tres Finocchiaro + */ +class MacShortcutCreator extends ShortcutCreator { + + private static final Logger log = LogManager.getLogger(MacShortcutCreator.class); + private static String SHORTCUT_PATH = System.getProperty("user.home") + "/Desktop/" + Constants.ABOUT_TITLE; + + /** + * Verify LaunchAgents plist file exists and parse it to verify it's enabled + */ + @Override + public boolean canAutoStart() { + // plist is stored as io.qz.plist + Path plistPath = Paths.get("/Library/LaunchAgents", MacUtilities.getBundleId() + ".plist"); + + if (Files.exists(plistPath)) { + try { + DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); + DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); + Document doc = dBuilder.parse(plistPath.toFile()); + doc.getDocumentElement().normalize(); + + NodeList dictList = doc.getElementsByTagName("dict"); + + // Loop to find "RunAtLoad" key, then the adjacent key + boolean foundItem = false; + if (dictList.getLength() > 0) { + NodeList children = dictList.item(0).getChildNodes(); + for(int n = 0; n < children.getLength(); n++) { + Node item = children.item(n); + // Apple stores booleans as adjacent tags to their owner + if (foundItem) { + String nodeName = children.item(n).getNodeName(); + log.debug("Found RunAtLoad value {}", nodeName); + return "true".equals(nodeName); + } + if (item.getNodeName().equals("key") && item.getTextContent().equals("RunAtLoad")) { + log.debug("Found RunAtLoad key in {}", plistPath); + foundItem = true; + } + } + } + log.warn("RunAtLoad was not in plist {}, autostart will not work.", plistPath); + } + catch(SAXException | IOException | ParserConfigurationException e) { + log.warn("Error reading plist {}, autostart will not work.", plistPath, e); + } + } else { + log.warn("No plist {} found, autostart will not work", plistPath); + } + return false; + } + + public void createDesktopShortcut() { + try { + new File(SHORTCUT_PATH).delete(); + if(SystemUtilities.getJarParentPath().endsWith("Contents")) { + // We're probably running from an .app bundle + Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getAppPath()); + } else { + // We're running from a mystery location, use the jar instead + Files.createSymbolicLink(Paths.get(SHORTCUT_PATH), SystemUtilities.getJarPath()); + } + + } catch(IOException e) { + log.warn("Could not create desktop shortcut {}", SHORTCUT_PATH, e); + } + } +} diff --git a/old code/tray/src/qz/installer/shortcut/ShortcutCreator.java b/old code/tray/src/qz/installer/shortcut/ShortcutCreator.java new file mode 100755 index 0000000..0572bef --- /dev/null +++ b/old code/tray/src/qz/installer/shortcut/ShortcutCreator.java @@ -0,0 +1,41 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.installer.shortcut; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.SystemUtilities; + +/** + * Utility class for creating, querying and removing startup shortcuts and + * desktop shortcuts. + * + * @author Tres Finocchiaro + */ +public abstract class ShortcutCreator { + private static ShortcutCreator instance; + protected static final Logger log = LogManager.getLogger(ShortcutCreator.class); + public abstract boolean canAutoStart(); + public abstract void createDesktopShortcut(); + + public static ShortcutCreator getInstance() { + if (instance == null) { + if (SystemUtilities.isWindows()) { + instance = new WindowsShortcutCreator(); + } else if (SystemUtilities.isMac()) { + instance = new MacShortcutCreator(); + } else { + instance = new LinuxShortcutCreator(); + } + } + return instance; + } +} diff --git a/old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java b/old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java new file mode 100755 index 0000000..504045e --- /dev/null +++ b/old code/tray/src/qz/installer/shortcut/WindowsShortcutCreator.java @@ -0,0 +1,60 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + * + */ + +package qz.installer.shortcut; + +import com.sun.jna.platform.win32.Win32Exception; +import mslinks.ShellLinkException; +import mslinks.ShellLinkHelper; +import qz.common.Constants; +import qz.installer.WindowsSpecialFolders; +import qz.utils.SystemUtilities; + +import java.io.File; +import java.io.IOException; +import java.nio.file.*; + +/** + * @author Tres Finocchiaro + */ +public class WindowsShortcutCreator extends ShortcutCreator { + private static String SHORTCUT_NAME = Constants.ABOUT_TITLE + ".lnk"; + + public void createDesktopShortcut() { + createShortcut(WindowsSpecialFolders.DESKTOP.toString()); + } + + public boolean canAutoStart() { + try { + return Files.exists(Paths.get(WindowsSpecialFolders.COMMON_STARTUP.toString(), SHORTCUT_NAME)); + } catch(Win32Exception e) { + log.warn("An exception occurred locating the startup folder; autostart cannot be determined.", e); + } + return false; + } + + private void createShortcut(String folderPath) { + try { + ShellLinkHelper.createLink(getAppPath(), folderPath + File.separator + SHORTCUT_NAME); + } + catch(ShellLinkException | IOException ex) { + log.warn("Error creating desktop shortcut", ex); + } + } + + /** + * Calculates .exe path from .jar + * fixme: overlaps SystemUtilities.getAppPath + */ + private static String getAppPath() { + return SystemUtilities.getJarPath().toString().replaceAll(".jar$", ".exe"); + } +} diff --git a/old code/tray/src/qz/printer/PrintOptions.java b/old code/tray/src/qz/printer/PrintOptions.java new file mode 100755 index 0000000..1a34bfe --- /dev/null +++ b/old code/tray/src/qz/printer/PrintOptions.java @@ -0,0 +1,752 @@ +package qz.printer; + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.LoggerUtilities; +import qz.utils.PrintingUtilities; +import qz.utils.SystemUtilities; + +import javax.print.attribute.ResolutionSyntax; +import javax.print.attribute.Size2DSyntax; +import javax.print.attribute.standard.Chromaticity; +import javax.print.attribute.standard.OrientationRequested; +import javax.print.attribute.standard.PrinterResolution; +import javax.print.attribute.standard.Sides; +import java.awt.*; +import java.awt.print.PageFormat; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class PrintOptions { + + private static final Logger log = LogManager.getLogger(PrintOptions.class); + + private Pixel psOptions = new Pixel(); + private Raw rawOptions = new Raw(); + private Default defOptions = new Default(); + + + /** + * Parses the provided JSON Object into relevant Pixel and Raw options + */ + public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities.Format format) { + if (configOpts == null) { return; } + + //check for raw options + if (!configOpts.isNull("forceRaw")) { + rawOptions.forceRaw = configOpts.optBoolean("forceRaw", false); + } else if (!configOpts.isNull("altPrinting")) { + log.warn("Raw option \"altPrinting\" is deprecated. Please use \"forceRaw\" instead."); + rawOptions.forceRaw = configOpts.optBoolean("altPrinting", false); + } + if (rawOptions.forceRaw && SystemUtilities.isWindows()) { + log.warn("Forced raw printing is not supported on Windows"); + rawOptions.forceRaw = false; + } + + if (!configOpts.isNull("encoding")) { + JSONObject encodings = configOpts.optJSONObject("encoding"); + if (encodings != null) { + rawOptions.srcEncoding = encodings.optString("from", null); + rawOptions.destEncoding = encodings.optString("to", null); + } else { + rawOptions.destEncoding = configOpts.optString("encoding", null); + } + } + if (!configOpts.isNull("spool")) { + JSONObject spool = configOpts.optJSONObject("spool"); + if (spool != null) { + if (!spool.isNull("size")) { + try { rawOptions.spoolSize = spool.getInt("size"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "spool.size", spool.opt("size")); } + } + // TODO: Implement spool.start + if (!spool.isNull("end")) { + rawOptions.spoolEnd = spool.optString("end"); + } + + } else { + LoggerUtilities.optionWarn(log, "JSONObject", "spool", configOpts.opt("spool")); + } + } else { + // Deprecated + if (!configOpts.isNull("perSpool")) { + try { rawOptions.spoolSize = configOpts.getInt("perSpool"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "perSpool", configOpts.opt("perSpool")); } + } + if (!configOpts.isNull("endOfDoc")) { + rawOptions.spoolEnd = configOpts.optString("endOfDoc", null); + } + } + if (!configOpts.isNull("copies")) { + try { rawOptions.copies = configOpts.getInt("copies"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "copies", configOpts.opt("copies")); } + } + if (!configOpts.isNull("jobName")) { + rawOptions.jobName = configOpts.optString("jobName", null); + } + if (!configOpts.isNull("retainTemp")) { + rawOptions.retainTemp = configOpts.optBoolean("retainTemp", false); + } + + + //check for pixel options + if (!configOpts.isNull("units")) { + switch(configOpts.optString("units")) { + case "mm": + psOptions.units = Unit.MM; break; + case "cm": + psOptions.units = Unit.CM; break; + case "in": + psOptions.units = Unit.INCH; break; + default: + LoggerUtilities.optionWarn(log, "valid value", "units", configOpts.opt("units")); break; + } + } + if (!configOpts.isNull("bounds")) { + try { + JSONObject bounds = configOpts.getJSONObject("bounds"); + psOptions.bounds = new Bounds(bounds.optDouble("x", 0), bounds.optDouble("y", 0), bounds.optDouble("width", 0), bounds.optDouble("height", 0)); + } + catch(JSONException e) { + LoggerUtilities.optionWarn(log, "JSONObject", "bounds", configOpts.opt("bounds")); + } + } + if (!configOpts.isNull("colorType")) { + try { + psOptions.colorType = ColorType.valueOf(configOpts.optString("colorType").toUpperCase(Locale.ENGLISH)); + } + catch(IllegalArgumentException e) { + LoggerUtilities.optionWarn(log, "valid value", "colorType", configOpts.opt("colorType")); + } + } + if (!configOpts.isNull("copies")) { + try { psOptions.copies = configOpts.getInt("copies"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "copies", configOpts.opt("copies")); } + if (psOptions.copies < 1) { + log.warn("Cannot have less than one copy"); + psOptions.copies = 1; + } + } + if (!configOpts.isNull("density")) { + JSONObject asymmDPI = configOpts.optJSONObject("density"); + if (asymmDPI != null) { + psOptions.density = asymmDPI.optInt("feed"); + psOptions.crossDensity = asymmDPI.optInt("cross"); + } else { + List rSupport = output.isSetService()? + output.getNativePrinter().getResolutions():new ArrayList<>(); + + JSONArray possibleDPIs = configOpts.optJSONArray("density"); + if (possibleDPIs != null && possibleDPIs.length() > 0) { + PrinterResolution usableRes = null; + + if (!rSupport.isEmpty()) { + for(int i = 0; i < possibleDPIs.length(); i++) { + PrinterResolution compareRes; + asymmDPI = possibleDPIs.optJSONObject(i); + if (asymmDPI != null) { + compareRes = new PrinterResolution(asymmDPI.optInt("cross"), asymmDPI.optInt("feed"), psOptions.units.resSyntax); + } else { + compareRes = new PrinterResolution(possibleDPIs.optInt(i), possibleDPIs.optInt(i), psOptions.units.resSyntax); + } + + if (rSupport.contains(compareRes)) { + usableRes = compareRes; + break; + } + } + } + + if (usableRes == null) { + log.warn("Supported printer densities not found, using first value provided"); + asymmDPI = possibleDPIs.optJSONObject(0); + if (asymmDPI != null) { + psOptions.density = asymmDPI.optInt("feed"); + psOptions.crossDensity = asymmDPI.optInt("cross"); + } else { + psOptions.density = possibleDPIs.optInt(0); + } + } else { + psOptions.density = usableRes.getFeedResolution(psOptions.units.resSyntax); + psOptions.crossDensity = usableRes.getCrossFeedResolution(psOptions.units.resSyntax); + } + } else { + String relDPI = configOpts.optString("density", "").toLowerCase(Locale.ENGLISH); + if ("best".equals(relDPI)) { + PrinterResolution bestRes = null; + for(PrinterResolution pr : rSupport) { + if (bestRes == null || !pr.lessThanOrEquals(bestRes)) { + bestRes = pr; + } + } + if (bestRes != null) { + psOptions.density = bestRes.getFeedResolution(psOptions.units.resSyntax); + psOptions.crossDensity = bestRes.getCrossFeedResolution(psOptions.units.resSyntax); + } else { + log.warn("No print densities were found; density: \"{}\" is being ignored", relDPI); + } + } else if ("draft".equals(relDPI)) { + PrinterResolution lowestRes = null; + for(PrinterResolution pr : rSupport) { + if (lowestRes == null || pr.lessThanOrEquals(lowestRes)) { + lowestRes = pr; + } + } + if (lowestRes != null) { + psOptions.density = lowestRes.getFeedResolution(psOptions.units.resSyntax); + psOptions.crossDensity = lowestRes.getCrossFeedResolution(psOptions.units.resSyntax); + } else { + log.warn("No print densities were found; density: \"{}\" is being ignored", relDPI); + } + } else { + try { psOptions.density = configOpts.getDouble("density"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "density", configOpts.opt("density")); } + } + } + } + } + if (!configOpts.isNull("dithering")) { + try { + if (configOpts.getBoolean("dithering")) { + psOptions.dithering = RenderingHints.VALUE_DITHER_ENABLE; + } else { + psOptions.dithering = RenderingHints.VALUE_DITHER_DISABLE; + } + } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "dithering", configOpts.opt("dithering")); } + } + if (!configOpts.isNull("duplex")) { + try { + if (configOpts.getBoolean("duplex")) { + psOptions.duplex = Sides.DUPLEX; + } + } + catch(JSONException e) { + //not a boolean, try as a string + try { + String duplex = configOpts.getString("duplex").toLowerCase(Locale.ENGLISH); + if (duplex.matches("^(duplex|(two.sided.)?long(.edge)?)$")) { + psOptions.duplex = Sides.DUPLEX; + } else if (duplex.matches("^(tumble|(two.sided.)?short(.edge)?)$")) { + psOptions.duplex = Sides.TUMBLE; + } + //else - one sided (default) + } + catch(JSONException e2) { LoggerUtilities.optionWarn(log, "valid value", "duplex", configOpts.opt("duplex")); } + } + } + if (!configOpts.isNull("interpolation")) { + switch(configOpts.optString("interpolation")) { + case "bicubic": + psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; break; + case "bilinear": + psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_BILINEAR; break; + case "nearest-neighbor": + case "nearest": + psOptions.interpolation = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR; break; + default: + LoggerUtilities.optionWarn(log, "valid value", "interpolation", configOpts.opt("interpolation")); break; + } + } + if (!configOpts.isNull("jobName")) { + psOptions.jobName = configOpts.optString("jobName", null); + } + if (!configOpts.isNull("legacy")) { + psOptions.legacy = configOpts.optBoolean("legacy", false); + } + if (!configOpts.isNull("margins")) { + Margins m = new Margins(); + JSONObject subMargins = configOpts.optJSONObject("margins"); + if (subMargins != null) { + //each individually + if (!subMargins.isNull("top")) { + try { m.top = subMargins.getDouble("top"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.top", subMargins.opt("top")); } + } + if (!subMargins.isNull("right")) { + try { m.right = subMargins.getDouble("right"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.right", subMargins.opt("right")); } + } + if (!subMargins.isNull("bottom")) { + try { m.bottom = subMargins.getDouble("bottom"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.bottom", subMargins.opt("bottom")); } + } + if (!subMargins.isNull("left")) { + try { m.left = subMargins.getDouble("left"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins.left", subMargins.opt("left")); } + } + } else { + try { m.setAll(configOpts.getDouble("margins")); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "margins", configOpts.opt("margins")); } + } + + psOptions.margins = m; + } + if (!configOpts.isNull("orientation")) { + try { + psOptions.orientation = Orientation.valueOf(configOpts.optString("orientation").replaceAll("-", "_").toUpperCase(Locale.ENGLISH)); + } + catch(IllegalArgumentException e) { + LoggerUtilities.optionWarn(log, "valid value", "orientation", configOpts.opt("orientation")); + } + } + if (!configOpts.isNull("paperThickness")) { + try { psOptions.paperThickness = configOpts.getDouble("paperThickness"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "paperThickness", configOpts.opt("paperThickness")); } + } + if (!configOpts.isNull("spool")) { + JSONObject spool = configOpts.optJSONObject("spool"); + if (spool != null) { + if (!spool.isNull("size")) { + try { psOptions.spoolSize = spool.getInt("size"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "integer", "spool.size", spool.opt("size")); } + } + } else { + LoggerUtilities.optionWarn(log, "JSONObject", "spool", configOpts.opt("spool")); + } + } + if (!configOpts.isNull("printerTray")) { + psOptions.printerTray = configOpts.optString("printerTray", null); + // Guard empty string value; will break pattern matching + if(psOptions.printerTray != null && psOptions.printerTray.trim().equals("")) { + psOptions.printerTray = null; + } + } + if (!configOpts.isNull("rasterize")) { + try { psOptions.rasterize = configOpts.getBoolean("rasterize"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "rasterize", configOpts.opt("rasterize")); } + } + if (!configOpts.isNull("rotation")) { + try { psOptions.rotation = configOpts.getDouble("rotation"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "rotation", configOpts.opt("rotation")); } + } + if (!configOpts.isNull("scaleContent")) { + try { psOptions.scaleContent = configOpts.getBoolean("scaleContent"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "boolean", "scaleContent", configOpts.opt("scaleContent")); } + } + if (!configOpts.isNull("size")) { + Size s = new Size(); + JSONObject subSize = configOpts.optJSONObject("size"); + if (subSize != null) { + if (!subSize.isNull("width")) { + try { s.width = subSize.getDouble("width"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "size.width", subSize.opt("width")); } + } + if (!subSize.isNull("height")) { + try { s.height = subSize.getDouble("height"); } + catch(JSONException e) { LoggerUtilities.optionWarn(log, "double", "size.height", subSize.opt("height")); } + } + + if (s.height <= 0 && s.width <= 0) { + log.warn("Page size has been set without dimensions, using default"); + } else { + psOptions.size = s; + } + } else { + LoggerUtilities.optionWarn(log, "JSONObject", "size", configOpts.opt("size")); + } + } + + //grab any useful service defaults + PrinterResolution defaultRes = null; + if (output.isSetService()) { + defaultRes = output.getNativePrinter().getResolution().value(); + + if (defaultRes == null) { + //printer has no default resolution set, see if it is possible to pull anything + List rSupport = output.getNativePrinter().getResolutions(); + if (rSupport.size() > 0) { + defaultRes = rSupport.get(0); + log.warn("Default resolution for {} is missing, using fallback: {}", output.getNativePrinter().getName(), defaultRes); + } else { + log.warn("Default resolution for {} is missing, no fallback available.", output.getNativePrinter().getName()); + } + } + } + if (defaultRes != null) { + //convert dphi to unit-dependant density ourselves (to keep as double type) + defOptions.density = (double)defaultRes.getFeedResolution(1) / psOptions.getUnits().getDPIUnits(); + } else { + try { defOptions.density = configOpts.getDouble("fallbackDensity"); } + catch(JSONException e) { + LoggerUtilities.optionWarn(log, "double", "fallbackDensity", configOpts.opt("fallbackDensity")); + //manually convert default dphi to a density value based on units + defOptions.density = 60000d / psOptions.getUnits().getDPIUnits(); + } + } + if ((psOptions.isRasterize() || format == PrintingUtilities.Format.IMAGE) && psOptions.getDensity() <= 1) { + psOptions.density = defOptions.density; + psOptions.crossDensity = defOptions.density; + } + + if (output.isSetService()) { + try { + PrinterJob job = PrinterJob.getPrinterJob(); + job.setPrintService(output.getPrintService()); + PageFormat page = job.getPageFormat(null); + defOptions.pageSize = new Size(page.getWidth(), page.getHeight()); + } + catch(PrinterException e) { + log.warn("Unable to find the default paper size"); + } + } + } + + + public Raw getRawOptions() { + return rawOptions; + } + + public Pixel getPixelOptions() { + return psOptions; + } + + public Default getDefaultOptions() { return defOptions; } + + + // Option groups // + + /** Raw printing options */ + public class Raw { + private boolean forceRaw = false; //Alternate printing for linux systems + private String destEncoding = null; //Text encoding / charset + private String srcEncoding = null; //Conversion text encoding + private String spoolEnd = null; //End of document character(s) + private int spoolSize = 1; //Pages per spool + private int copies = 1; //Job copies + private String jobName = null; //Job name + private boolean retainTemp = false; //Retain any temporary files + + + public boolean isForceRaw() { + return forceRaw; + } + + public String getDestEncoding() { + return destEncoding; + } + + public String getSrcEncoding() { + return srcEncoding; + } + + public String getSpoolEnd() { + return spoolEnd; + } + + public int getSpoolSize() { + return spoolSize; + } + + public int getCopies() { + return copies; + } + + public boolean isRetainTemp() { return retainTemp; } + + public String getJobName(String defaultVal) { + return jobName == null || jobName.isEmpty()? defaultVal:jobName; + } + } + + /** Pixel printing options */ + public class Pixel { + private Bounds bounds = null; //Bounding box rectangle + private ColorType colorType = ColorType.COLOR; //Color / black&white + private int copies = 1; //Job copies + private double crossDensity = 0; //Cross feed density + private double density = 0; //Pixel density (DPI or DPMM), feed density if crossDensity is defined + private Object dithering = RenderingHints.VALUE_DITHER_DEFAULT; //Image dithering + private Sides duplex = Sides.ONE_SIDED; //Multi-siding + private Object interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; //Image interpolation + private String jobName = null; //Job name + private boolean legacy = false; //Legacy printing + private Margins margins = new Margins(); //Page margins + private Orientation orientation = null; //Page orientation + private double paperThickness = -1; //Paper thickness + private int spoolSize = 0; //Pages before sending to printer + private String printerTray = null; //Printer tray to use + private boolean rasterize = true; //Whether documents are rasterized before printing + private double rotation = 0; //Image rotation + private boolean scaleContent = true; //Adjust paper size for best image fit + private Size size = null; //Paper size + private Unit units = Unit.INCH; //Units for density, margins, size + + + public Bounds getBounds() { + return bounds; + } + + public ColorType getColorType() { + return colorType; + } + + public int getCopies() { + return copies; + } + + public double getCrossDensity() { + return crossDensity; + } + + public double getDensity() { + return density; + } + + public Object getDithering() { + return dithering; + } + + public Sides getDuplex() { + return duplex; + } + + public Object getInterpolation() { + return interpolation; + } + + public String getJobName(String defaultVal) { + return jobName == null || jobName.isEmpty()? defaultVal:jobName; + } + + public boolean isLegacy() { + return legacy; + } + + public Margins getMargins() { + return margins; + } + + public Orientation getOrientation() { + return orientation; + } + + public double getPaperThickness() { + return paperThickness; + } + + public int getSpoolSize() { + return spoolSize; + } + + public String getPrinterTray() { + return printerTray; + } + + public boolean isRasterize() { + return rasterize; + } + + public double getRotation() { + return rotation; + } + + public boolean isScaleContent() { + return scaleContent; + } + + public Size getSize() { + return size; + } + + public Unit getUnits() { + return units; + } + } + + /** PrintService Defaults **/ + public class Default { + private double density; + private Size pageSize; + + + public double getDensity() { + return density; + } + + public Size getPageSize() { + return pageSize; + } + } + + // Sub options // + + /** Pixel page size options */ + public class Size { + private double width = -1; //Page width + private double height = -1; //Page height + + + public Size() {} + + public Size(double width, double height) { + this.width = width; + this.height = height; + } + + public double getWidth() { + return width; + } + + public double getHeight() { + return height; + } + } + + /** Pixel page margins options */ + public class Margins { + private double top = 0; //Top page margin + private double right = 0; //Right page margin + private double bottom = 0; //Bottom page margin + private double left = 0; //Left page margin + + private void setAll(double margin) { + top = margin; + right = margin; + bottom = margin; + left = margin; + } + + + public double top() { + return top; + } + + public double right() { + return right; + } + + public double bottom() { + return bottom; + } + + public double left() { + return left; + } + } + + /* Bounding box generic rectangle */ + public class Bounds { + private double x; + private double y; + private double width; + private double height; + + public Bounds(double x, double y, double width, double height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + public double getX() { + return x; + } + + public double getY() { + return y; + } + + public double getWidth() { + return width; + } + + public double getHeight() { + return height; + } + } + + /** Pixel dimension values */ + public enum Unit { + INCH(ResolutionSyntax.DPI, 1.0f, 1.0f, Size2DSyntax.INCH), //1in = 1in + CM(ResolutionSyntax.DPCM, .3937f, 2.54f, 10000), //1cm = .3937in ; 1in = 2.54cm + MM(ResolutionSyntax.DPCM * 10, .03937f, 25.4f, Size2DSyntax.MM); //1mm = .03937in ; 1in = 25.4mm + + private final float fromInch; + private final float toInch; //multiplicand to convert to inches + private final int resSyntax; + private final int µm; + + Unit(int resSyntax, float toIN, float fromIN, int µm) { + toInch = toIN; + fromInch = fromIN; + this.resSyntax = resSyntax; + this.µm = µm; + } + + public float toInches() { + return toInch; + } + + public float as1Inch() { + return fromInch; + } + + public int getDPIUnits() { + return resSyntax; + } + + public int getMediaSizeUnits() { + return µm; + } + } + + /** Pixel page orientation option */ + public enum Orientation { + PORTRAIT(OrientationRequested.PORTRAIT, PageFormat.PORTRAIT, 0), + REVERSE_PORTRAIT(OrientationRequested.PORTRAIT, PageFormat.PORTRAIT, 180), + LANDSCAPE(OrientationRequested.LANDSCAPE, PageFormat.LANDSCAPE, 270), + REVERSE_LANDSCAPE(OrientationRequested.REVERSE_LANDSCAPE, PageFormat.REVERSE_LANDSCAPE, 90); + + private final OrientationRequested orientationRequested; + private final int orientationFormat; + private final int degreesRot; + + Orientation(OrientationRequested orientationRequested, int orientationFormat, int degreesRot) { + this.orientationRequested = orientationRequested; + this.orientationFormat = orientationFormat; + this.degreesRot = degreesRot; + } + + + public OrientationRequested getAsOrientRequested() { + return orientationRequested; + } + + public int getAsOrientFormat() { + return orientationFormat; + } + + public int getDegreesRot() { + return degreesRot; + } + } + + /** Pixel page color option */ + public enum ColorType { + COLOR(Chromaticity.COLOR), + GREYSCALE(Chromaticity.MONOCHROME), + GRAYSCALE(Chromaticity.MONOCHROME), + BLACKWHITE(Chromaticity.MONOCHROME), + DEFAULT(null); + + private final Chromaticity chromatic; + + ColorType(Chromaticity chromatic) { + this.chromatic = chromatic; + } + + + public Chromaticity getAsChromaticity() { + return chromatic; + } + } + +} diff --git a/old code/tray/src/qz/printer/PrintOutput.java b/old code/tray/src/qz/printer/PrintOutput.java new file mode 100755 index 0000000..50b4645 --- /dev/null +++ b/old code/tray/src/qz/printer/PrintOutput.java @@ -0,0 +1,92 @@ +package qz.printer; + +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import qz.printer.info.NativePrinter; +import qz.utils.FileUtilities; + +import javax.print.PrintService; +import javax.print.attribute.standard.Media; +import java.io.File; +import java.nio.file.Paths; + +public class PrintOutput { + + private NativePrinter printer = null; + + private File file = null; + + private String host = null; + private int port = -1; + + + public PrintOutput(JSONObject configPrinter) throws JSONException, IllegalArgumentException { + if (configPrinter == null) { return; } + + if (configPrinter.has("name")) { + printer = PrintServiceMatcher.matchPrinter(configPrinter.getString("name")); + if (printer == null) { + throw new IllegalArgumentException("Cannot find printer with name \"" + configPrinter.getString("name") + "\""); + } + } + + if (configPrinter.has("file")) { + String filename = configPrinter.getString("file"); + if (!FileUtilities.isGoodExtension(Paths.get(filename))) { + throw new IllegalArgumentException("Writing to file \"" + filename + "\" is denied for security reasons. (Prohibited file extension)"); + } else if (FileUtilities.isBadPath(filename)) { + throw new IllegalArgumentException("Writing to file \"" + filename + "\" is denied for security reasons. (Prohibited directory name)"); + } else { + file = new File(filename); + } + } + + if (configPrinter.has("host")) { + host = configPrinter.getString("host"); + port = configPrinter.optInt("port", 9100); // default to port 9100 (HP/JetDirect standard) if not provided + } + + //at least one method must be set for printing + if (!isSetService() && !isSetFile() && !isSetHost()) { + throw new IllegalArgumentException("No printer output has been specified"); + } + } + + + public boolean isSetService() { + return printer != null && printer.getPrintService() != null && !printer.getPrintService().isNull(); + } + + public PrintService getPrintService() { + return printer.getPrintService().value(); + } + + public NativePrinter getNativePrinter() { + return printer; + } + + public boolean isSetFile() { + return file != null; + } + + public File getFile() { + return file; + } + + public boolean isSetHost() { + return host != null; + } + + public String getHost() { + return host; + } + + public int getPort() { + return port; + } + + public Media[] getSupportedMedia() { + return (Media[])getPrintService().getSupportedAttributeValues(Media.class, null, null); + } + +} diff --git a/old code/tray/src/qz/printer/PrintServiceMatcher.java b/old code/tray/src/qz/printer/PrintServiceMatcher.java new file mode 100755 index 0000000..6c99f47 --- /dev/null +++ b/old code/tray/src/qz/printer/PrintServiceMatcher.java @@ -0,0 +1,242 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.printer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import qz.printer.info.CachedPrintServiceLookup; +import qz.printer.info.NativePrinter; +import qz.printer.info.NativePrinterMap; +import qz.utils.SystemUtilities; + +import javax.print.PrintService; +import javax.print.PrintServiceLookup; +import javax.print.attribute.ResolutionSyntax; +import javax.print.attribute.standard.*; +import java.util.*; + +public class PrintServiceMatcher { + private static final Logger log = LogManager.getLogger(PrintServiceMatcher.class); + + // PrintService is slow in CUPS, use a cache instead per JDK-7001133 + // TODO: Include JDK version test for caching when JDK-7001133 is fixed upstream + private static final boolean useCache = SystemUtilities.isUnix(); + + public static NativePrinterMap getNativePrinterList(boolean silent, boolean withAttributes) { + NativePrinterMap printers = NativePrinterMap.getInstance(); + printers.putAll(true, lookupPrintServices()); + if (withAttributes) { printers.values().forEach(NativePrinter::getDriverAttributes); } + if (!silent) { log.debug("Found {} printers", printers.size()); } + return printers; + } + + private static PrintService[] lookupPrintServices() { + return useCache ? CachedPrintServiceLookup.lookupPrintServices() : + PrintServiceLookup.lookupPrintServices(null, null); + } + + private static PrintService lookupDefaultPrintService() { + return useCache ? CachedPrintServiceLookup.lookupDefaultPrintService() : + PrintServiceLookup.lookupDefaultPrintService(); + } + + public static NativePrinterMap getNativePrinterList(boolean silent) { + return getNativePrinterList(silent, false); + } + + public static NativePrinterMap getNativePrinterList() { + return getNativePrinterList(false); + } + + public static NativePrinter getDefaultPrinter() { + PrintService defaultService = lookupDefaultPrintService(); + + if(defaultService == null) { + return null; + } + + NativePrinterMap printers = NativePrinterMap.getInstance(); + if (!printers.contains(defaultService)) { + printers.putAll(false, defaultService); + } + + return printers.get(defaultService); + } + + public static String findPrinterName(String query) throws JSONException { + NativePrinter printer = PrintServiceMatcher.matchPrinter(query); + + if (printer != null) { + return printer.getPrintService().value().getName(); + } else { + return null; + } + } + + /** + * Finds {@code PrintService} by looking at any matches to {@code printerSearch}. + * + * @param printerSearch Search query to compare against service names. + */ + public static NativePrinter matchPrinter(String printerSearch, boolean silent) { + NativePrinter exact = null; + NativePrinter begins = null; + NativePrinter partial = null; + + if (!silent) { log.debug("Searching for PrintService matching {}", printerSearch); } + + // Fix for https://github.com/qzind/tray/issues/931 + // This is more than an optimization, removal will lead to a regression + NativePrinter defaultPrinter = getDefaultPrinter(); + if (defaultPrinter != null && printerSearch.equals(defaultPrinter.getName())) { + if (!silent) { log.debug("Matched default printer, skipping further search"); } + return defaultPrinter; + } + + printerSearch = printerSearch.toLowerCase(Locale.ENGLISH); + + // Search services for matches + for(NativePrinter printer : getNativePrinterList(silent).values()) { + if (printer.getName() == null) { + continue; + } + String printerName = printer.getName().toLowerCase(Locale.ENGLISH); + if (printerName.equals(printerSearch)) { + exact = printer; + break; + } + if (printerName.startsWith(printerSearch)) { + begins = printer; + continue; + } + if (printerName.contains(printerSearch)) { + partial = printer; + continue; + } + + if (SystemUtilities.isMac()) { + // 1.9 compat: fallback for old style names + PrinterName name = printer.getLegacyName(); + if (name == null || name.getValue() == null) { continue; } + printerName = name.getValue().toLowerCase(Locale.ENGLISH); + if (printerName.equals(printerSearch)) { + exact = printer; + continue; + } + if (printerName.startsWith(printerSearch)) { + begins = printer; + continue; + } + if (printerName.contains(printerSearch)) { + partial = printer; + } + } + } + + // Return closest match + NativePrinter use = null; + if (exact != null) { + use = exact; + } else if (begins != null) { + use = begins; + } else if (partial != null) { + use = partial; + } + + if (use != null) { + if(!silent) log.debug("Found match: {}", use.getPrintService().value().getName()); + } else { + log.warn("Printer not found: {}", printerSearch); + } + + return use; + } + + public static NativePrinter matchPrinter(String printerSearch) { + return matchPrinter(printerSearch, false); + } + + public static JSONArray getPrintersJSON(boolean includeDetails) throws JSONException { + JSONArray list = new JSONArray(); + + PrintService defaultService = lookupDefaultPrintService(); + + boolean mediaTrayCrawled = false; + + for(NativePrinter printer : getNativePrinterList().values()) { + PrintService ps = printer.getPrintService().value(); + JSONObject jsonService = new JSONObject(); + jsonService.put("name", ps.getName()); + + if (includeDetails) { + jsonService.put("driver", printer.getDriver().value()); + jsonService.put("connection", printer.getConnection()); + jsonService.put("default", ps == defaultService); + + if (!mediaTrayCrawled) { + log.info("Gathering printer MediaTray information..."); + mediaTrayCrawled = true; + } + + HashSet uniqueSizes = new HashSet<>(); // prevents duplicates + JSONArray trays = new JSONArray(); + JSONArray sizes = new JSONArray(); + + for(Media m : (Media[])ps.getSupportedAttributeValues(Media.class, null, null)) { + if (m instanceof MediaTray) { trays.put(m.toString()); } + if (m instanceof MediaSizeName) { + if(uniqueSizes.add(m.toString())) { + MediaSize mediaSize = MediaSize.getMediaSizeForName((MediaSizeName)m); + if(mediaSize == null) { + continue; + } + + JSONObject size = new JSONObject(); + size.put("name", m.toString()); + + JSONObject in = new JSONObject(); + in.put("width", mediaSize.getX(MediaPrintableArea.INCH)); + in.put("height", mediaSize.getY(MediaPrintableArea.INCH)); + size.put("in", in); + + JSONObject mm = new JSONObject(); + mm.put("width", mediaSize.getX(MediaPrintableArea.MM)); + mm.put("height", mediaSize.getY(MediaPrintableArea.MM)); + size.put("mm", mm); + + sizes.put(size); + } + + } + } + + if(trays.length() > 0) { + jsonService.put("trays", trays); + } + if(sizes.length() > 0) { + jsonService.put("sizes", sizes); + } + + PrinterResolution res = printer.getResolution().value(); + int density = -1; if (res != null) { density = res.getFeedResolution(ResolutionSyntax.DPI); } + jsonService.put("density", density); + } + + list.put(jsonService); + } + + return list; + } + +} diff --git a/old code/tray/src/qz/printer/action/PrintDirect.java b/old code/tray/src/qz/printer/action/PrintDirect.java new file mode 100755 index 0000000..aa21802 --- /dev/null +++ b/old code/tray/src/qz/printer/action/PrintDirect.java @@ -0,0 +1,97 @@ +package qz.printer.action; + +import org.apache.commons.codec.binary.Base64InputStream; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.printer.PrintOptions; +import qz.printer.PrintOutput; +import qz.utils.PrintingUtilities; + +import javax.print.DocFlavor; +import javax.print.DocPrintJob; +import javax.print.PrintException; +import javax.print.SimpleDoc; +import javax.print.attribute.HashPrintRequestAttributeSet; +import javax.print.attribute.PrintRequestAttributeSet; +import javax.print.attribute.standard.JobName; +import java.io.ByteArrayInputStream; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Locale; + +public class PrintDirect extends PrintRaw { + + private static final Logger log = LogManager.getLogger(PrintDirect.class); + + private ArrayList prints = new ArrayList<>(); + private ArrayList flavors = new ArrayList<>(); + + + @Override + public PrintingUtilities.Format getFormat() { + return PrintingUtilities.Format.DIRECT; + } + + @Override + public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException { + for(int i = 0; i < printData.length(); i++) { + JSONObject data = printData.optJSONObject(i); + if (data == null) { continue; } + + prints.add(data.getString("data")); + flavors.add(PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.PLAIN)); + } + } + + @Override + public void print(PrintOutput output, PrintOptions options) throws PrintException { + PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet(); + attributes.add(new JobName(options.getRawOptions().getJobName(Constants.RAW_PRINT), Locale.getDefault())); + + for(int i = 0; i < prints.size(); i++) { + DocPrintJob printJob = output.getPrintService().createPrintJob(); + InputStream stream = null; + + try { + switch(flavors.get(i)) { + case BASE64: + stream = new Base64InputStream(new ByteArrayInputStream(prints.get(i).getBytes("UTF-8"))); + break; + case FILE: + stream = new DataInputStream(new URL(prints.get(i)).openStream()); + break; + case PLAIN: + default: + stream = new ByteArrayInputStream(prints.get(i).getBytes("UTF-8")); + break; + } + + SimpleDoc doc = new SimpleDoc(stream, DocFlavor.INPUT_STREAM.AUTOSENSE, null); + + waitForPrint(printJob, doc, attributes); + } + catch(IOException e) { + throw new PrintException(e); + } + finally { + if (stream != null) { + try { stream.close(); } catch(Exception ignore) {} + } + } + } + } + + @Override + public void cleanup() { + prints.clear(); + flavors.clear(); + } + +} diff --git a/old code/tray/src/qz/printer/action/PrintHTML.java b/old code/tray/src/qz/printer/action/PrintHTML.java new file mode 100755 index 0000000..fafa6d3 --- /dev/null +++ b/old code/tray/src/qz/printer/action/PrintHTML.java @@ -0,0 +1,414 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.printer.action; + +import com.sun.javafx.print.PrintHelper; +import com.sun.javafx.print.Units; +import javafx.print.*; +import org.apache.commons.io.IOUtils; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.printer.PrintOptions; +import qz.printer.PrintOutput; +import qz.printer.action.html.WebApp; +import qz.printer.action.html.WebAppModel; +import qz.utils.PrintingUtilities; + +import javax.print.attribute.PrintRequestAttributeSet; +import javax.print.attribute.standard.Copies; +import javax.print.attribute.standard.CopiesSupported; +import javax.print.attribute.standard.Sides; +import javax.swing.*; +import java.awt.*; +import java.awt.print.PageFormat; +import java.awt.print.PrinterException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +public class PrintHTML extends PrintImage implements PrintProcessor { + + private static final Logger log = LogManager.getLogger(PrintHTML.class); + + private List models; + + private JLabel legacyLabel = null; + + + public PrintHTML() { + super(); + models = new ArrayList<>(); + } + + @Override + public PrintingUtilities.Format getFormat() { + return PrintingUtilities.Format.HTML; + } + + @Override + public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException { + try { + PrintOptions.Pixel pxlOpts = options.getPixelOptions(); + if (!pxlOpts.isLegacy()) { + WebApp.initialize(); + } + + for(int i = 0; i < printData.length(); i++) { + JSONObject data = printData.getJSONObject(i); + + PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE); + + String source; + switch(flavor) { + case FILE: + case PLAIN: + // We'll toggle between 'plain' and 'file' when we construct WebAppModel + source = data.getString("data"); + break; + default: + source = new String(flavor.read(data.getString("data")), StandardCharsets.UTF_8); + } + + double pageZoom = (pxlOpts.getDensity() * pxlOpts.getUnits().as1Inch()) / 72.0; + if (pageZoom <= 1) { pageZoom = 1; } + + double pageWidth = 0; + double pageHeight = 0; + double convertFactor = (72.0 / pxlOpts.getUnits().as1Inch()); + + boolean renderFromHeight = Arrays.asList(PrintOptions.Orientation.LANDSCAPE, + PrintOptions.Orientation.REVERSE_LANDSCAPE).contains(pxlOpts.getOrientation()); + + if (pxlOpts.getSize() != null) { + if (!renderFromHeight) { + pageWidth = pxlOpts.getSize().getWidth() * convertFactor; + } else { + pageWidth = pxlOpts.getSize().getHeight() * convertFactor; + } + } else if (options.getDefaultOptions().getPageSize() != null) { + if (!renderFromHeight) { + pageWidth = options.getDefaultOptions().getPageSize().getWidth(); + } else { + pageWidth = options.getDefaultOptions().getPageSize().getHeight(); + } + } + + if (pxlOpts.getMargins() != null) { + PrintOptions.Margins margins = pxlOpts.getMargins(); + if (!renderFromHeight || pxlOpts.isRasterize()) { + pageWidth -= (margins.left() + margins.right()) * convertFactor; + } else { + pageWidth -= (margins.top() + margins.bottom()) * convertFactor; //due to vector margin matching + } + } + + if (!data.isNull("options")) { + JSONObject dataOpt = data.getJSONObject("options"); + + if (!dataOpt.isNull("pageWidth") && dataOpt.optDouble("pageWidth") > 0) { + pageWidth = dataOpt.optDouble("pageWidth") * convertFactor; + } + if (!dataOpt.isNull("pageHeight") && dataOpt.optDouble("pageHeight") > 0) { + pageHeight = dataOpt.optDouble("pageHeight") * convertFactor; + } + } + + models.add(new WebAppModel(source, (flavor != PrintingUtilities.Flavor.FILE), pageWidth, pageHeight, pxlOpts.isScaleContent(), pageZoom)); + } + + log.debug("Parsed {} html records", models.size()); + } + catch(IOException e) { + throw new UnsupportedOperationException("Unable to start JavaFX service", e); + } + catch(NoClassDefFoundError e) { + throw new UnsupportedOperationException("JavaFX libraries not found", e); + } + } + + @Override + public void print(PrintOutput output, PrintOptions options) throws PrinterException { + if (options.getPixelOptions().isLegacy()) { + printLegacy(output, options); + } else if (options.getPixelOptions().isRasterize()) { + //grab a snapshot of the pages for PrintImage instead of printing directly + for(WebAppModel model : models) { + try { images.add(WebApp.raster(model)); } + catch(Throwable t) { + if (model.getZoom() > 1 && t instanceof IllegalArgumentException) { + //probably a unrecognized image loader error, try at default zoom + try { + log.warn("Capture failed with increased zoom, attempting with default value"); + model.setZoom(1); + images.add(WebApp.raster(model)); + } + catch(Throwable tt) { + throw new PrinterException(tt.getMessage()); + } + } else { + throw new PrinterException(t.getMessage()); + } + } + } + + super.print(output, options); + } else { + Printer fxPrinter = null; + for(Printer p : Printer.getAllPrinters()) { + if (p.getName().equals(output.getPrintService().getName())) { + fxPrinter = p; + break; + } + } + if (fxPrinter == null) { + throw new PrinterException("Cannot find printer under the JavaFX libraries"); + } + + PrinterJob job = PrinterJob.createPrinterJob(fxPrinter); + + + // apply option settings + PrintOptions.Pixel pxlOpts = options.getPixelOptions(); + JobSettings settings = job.getJobSettings(); + settings.setJobName(pxlOpts.getJobName(Constants.HTML_PRINT)); + settings.setPrintQuality(PrintQuality.HIGH); + + // If colortype is default, leave printColor blank. The system's printer settings will be used instead. + if (pxlOpts.getColorType() != PrintOptions.ColorType.DEFAULT) { + settings.setPrintColor(getColor(pxlOpts)); + } + if (pxlOpts.getDuplex() == Sides.DUPLEX || pxlOpts.getDuplex() == Sides.TWO_SIDED_LONG_EDGE) { + settings.setPrintSides(PrintSides.DUPLEX); + } + if (pxlOpts.getDuplex() == Sides.TUMBLE || pxlOpts.getDuplex() == Sides.TWO_SIDED_SHORT_EDGE) { + settings.setPrintSides(PrintSides.TUMBLE); + } + if (pxlOpts.getPrinterTray() != null) { + PaperSource tray = findFXTray(fxPrinter.getPrinterAttributes().getSupportedPaperSources(), pxlOpts.getPrinterTray()); + if (tray != null) { + settings.setPaperSource(tray); + } + } + + if (pxlOpts.getDensity() > 0) { + settings.setPrintResolution(PrintHelper.createPrintResolution((int)pxlOpts.getDensity(), (int)pxlOpts.getDensity())); + } + + Paper paper; + if (pxlOpts.getSize() != null && pxlOpts.getSize().getWidth() > 0 && pxlOpts.getSize().getHeight() > 0) { + double convert = 1; + Units units = getUnits(pxlOpts); + if (units == null) { + convert = 10; //need to adjust from cm to mm only for DPCM sizes + units = Units.MM; + } + paper = PrintHelper.createPaper("Custom", pxlOpts.getSize().getWidth() * convert, pxlOpts.getSize().getHeight() * convert, units); + } else { + PrintOptions.Size paperSize = options.getDefaultOptions().getPageSize(); + paper = PrintHelper.createPaper("Default", paperSize.getWidth(), paperSize.getHeight(), Units.POINT); + } + + PageOrientation orient = fxPrinter.getPrinterAttributes().getDefaultPageOrientation(); + if (pxlOpts.getOrientation() != null) { + orient = getOrientation(pxlOpts); + } + + try { + PageLayout layout; + PrintOptions.Margins m = pxlOpts.getMargins(); + if (m != null) { + //force access to the page layout constructor as the adjusted margins on small sizes are wildly inaccurate + Constructor plCon = PageLayout.class.getDeclaredConstructor(Paper.class, PageOrientation.class, double.class, double.class, double.class, double.class); + plCon.setAccessible(true); + + //margins defined as pnt (1/72nds) + double asPnt = pxlOpts.getUnits().toInches() * 72; + if (orient == PageOrientation.PORTRAIT || orient == PageOrientation.REVERSE_PORTRAIT) { + layout = plCon.newInstance(paper, orient, m.left() * asPnt, m.right() * asPnt, m.top() * asPnt, m.bottom() * asPnt); + } else { + //rotate margins to match raster prints + layout = plCon.newInstance(paper, orient, m.top() * asPnt, m.bottom() * asPnt, m.right() * asPnt, m.left() * asPnt); + } + } else { + //if margins are not provided, use default paper margins + PageLayout valid = fxPrinter.getDefaultPageLayout(); + layout = fxPrinter.createPageLayout(paper, orient, valid.getLeftMargin(), valid.getRightMargin(), valid.getTopMargin(), valid.getBottomMargin()); + } + + //force our layout as the default to avoid default-margin exceptions on small paper sizes + Field field = fxPrinter.getClass().getDeclaredField("defPageLayout"); + field.setAccessible(true); + field.set(fxPrinter, layout); + + settings.setPageLayout(layout); + } + catch(Exception e) { + log.error("Failed to set custom layout", e); + } + + settings.setCopies(pxlOpts.getCopies()); + log.trace("{}", settings.toString()); + + //javaFX lies about this value, so pull from original print service + CopiesSupported cSupport = (CopiesSupported)output.getPrintService() + .getSupportedAttributeValues(Copies.class, output.getPrintService().getSupportedDocFlavors()[0], null); + + try { + if (cSupport != null && cSupport.contains(pxlOpts.getCopies())) { + for(WebAppModel model : models) { + WebApp.print(job, model); + } + } else { + settings.setCopies(1); //manually handle copies if they are not supported + for(int i = 0; i < pxlOpts.getCopies(); i++) { + for(WebAppModel model : models) { + WebApp.print(job, model); + } + } + } + } + catch(Throwable t) { + job.cancelJob(); + throw new PrinterException(t.getMessage()); + } + + //send pending prints + job.endJob(); + } + } + + private void printLegacy(PrintOutput output, PrintOptions options) throws PrinterException { + PrintOptions.Pixel pxlOpts = options.getPixelOptions(); + + java.awt.print.PrinterJob job = java.awt.print.PrinterJob.getPrinterJob(); + job.setPrintService(output.getPrintService()); + PageFormat page = job.getPageFormat(null); + + PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, page, output.getSupportedMedia()); + + //setup swing ui + JFrame legacyFrame = new JFrame(pxlOpts.getJobName(Constants.HTML_PRINT)); + legacyFrame.setUndecorated(true); + legacyFrame.setLayout(new FlowLayout()); + legacyFrame.setExtendedState(Frame.ICONIFIED); + + legacyLabel = new JLabel(); + legacyLabel.setOpaque(true); + legacyLabel.setBackground(Color.WHITE); + legacyLabel.setBorder(null); + legacyLabel.setDoubleBuffered(false); + + legacyFrame.add(legacyLabel); + + try { + for(WebAppModel model : models) { + if (model.isPlainText()) { + legacyLabel.setText(cleanHtmlContent(model.getSource())); + } else { + try(InputStream fis = new URL(model.getSource()).openStream()) { + String webPage = cleanHtmlContent(IOUtils.toString(fis, "UTF-8")); + legacyLabel.setText(webPage); + } + } + + legacyFrame.pack(); + legacyFrame.setVisible(true); + + job.setPrintable(this); + printCopies(output, pxlOpts, job, attributes); + } + } + catch(Exception e) { + throw new PrinterException(e.getMessage()); + } + finally { + legacyFrame.dispose(); + } + } + + private String cleanHtmlContent(String html) { + return html.replaceAll("^[\\s\\S]*<(HTML|html)\\b.*?>", ""); + } + + @Override + public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException { + if (legacyLabel == null) { + return super.print(graphics, pageFormat, pageIndex); + } else { + if (graphics == null) { throw new PrinterException("No graphics specified"); } + if (pageFormat == null) { throw new PrinterException("No page format specified"); } + + if (pageIndex + 1 > models.size()) { + return NO_SUCH_PAGE; + } + log.trace("Requested page {} for printing", pageIndex); + + Graphics2D graphics2D = (Graphics2D)graphics; + graphics2D.setRenderingHints(buildRenderingHints(dithering, interpolation)); + graphics2D.translate(pageFormat.getImageableX(), pageFormat.getImageableY()); + graphics2D.scale(pageFormat.getImageableWidth() / pageFormat.getWidth(), pageFormat.getImageableHeight() / pageFormat.getHeight()); + legacyLabel.paint(graphics2D); + + return PAGE_EXISTS; + } + } + + @Override + public void cleanup() { + super.cleanup(); + + models.clear(); + legacyLabel = null; + } + + public static Units getUnits(PrintOptions.Pixel opts) { + switch(opts.getUnits()) { + case INCH: + return Units.INCH; + case MM: + return Units.MM; + default: + return null; + } + } + + public static PageOrientation getOrientation(PrintOptions.Pixel opts) { + switch(opts.getOrientation()) { + case LANDSCAPE: + return PageOrientation.LANDSCAPE; + case REVERSE_LANDSCAPE: + return PageOrientation.REVERSE_LANDSCAPE; + case REVERSE_PORTRAIT: + return PageOrientation.REVERSE_PORTRAIT; + default: + return PageOrientation.PORTRAIT; + } + } + + public static PrintColor getColor(PrintOptions.Pixel opts) { + switch(opts.getColorType()) { + case COLOR: + return PrintColor.COLOR; + default: + return PrintColor.MONOCHROME; + } + } +} diff --git a/old code/tray/src/qz/printer/action/PrintImage.java b/old code/tray/src/qz/printer/action/PrintImage.java new file mode 100755 index 0000000..957a556 --- /dev/null +++ b/old code/tray/src/qz/printer/action/PrintImage.java @@ -0,0 +1,341 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + * + */ +package qz.printer.action; + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.printer.PrintOptions; +import qz.printer.PrintOutput; +import qz.utils.ConnectionUtilities; +import qz.utils.PrintingUtilities; +import qz.utils.SystemUtilities; + +import javax.imageio.IIOException; +import javax.imageio.ImageIO; +import javax.print.attribute.PrintRequestAttributeSet; +import javax.print.attribute.ResolutionSyntax; +import javax.print.attribute.standard.OrientationRequested; +import javax.print.attribute.standard.PrinterResolution; +import java.awt.*; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + + +/** + * @author Tres Finocchiaro, Anton Mezerny + */ +public class PrintImage extends PrintPixel implements PrintProcessor, Printable { + + private static final Logger log = LogManager.getLogger(PrintImage.class); + + protected List images; + + protected double dpiScale = 1; + protected boolean scaleImage = false; + protected Object dithering = RenderingHints.VALUE_DITHER_DEFAULT; + protected Object interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; + protected double imageRotation = 0; + protected boolean manualReverse = false; + + public PrintImage() { + images = new ArrayList<>(); + } + + @Override + public PrintingUtilities.Format getFormat() { + return PrintingUtilities.Format.IMAGE; + } + + @Override + public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException { + dpiScale = (options.getPixelOptions().getDensity() * options.getPixelOptions().getUnits().as1Inch()) / 72.0; + + for(int i = 0; i < printData.length(); i++) { + JSONObject data = printData.getJSONObject(i); + + PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE); + + try { + BufferedImage bi; + switch(flavor) { + case PLAIN: + // There's really no such thing as a 'PLAIN' image, assume it's a URL + case FILE: + bi = ImageIO.read(ConnectionUtilities.getInputStream(data.getString("data"), true)); + break; + default: + bi = ImageIO.read(new ByteArrayInputStream(flavor.read(data.getString("data")))); + } + + images.add(bi); + } + catch(IIOException e) { + if (e.getCause() != null && e.getCause() instanceof FileNotFoundException) { + throw new UnsupportedOperationException("Image file specified could not be found.", e); + } else { + throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as an image", flavor, data.getString("data")), e); + } + } + catch(IOException e) { + throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as an image: %s", flavor, data.getString("data"), e.getLocalizedMessage()), e); + } + } + + log.debug("Parsed {} images for printing", images.size()); + } + + private List breakupOverPages(BufferedImage img, PageFormat page, PrintRequestAttributeSet attributes) { + List splits = new ArrayList<>(); + + Rectangle printBounds = new Rectangle(0, 0, (int)page.getImageableWidth(), (int)page.getImageableHeight()); + PrinterResolution res = (PrinterResolution)attributes.get(PrinterResolution.class); + float dpi = res.getFeedResolution(1) / (float)ResolutionSyntax.DPI; + float cdpi = res.getCrossFeedResolution(1) / (float)ResolutionSyntax.DPI; + + //printing uses 72dpi, convert so we can check split size correctly + int useWidth = (int)((img.getWidth() / cdpi) * 72); + int useHeight = (int)((img.getHeight() / dpi) * 72); + + int columnsNeed = (int)Math.ceil(useWidth / page.getImageableWidth()); + int rowsNeed = (int)Math.ceil(useHeight / page.getImageableHeight()); + + if (columnsNeed == 1 && rowsNeed == 1) { + log.trace("Unscaled image does not need spit"); + splits.add(img); + } else { + log.trace("Image to be printed across {} pages", columnsNeed * rowsNeed); + //allows us to split the image at the actual dpi instead of 72 + float upscale = dpi / 72f; + float c_upscale = cdpi / 72f; + + for(int row = 0; row < rowsNeed; row++) { + for(int col = 0; col < columnsNeed; col++) { + Rectangle clip = new Rectangle((col * (int)(printBounds.width * c_upscale)), (row * (int)(printBounds.height * upscale)), + (int)(printBounds.width * c_upscale), (int)(printBounds.height * upscale)); + + if (clip.x + clip.width > img.getWidth()) { clip.width = img.getWidth() - clip.x; } + if (clip.y + clip.height > img.getHeight()) { clip.height = img.getHeight() - clip.y; } + + splits.add(img.getSubimage(clip.x, clip.y, clip.width, clip.height)); + } + } + } + + return splits; + } + + @Override + public void print(PrintOutput output, PrintOptions options) throws PrinterException { + if (images.isEmpty()) { + log.warn("Nothing to print"); + return; + } + + PrinterJob job = PrinterJob.getPrinterJob(); + job.setPrintService(output.getPrintService()); + PageFormat page = job.getPageFormat(null); + + PrintOptions.Pixel pxlOpts = options.getPixelOptions(); + PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, page, output.getSupportedMedia()); + + scaleImage = pxlOpts.isScaleContent(); + dithering = pxlOpts.getDithering(); + interpolation = pxlOpts.getInterpolation(); + imageRotation = pxlOpts.getRotation(); + + //reverse fix for OSX + if (SystemUtilities.isMac() && pxlOpts.getOrientation() != null + && pxlOpts.getOrientation().getAsOrientRequested() == OrientationRequested.REVERSE_LANDSCAPE) { + imageRotation += 180; + manualReverse = true; + } + + if (!scaleImage) { + //breakup large images to print across pages as needed + List split = new ArrayList<>(); + for(BufferedImage bi : images) { + split.addAll(breakupOverPages(bi, page, attributes)); + } + images = split; + } + + job.setJobName(pxlOpts.getJobName(Constants.IMAGE_PRINT)); + job.setPrintable(this, job.validatePage(page)); + + printCopies(output, pxlOpts, job, attributes); + } + + + @Override + public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException { + if (graphics == null) { throw new PrinterException("No graphics specified"); } + if (pageFormat == null) { throw new PrinterException("No page format specified"); } + + if (pageIndex + 1 > images.size()) { + return NO_SUCH_PAGE; + } + log.trace("Requested page {} for printing", pageIndex); + + if ("sun.print.PeekGraphics".equals(graphics.getClass().getCanonicalName())) { + //java uses class only to query if a page needs printed - save memory/time by short circuiting + return PAGE_EXISTS; + } + + + //allows pages view to rotate in different orientations + graphics.drawString(" ", 0, 0); + + BufferedImage imgToPrint = fixColorModel(images.get(pageIndex)); + if (imageRotation % 360 != 0) { + imgToPrint = rotate(imgToPrint, imageRotation); + } + + // apply image scaling + double boundW = pageFormat.getImageableWidth(); + double boundH = pageFormat.getImageableHeight(); + + double imgW = imgToPrint.getWidth() / dpiScale; + double imgH = imgToPrint.getHeight() / dpiScale; + + if (scaleImage) { + imgToPrint = scale(imgToPrint, pageFormat); + + // adjust dimensions to smallest edge, keeping size ratio + if (((float)imgToPrint.getWidth() / (float)imgToPrint.getHeight()) >= (boundW / boundH)) { + imgW = boundW; + imgH = (imgToPrint.getHeight() / (imgToPrint.getWidth() / boundW)); + } else { + imgW = (imgToPrint.getWidth() / (imgToPrint.getHeight() / boundH)); + imgH = boundH; + } + } + + double boundX = pageFormat.getImageableX(); + double boundY = pageFormat.getImageableY(); + + log.debug("Paper area: {},{}:{},{}", (int)boundX, (int)boundY, (int)boundW, (int)boundH); + log.trace("Image size: {},{}", imgW, imgH); + + // Now we perform our rendering + Graphics2D graphics2D = (Graphics2D)graphics; + graphics2D.setRenderingHints(buildRenderingHints(dithering, interpolation)); + log.trace("{}", graphics2D.getRenderingHints()); + + log.debug("Memory: {}m/{}m", (Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()) / 1048576, Runtime.getRuntime().maxMemory() / 1048576); + + if (!manualReverse) { + graphics2D.drawImage(imgToPrint, (int)boundX, (int)boundY, (int)(boundX + imgW), (int)(boundY + imgH), + 0, 0, imgToPrint.getWidth(), imgToPrint.getHeight(), null); + } else { + graphics2D.drawImage(imgToPrint, (int)(boundW + boundX - imgW), (int)(boundH + boundY - imgH), (int)(boundW + boundX), (int)(boundH + boundY), + 0, 0, imgToPrint.getWidth(), imgToPrint.getHeight(), null); + } + + // Valid page + return PAGE_EXISTS; + } + + /** + * + * @param image + * @param pageFormat + * @return + */ + private BufferedImage scale(BufferedImage image, PageFormat pageFormat) { + //scale up to print density (using less of a stretch if image is already larger than page) + double upScale = dpiScale * Math.min((pageFormat.getImageableWidth() / image.getWidth()), (pageFormat.getImageableHeight() / image.getHeight())); + if (upScale > dpiScale) { upScale = dpiScale; } else if (upScale < 1) { upScale = 1; } + + if (upScale > 1) { + log.debug("Scaling image up by x{}", upScale); + + BufferedImage scaled = new BufferedImage((int)(image.getWidth() * upScale), (int)(image.getHeight() * upScale), BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = scaled.createGraphics(); + g2d.setRenderingHints(buildRenderingHints(dithering, interpolation)); + g2d.drawImage(image, 0, 0, (int)(image.getWidth() * upScale), (int)(image.getHeight() * upScale), null); + g2d.dispose(); + + return scaled; + } else { + log.debug("No need to upscale image"); + return image; + } + } + + /** + * Rotates {@code image} by the specified {@code angle}. + * + * @param image BufferedImage to rotate + * @param angle Rotation angle in degrees + * @return Rotated image data + */ + public static BufferedImage rotate(BufferedImage image, double angle, Object dithering, Object interpolation) { + double rads = Math.toRadians(angle); + double sin = Math.abs(Math.sin(rads)), cos = Math.abs(Math.cos(rads)); + + int sWidth = image.getWidth(), sHeight = image.getHeight(); + int eWidth = (int)Math.floor((sWidth * cos) + (sHeight * sin)), eHeight = (int)Math.floor((sHeight * cos) + (sWidth * sin)); + + BufferedImage result; + if(!GraphicsEnvironment.isHeadless()) { + GraphicsConfiguration gc = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()[0].getDefaultConfiguration(); + result = gc.createCompatibleImage(eWidth, eHeight, Transparency.TRANSLUCENT); + } else { + result = new BufferedImage(eWidth, eHeight, Transparency.TRANSLUCENT); + } + + Graphics2D g2d = result.createGraphics(); + g2d.setRenderingHints(buildRenderingHints(dithering, interpolation)); + g2d.translate((eWidth - sWidth) / 2, (eHeight - sHeight) / 2); + g2d.rotate(rads, sWidth / 2, sHeight / 2); + + if (angle % 90 == 0 || interpolation == RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR) { + g2d.drawRenderedImage(image, null); + } else { + g2d.setPaint(new TexturePaint(image, new Rectangle2D.Float(0, 0, image.getWidth(), image.getHeight()))); + g2d.fillRect(0, 0, image.getWidth(), image.getHeight()); + } + + g2d.dispose(); + + return result; + } + + private BufferedImage rotate(BufferedImage image, double angle) { + return rotate(image, angle, dithering, interpolation); + } + + @Override + public void cleanup() { + images.clear(); + + dpiScale = 1.0; + scaleImage = false; + imageRotation = 0; + dithering = RenderingHints.VALUE_DITHER_DEFAULT; + interpolation = RenderingHints.VALUE_INTERPOLATION_BICUBIC; + manualReverse = false; + } + +} diff --git a/old code/tray/src/qz/printer/action/PrintPDF.java b/old code/tray/src/qz/printer/action/PrintPDF.java new file mode 100755 index 0000000..8e3e68c --- /dev/null +++ b/old code/tray/src/qz/printer/action/PrintPDF.java @@ -0,0 +1,326 @@ +package qz.printer.action; + +import com.github.zafarkhaja.semver.Version; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.io.IOUtils; +import org.apache.pdfbox.multipdf.Splitter; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.printing.Scaling; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import qz.common.Constants; +import qz.printer.PrintOptions; +import qz.printer.PrintOutput; +import qz.printer.action.pdf.BookBundle; +import qz.printer.action.pdf.PDFWrapper; +import qz.utils.ConnectionUtilities; +import qz.utils.PrintingUtilities; +import qz.utils.SystemUtilities; + +import javax.print.attribute.PrintRequestAttributeSet; +import javax.print.attribute.standard.Media; +import javax.print.attribute.standard.MediaPrintableArea; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.print.PageFormat; +import java.awt.print.Paper; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; +import java.io.*; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +public class PrintPDF extends PrintPixel implements PrintProcessor { + + private static final Logger log = LogManager.getLogger(PrintPDF.class); + + private List originals; + private List printables; + private Splitter splitter = new Splitter(); + + private double docWidth = 0; + private double docHeight = 0; + private boolean ignoreTransparency = false; + private boolean altFontRendering = false; + + + public PrintPDF() { + originals = new ArrayList<>(); + printables = new ArrayList<>(); + } + + @Override + public PrintingUtilities.Format getFormat() { + return PrintingUtilities.Format.PDF; + } + + @Override + public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException { + PrintOptions.Pixel pxlOpts = options.getPixelOptions(); + double convert = 72.0 / pxlOpts.getUnits().as1Inch(); + + for(int i = 0; i < printData.length(); i++) { + JSONObject data = printData.getJSONObject(i); + HashSet pagesToPrint = new HashSet<>(); + + if (!data.isNull("options")) { + JSONObject dataOpt = data.getJSONObject("options"); + + if (!dataOpt.isNull("pageWidth") && dataOpt.optDouble("pageWidth") > 0) { + docWidth = dataOpt.optDouble("pageWidth") * convert; + } + if (!dataOpt.isNull("pageHeight") && dataOpt.optDouble("pageHeight") > 0) { + docHeight = dataOpt.optDouble("pageHeight") * convert; + } + + ignoreTransparency = dataOpt.optBoolean("ignoreTransparency", false); + altFontRendering = dataOpt.optBoolean("altFontRendering", false); + + if (!dataOpt.isNull("pageRanges")) { + String[] ranges = dataOpt.optString("pageRanges", "").split(","); + for(String range : ranges) { + range = range.trim(); + if(range.isEmpty()) { + continue; + } + String[] period = range.split("-"); + + try { + int start = Integer.parseInt(period[0]); + pagesToPrint.add(start); + + if (period.length > 1) { + int end = Integer.parseInt(period[period.length - 1]); + pagesToPrint.addAll(IntStream.rangeClosed(start, end).boxed().collect(Collectors.toSet())); + } + } + catch(NumberFormatException nfe) { + log.warn("Unable to parse page range {}.", range); + } + } + } + } + + PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.FILE); + + try { + PDDocument doc; + switch(flavor) { + case PLAIN: + // There's really no such thing as a 'PLAIN' PDF, assume it's a URL + case FILE: + doc = PDDocument.load(ConnectionUtilities.getInputStream(data.getString("data"), true)); + break; + default: + doc = PDDocument.load(new ByteArrayInputStream(flavor.read(data.getString("data")))); + } + + if (pxlOpts.getBounds() != null) { + PrintOptions.Bounds bnd = pxlOpts.getBounds(); + + for(PDPage page : doc.getPages()) { + PDRectangle box = new PDRectangle( + (float)(bnd.getX() * convert), + page.getMediaBox().getUpperRightY() - (float)((bnd.getHeight() + bnd.getY()) * convert), + (float)(bnd.getWidth() * convert), + (float)(bnd.getHeight() * convert)); + page.setMediaBox(box); + } + } + + if (pagesToPrint.isEmpty()) { + pagesToPrint.addAll(IntStream.rangeClosed(1, doc.getNumberOfPages()).boxed().collect(Collectors.toSet())); + } + + originals.add(doc); + + List splitPages = splitter.split(doc); + originals.addAll(splitPages); //ensures non-ranged page will still get closed + + for(int pg = 0; pg < splitPages.size(); pg++) { + if (pagesToPrint.contains(pg + 1)) { //ranges are 1-indexed + printables.add(splitPages.get(pg)); + } + } + } + catch(FileNotFoundException e) { + throw new UnsupportedOperationException("PDF file specified could not be found.", e); + } + catch(IOException e) { + throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s as a PDF file: %s", flavor, data.getString("data"), e.getLocalizedMessage()), e); + } + } + + log.debug("Parsed {} files for printing", printables.size()); + } + + @Override + public PrintRequestAttributeSet applyDefaultSettings(PrintOptions.Pixel pxlOpts, PageFormat page, Media[] supported) { + if (pxlOpts.getOrientation() != null) { + //page orient does not set properly on pdfs with orientation requested attribute + page.setOrientation(pxlOpts.getOrientation().getAsOrientFormat()); + } + + return super.applyDefaultSettings(pxlOpts, page, supported); + } + + @Override + public void print(PrintOutput output, PrintOptions options) throws PrinterException { + if (printables.isEmpty()) { + log.warn("Nothing to print"); + return; + } + + PrinterJob job = PrinterJob.getPrinterJob(); + job.setPrintService(output.getPrintService()); + + PrintOptions.Pixel pxlOpts = options.getPixelOptions(); + Scaling scale = (pxlOpts.isScaleContent()? Scaling.SCALE_TO_FIT:Scaling.ACTUAL_SIZE); + + PrintRequestAttributeSet attributes = applyDefaultSettings(pxlOpts, job.getPageFormat(null), (Media[])output.getPrintService().getSupportedAttributeValues(Media.class, null, null)); + + // Disable attributes per https://github.com/qzind/tray/issues/174 + if (SystemUtilities.isMac() && Constants.JAVA_VERSION.compareWithBuildsTo(Version.valueOf("1.8.0+202")) < 0) { + log.warn("MacOS and Java < 1.8.0u202 cannot use attributes with PDF prints, disabling"); + attributes.clear(); + } + + RenderingHints hints = new RenderingHints(buildRenderingHints(pxlOpts.getDithering(), pxlOpts.getInterpolation())); + double useDensity = pxlOpts.getDensity(); + + if (!pxlOpts.isRasterize()) { + if (pxlOpts.getDensity() > 0) { + // clear density for vector prints (applied via print attributes instead) + useDensity = 0; + } else if (SystemUtilities.isMac() && Constants.JAVA_VERSION.compareWithBuildsTo(Version.valueOf("1.8.0+121")) < 0) { + log.warn("OSX systems cannot print vector PDF's, forcing raster to prevent crash."); + useDensity = options.getDefaultOptions().getDensity(); + } + } + + BookBundle bundle = new BookBundle(); + + for(PDDocument doc : printables) { + PageFormat page = job.getPageFormat(null); + applyDefaultSettings(pxlOpts, page, output.getSupportedMedia()); + + //trick pdfbox into an alternate doc size if specified + if (docWidth > 0 || docHeight > 0) { + Paper paper = page.getPaper(); + + if (docWidth <= 0) { docWidth = page.getImageableWidth(); } + if (docHeight <= 0) { docHeight = page.getImageableHeight(); } + + paper.setImageableArea(paper.getImageableX(), paper.getImageableY(), docWidth, docHeight); + page.setPaper(paper); + + scale = Scaling.SCALE_TO_FIT; //to get custom size we need to force scaling + + //pdf uses imageable area from Paper, so this can be safely removed + attributes.remove(MediaPrintableArea.class); + } + + for(PDPage pd : doc.getPages()) { + if (pxlOpts.getRotation() % 360 != 0) { + rotatePage(doc, pd, pxlOpts.getRotation()); + } + + if (pxlOpts.getOrientation() == null) { + PDRectangle bounds = pd.getBBox(); + if ((page.getImageableHeight() > page.getImageableWidth() && bounds.getWidth() > bounds.getHeight()) ^ (pd.getRotation() / 90) % 2 == 1) { + log.info("Adjusting orientation to print landscape PDF source"); + page.setOrientation(PrintOptions.Orientation.LANDSCAPE.getAsOrientFormat()); + } + } else if (pxlOpts.getOrientation() != PrintOptions.Orientation.PORTRAIT) { + //flip imageable area dimensions when in landscape + Paper repap = page.getPaper(); + repap.setImageableArea(repap.getImageableX(), repap.getImageableY(), repap.getImageableHeight(), repap.getImageableWidth()); + page.setPaper(repap); + + //reverse fix for OSX + if (SystemUtilities.isMac() && pxlOpts.getOrientation() == PrintOptions.Orientation.REVERSE_LANDSCAPE) { + pd.setRotation(pd.getRotation() + 180); + } + } + } + + PDFWrapper wrapper = new PDFWrapper(doc, scale, false, ignoreTransparency, altFontRendering, + (float)(useDensity * pxlOpts.getUnits().as1Inch()), + false, pxlOpts.getOrientation(), hints); + + bundle.append(wrapper, page, doc.getNumberOfPages()); + } + + if (pxlOpts.getSpoolSize() > 0 && bundle.getNumberOfPages() > pxlOpts.getSpoolSize()) { + int jobNum = 1; + int offset = 0; + while(offset < bundle.getNumberOfPages()) { + job.setJobName(pxlOpts.getJobName(Constants.PDF_PRINT) + "-" + jobNum++); + job.setPageable(bundle.wrapAndPresent(offset, pxlOpts.getSpoolSize())); + + printCopies(output, pxlOpts, job, attributes); + + offset += pxlOpts.getSpoolSize(); + } + } else { + job.setJobName(pxlOpts.getJobName(Constants.PDF_PRINT)); + job.setPageable(bundle.wrapAndPresent()); + + printCopies(output, pxlOpts, job, attributes); + } + } + + private void rotatePage(PDDocument doc, PDPage page, double rotation) { + try { + //copy page to object for manipulation + PDFormXObject xobject = new PDFormXObject(doc); + InputStream src = page.getContents(); + OutputStream dest = xobject.getStream().createOutputStream(); + + try { IOUtils.copy(src, dest); } + finally { + IOUtils.closeQuietly(src); + IOUtils.closeQuietly(dest); + } + + xobject.setResources(page.getResources()); + xobject.setBBox(page.getBBox()); + + //draw our object at a rotated angle + AffineTransform transform = new AffineTransform(); + transform.rotate(Math.toRadians(360 - rotation), xobject.getBBox().getWidth() / 2.0, xobject.getBBox().getHeight() / 2.0); + xobject.setMatrix(transform); + + PDPageContentStream stream = new PDPageContentStream(doc, page); + stream.drawForm(xobject); + stream.close(); + } + catch(IOException e) { + log.warn("Failed to rotate PDF page for printing"); + } + } + + @Override + public void cleanup() { + for(PDDocument doc : originals) { + try { doc.close(); } catch(IOException ignore) {} + } + + originals.clear(); + printables.clear(); + + docWidth = 0; + docHeight = 0; + ignoreTransparency = false; + altFontRendering = false; + } +} diff --git a/old code/tray/src/qz/printer/action/PrintPixel.java b/old code/tray/src/qz/printer/action/PrintPixel.java new file mode 100755 index 0000000..fecefbc --- /dev/null +++ b/old code/tray/src/qz/printer/action/PrintPixel.java @@ -0,0 +1,231 @@ +package qz.printer.action; + +import javafx.print.PaperSource; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.printer.PrintOptions; +import qz.printer.PrintOutput; +import qz.utils.SystemUtilities; + +import javax.print.attribute.HashPrintRequestAttributeSet; +import javax.print.attribute.PrintRequestAttributeSet; +import javax.print.attribute.standard.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.awt.image.ColorModel; +import java.awt.image.IndexColorModel; +import java.awt.print.PageFormat; +import java.awt.print.Paper; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; +import java.util.List; +import java.util.*; +import java.util.function.Function; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +public abstract class PrintPixel { + + private static final Logger log = LogManager.getLogger(PrintPixel.class); + + private static final List MAC_BAD_IMAGE_TYPES = Arrays.asList(BufferedImage.TYPE_BYTE_BINARY, BufferedImage.TYPE_CUSTOM); + + + protected PrintRequestAttributeSet applyDefaultSettings(PrintOptions.Pixel pxlOpts, PageFormat page, Media[] supported) { + PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet(); + + //apply general attributes + // If colortype is default, leave printColor blank. The system's printer settings will be used instead. + if (pxlOpts.getColorType() != PrintOptions.ColorType.DEFAULT) { + attributes.add(pxlOpts.getColorType().getAsChromaticity()); + } + attributes.add(pxlOpts.getDuplex()); + if (pxlOpts.getOrientation() != null) { + attributes.add(pxlOpts.getOrientation().getAsOrientRequested()); + } + if (pxlOpts.getPrinterTray() != null && !pxlOpts.getPrinterTray().isEmpty()) { + Media tray = findMediaTray(supported, pxlOpts.getPrinterTray()); + if (tray != null) { + attributes.add(tray); + } + } + + //TODO - set paper thickness + + + // Java prints using inches at 72dpi + final float CONVERT = pxlOpts.getUnits().toInches() * 72f; + + log.trace("DPI: [{}x{}]\tCNV: {}", pxlOpts.getDensity(), pxlOpts.getCrossDensity(), CONVERT); + if (pxlOpts.getDensity() > 0) { + double cross = pxlOpts.getCrossDensity(); + if (cross == 0) { cross = pxlOpts.getDensity(); } + + attributes.add(new PrinterResolution((int)cross, (int)pxlOpts.getDensity(), pxlOpts.getUnits().getDPIUnits())); + } + + //apply sizing and margins + Paper paper = page.getPaper(); + + float pageX = 0f; + float pageY = 0f; + float pageW = (float)page.getWidth() / CONVERT; + float pageH = (float)page.getHeight() / CONVERT; + + //page size + if (pxlOpts.getSize() != null && pxlOpts.getSize().getWidth() > 0 && pxlOpts.getSize().getHeight() > 0) { + pageW = (float)pxlOpts.getSize().getWidth(); + pageH = (float)pxlOpts.getSize().getHeight(); + + paper.setSize(pageW * CONVERT, pageH * CONVERT); + } + + //margins + if (pxlOpts.getMargins() != null) { + pageX += pxlOpts.getMargins().left(); + pageY += pxlOpts.getMargins().top(); + pageW -= (pxlOpts.getMargins().right() + pxlOpts.getMargins().left()); + pageH -= (pxlOpts.getMargins().bottom() + pxlOpts.getMargins().top()); + } + + log.trace("Drawable area: {},{}:{},{}", pageX, pageY, pageW, pageH); + if (pageW > 0 && pageH > 0) { + attributes.add(new MediaPrintableArea(pageX, pageY, pageW, pageH, pxlOpts.getUnits().getMediaSizeUnits())); + paper.setImageableArea(pageX * CONVERT, pageY * CONVERT, pageW * CONVERT, pageH * CONVERT); + page.setPaper(paper); + } else { + log.warn("Could not apply custom size, using printer default"); + attributes.add(new MediaPrintableArea(0, 0, (float)page.getWidth() / 72f, (float)page.getHeight() / 72f, PrintOptions.Unit.INCH.getMediaSizeUnits())); + } + + log.trace("{}", Arrays.toString(attributes.toArray())); + + return attributes; + } + + + protected void printCopies(PrintOutput output, PrintOptions.Pixel pxlOpts, PrinterJob job, PrintRequestAttributeSet attributes) throws PrinterException { + log.info("Starting printing ({} copies)", pxlOpts.getCopies()); + + PrinterResolution rUsing = (PrinterResolution)attributes.get(PrinterResolution.class); + if (rUsing != null) { + List rSupport = output.getNativePrinter().getResolutions(); + if (!rSupport.isEmpty()) { + if (!rSupport.contains(rUsing)) { + log.warn("Not using a supported DPI for printing"); + log.debug("Available DPI: {}", ArrayUtils.toString(rSupport)); + } + } else { + log.warn("Supported printer densities not found"); + } + } + + CopiesSupported cSupport = (CopiesSupported)output.getPrintService() + .getSupportedAttributeValues(Copies.class, output.getPrintService().getSupportedDocFlavors()[0], attributes); + + if (cSupport != null && cSupport.contains(pxlOpts.getCopies())) { + attributes.add(new Copies(pxlOpts.getCopies())); + job.print(attributes); + } else { + for(int i = 0; i < pxlOpts.getCopies(); i++) { + job.print(attributes); + } + } + } + + /** + * FIXME: Temporary fix for OS X 10.10 hard crash. + * See https://github.com/qzind/qz-print/issues/75 + */ + protected BufferedImage fixColorModel(BufferedImage imgToPrint) { + if (SystemUtilities.isMac()) { + if (MAC_BAD_IMAGE_TYPES.contains(imgToPrint.getType())) { + BufferedImage sanitizedImage; + ColorModel cm = imgToPrint.getColorModel(); + + if (cm instanceof IndexColorModel) { + log.info("Image converted to 256 colors for OSX 10.10 Workaround"); + sanitizedImage = new BufferedImage(imgToPrint.getWidth(), imgToPrint.getHeight(), BufferedImage.TYPE_BYTE_INDEXED, (IndexColorModel)cm); + } else { + log.info("Image converted to ARGB for OSX 10.10 Workaround"); + sanitizedImage = new BufferedImage(imgToPrint.getWidth(), imgToPrint.getHeight(), BufferedImage.TYPE_INT_ARGB); + } + + sanitizedImage.createGraphics().drawImage(imgToPrint, 0, 0, null); + imgToPrint = sanitizedImage; + } + } + + return imgToPrint; + } + + protected static Map buildRenderingHints(Object dithering, Object interpolation) { + Map rhMap = new HashMap<>(); + rhMap.put(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + rhMap.put(RenderingHints.KEY_DITHERING, dithering); + rhMap.put(RenderingHints.KEY_INTERPOLATION, interpolation); + rhMap.put(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + rhMap.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + rhMap.put(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + if (RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR.equals(interpolation)) { + rhMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF); + rhMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_OFF); + } else { + rhMap.put(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + rhMap.put(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + } + + + return rhMap; + } + + protected Media findMediaTray(Media[] supportedMedia, String traySelection) { + HashMap mediaTrays = new HashMap<>(); + for(Media m : supportedMedia) { + if (m instanceof MediaTray) { + mediaTrays.put(m.toString(), m); + } + } + + String tray = findTray(mediaTrays.keySet(), traySelection); + return mediaTrays.get(tray); + } + + protected PaperSource findFXTray(Set paperSources, String traySelection) { + Map fxTrays = paperSources.stream().collect(Collectors.toMap(PaperSource::getName, Function.identity())); + + String tray = findTray(fxTrays.keySet(), traySelection); + return fxTrays.get(tray); + } + + private String findTray(Set trayOptions, String traySelection) { + Pattern exactPattern = Pattern.compile("\\b" + Pattern.quote(traySelection) + "\\b", Pattern.CASE_INSENSITIVE); + Pattern fuzzyPattern = Pattern.compile("\\b.*?[" + Pattern.quote(traySelection) + "]+.*?\\b", Pattern.CASE_INSENSITIVE); + String bestFit = null; + Integer fuzzyFitDelta = null; + + for(String option : trayOptions) { + Matcher exactly = exactPattern.matcher(option.trim()); + Matcher fuzzily = fuzzyPattern.matcher(option.trim()); + + if (exactly.find()) { + bestFit = option; + break; + } + + while(fuzzily.find()) { + //look for as close to exact match as possible + int delta = Math.abs(fuzzily.group().length() - traySelection.length()); + if (fuzzyFitDelta == null || delta < fuzzyFitDelta) { + fuzzyFitDelta = delta; + bestFit = option; + } + } + } + + return bestFit; + } + +} diff --git a/old code/tray/src/qz/printer/action/PrintProcessor.java b/old code/tray/src/qz/printer/action/PrintProcessor.java new file mode 100755 index 0000000..500dc3b --- /dev/null +++ b/old code/tray/src/qz/printer/action/PrintProcessor.java @@ -0,0 +1,39 @@ +package qz.printer.action; + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import qz.printer.PrintOptions; +import qz.printer.PrintOutput; +import qz.utils.PrintingUtilities; + +import javax.print.PrintException; +import java.awt.print.PrinterException; + +public interface PrintProcessor { + + + PrintingUtilities.Format getFormat(); + + /** + * Used to parse information passed from the web API for printing. + * + * @param printData JSON Array of printer data + * @param options Printing options to use for the print job + */ + void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException; + + + /** + * Used to setup and send documents to the specified printing {@code service}. + * + * @param output Destination used for printing + * @param options Printing options to use for the print job + */ + void print(PrintOutput output, PrintOptions options) throws PrintException, PrinterException; + + /** + * Reset a processor back to it's initial state. + */ + void cleanup(); + +} diff --git a/old code/tray/src/qz/printer/action/PrintRaw.java b/old code/tray/src/qz/printer/action/PrintRaw.java new file mode 100755 index 0000000..aa4a3a5 --- /dev/null +++ b/old code/tray/src/qz/printer/action/PrintRaw.java @@ -0,0 +1,530 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.printer.action; + +import com.ibm.icu.text.ArabicShapingException; +import org.apache.commons.lang3.StringUtils; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.ByteArrayBuilder; +import qz.common.Constants; +import qz.exception.NullCommandException; +import qz.exception.NullPrintServiceException; +import qz.printer.action.raw.ImageWrapper; +import qz.printer.action.raw.LanguageType; +import qz.printer.PrintOptions; +import qz.printer.PrintOutput; +import qz.printer.action.html.WebApp; +import qz.printer.action.html.WebAppModel; +import qz.printer.info.NativePrinter; +import qz.printer.status.CupsUtils; +import qz.utils.*; + +import javax.imageio.ImageIO; +import javax.print.*; +import javax.print.attribute.HashPrintRequestAttributeSet; +import javax.print.attribute.PrintRequestAttributeSet; +import javax.print.attribute.standard.JobName; +import javax.print.event.PrintJobEvent; +import javax.print.event.PrintJobListener; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.net.Socket; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Sends raw data to the printer, overriding your operating system's print + * driver. Most useful for printers such as zebra card or barcode printers. + * + * @author A. Tres Finocchiaro + */ +public class PrintRaw implements PrintProcessor { + + private static final Logger log = LogManager.getLogger(PrintRaw.class); + + private ByteArrayBuilder commands; + + private String destEncoding = null; + + private enum Backend { + CUPS_RSS, + CUPS_LPR, + WIN32_WMI + } + + public PrintRaw() { + commands = new ByteArrayBuilder(); + } + + @Override + public PrintingUtilities.Format getFormat() { + return PrintingUtilities.Format.COMMAND; + } + + private byte[] getBytes(String str, String destEncoding) throws ArabicShapingException, IOException { + switch(destEncoding.toLowerCase(Locale.ENGLISH)) { + case "ibm864": + case "cp864": + case "csibm864": + case "864": + case "ibm-864": + return ArabicConversionUtilities.convertToIBM864(str); + default: + return str.getBytes(destEncoding); + } + } + + + @Override + public void parseData(JSONArray printData, PrintOptions options) throws JSONException, UnsupportedOperationException { + for(int i = 0; i < printData.length(); i++) { + JSONObject data = printData.optJSONObject(i); + if (data == null) { + data = new JSONObject(); + data.put("data", printData.getString(i)); + } + + String cmd = data.getString("data"); + JSONObject opt = data.optJSONObject("options"); + if (opt == null) { opt = new JSONObject(); } + + PrintingUtilities.Format format = PrintingUtilities.Format.valueOf(data.optString("format", "COMMAND").toUpperCase(Locale.ENGLISH)); + PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(data, PrintingUtilities.Flavor.PLAIN); + PrintOptions.Raw rawOpts = options.getRawOptions(); + PrintOptions.Pixel pxlOpts = options.getPixelOptions(); + + destEncoding = rawOpts.getDestEncoding(); + if (destEncoding == null || destEncoding.isEmpty()) { destEncoding = Charset.defaultCharset().name(); } + + try { + switch(format) { + case HTML: + commands.append(getHtmlWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt)); + break; + case IMAGE: + commands.append(getImageWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt)); + break; + case PDF: + commands.append(getPdfWrapper(cmd, opt, flavor, rawOpts, pxlOpts).getImageCommand(opt)); + break; + case COMMAND: + default: + switch(flavor) { + case PLAIN: + commands.append(getBytes(cmd, destEncoding)); + break; + default: + commands.append(seekConversion(flavor.read(cmd, opt.optString("xmlTag", null)), rawOpts)); + } + break; + } + } + catch(Exception e) { + throw new UnsupportedOperationException(String.format("Cannot parse (%s)%s into a raw %s command: %s", flavor, data.getString("data"), format, e.getLocalizedMessage()), e); + } + } + } + + private byte[] seekConversion(byte[] rawBytes, PrintOptions.Raw rawOpts) { + if (rawOpts.getSrcEncoding() != null) { + if(rawOpts.getSrcEncoding().equals(rawOpts.getDestEncoding()) || rawOpts.getDestEncoding() == null) { + log.warn("Provided srcEncoding and destEncoding are the same, skipping"); + } else { + try { + String rawConvert = new String(rawBytes, rawOpts.getSrcEncoding()); + return rawConvert.getBytes(rawOpts.getDestEncoding()); + } + catch(UnsupportedEncodingException e) { + throw new UnsupportedOperationException(e); + } + } + } + return rawBytes; + } + + private ImageWrapper getImageWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException { + BufferedImage bi; + // 2.0 compat + if (data.startsWith("data:image/") && data.contains(";base64,")) { + String[] parts = data.split(";base64,"); + data = parts[parts.length - 1]; + flavor = PrintingUtilities.Flavor.BASE64; + } + + switch(flavor) { + case PLAIN: + // There's really no such thing as a 'PLAIN' image, assume it's a URL + case FILE: + bi = ImageIO.read(ConnectionUtilities.getInputStream(data, true)); + break; + default: + bi = ImageIO.read(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts))); + } + + return getWrapper(bi, opt, pxlOpts); + } + + private ImageWrapper getPdfWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException { + PDDocument doc; + + switch(flavor) { + case PLAIN: + // There's really no such thing as a 'PLAIN' PDF, assume it's a URL + case FILE: + doc = PDDocument.load(ConnectionUtilities.getInputStream(data, true)); + break; + default: + doc = PDDocument.load(new ByteArrayInputStream(seekConversion(flavor.read(data), rawOpts))); + } + + double scale; + PDRectangle rect = doc.getPage(0).getBBox(); + double pw = opt.optDouble("pageWidth", 0), ph = opt.optDouble("pageHeight", 0); + if (ph <= 0 || (pw > 0 && (rect.getWidth() / rect.getHeight()) >= (pw / ph))) { + scale = pw / rect.getWidth(); + } else { + scale = ph / rect.getHeight(); + } + if (scale <= 0) { scale = 1.0; } + + BufferedImage bi = new PDFRenderer(doc).renderImage(0, (float)scale); + return getWrapper(bi, opt, pxlOpts); + } + + private ImageWrapper getHtmlWrapper(String data, JSONObject opt, PrintingUtilities.Flavor flavor, PrintOptions.Raw rawOpts, PrintOptions.Pixel pxlOpts) throws IOException { + switch(flavor) { + case FILE: + case PLAIN: + // We'll toggle between 'plain' and 'file' when we construct WebAppModel + break; + default: + data = new String(seekConversion(flavor.read(data), rawOpts), destEncoding); + } + + double density = (pxlOpts.getDensity() * pxlOpts.getUnits().as1Inch()); + if (density <= 1) { + density = LanguageType.getType(opt.optString("language")).getDefaultDensity(); + } + double pageZoom = density / 72.0; + + double pageWidth = opt.optInt("pageWidth") / density * 72; + double pageHeight = opt.optInt("pageHeight") / density * 72; + + BufferedImage bi; + WebAppModel model = new WebAppModel(data, (flavor != PrintingUtilities.Flavor.FILE), pageWidth, pageHeight, false, pageZoom); + + try { + WebApp.initialize(); //starts if not already started + bi = WebApp.raster(model); + + // down scale back from web density + double scaleFactor = opt.optDouble("pageWidth", 0) / bi.getWidth(); + BufferedImage scaled = new BufferedImage((int)(bi.getWidth() * scaleFactor), (int)(bi.getHeight() * scaleFactor), BufferedImage.TYPE_INT_ARGB); + Graphics2D g2d = scaled.createGraphics(); + g2d.drawImage(bi, 0, 0, (int)(bi.getWidth() * scaleFactor), (int)(bi.getHeight() * scaleFactor), null); + g2d.dispose(); + bi = scaled; + } + catch(Throwable t) { + if (model.getZoom() > 1 && t instanceof IllegalArgumentException) { + //probably a unrecognized image loader error, try at default zoom + try { + log.warn("Capture failed with increased zoom, attempting with default value"); + model.setZoom(1); + bi = WebApp.raster(model); + } + catch(Throwable tt) { + log.error("Failed to capture html raster"); + throw new IOException(tt); + } + } else { + log.error("Failed to capture html raster"); + throw new IOException(t); + } + } + + return getWrapper(bi, opt, pxlOpts); + } + + private ImageWrapper getWrapper(BufferedImage img, JSONObject opt, PrintOptions.Pixel pxlOpts) throws IOException { + if(img == null) { + throw new IOException("Image provided is empty or null and cannot be converted."); + } + // Rotate image using orientation or rotation before sending to ImageWrapper + if (pxlOpts.getOrientation() != null && pxlOpts.getOrientation() != PrintOptions.Orientation.PORTRAIT) { + img = PrintImage.rotate(img, pxlOpts.getOrientation().getDegreesRot(), pxlOpts.getDithering(), pxlOpts.getInterpolation()); + } else if (pxlOpts.getRotation() % 360 != 0) { + img = PrintImage.rotate(img, pxlOpts.getRotation(), pxlOpts.getDithering(), pxlOpts.getInterpolation()); + } + + ImageWrapper iw = new ImageWrapper(img, LanguageType.getType(opt.optString("language"))); + iw.setCharset(Charset.forName(destEncoding)); + + //ESC/POS only + int density = opt.optInt("dotDensity", -1); + if (density == -1) { + String dStr = opt.optString("dotDensity", null); + if (dStr != null && !dStr.isEmpty()) { + switch(dStr.toLowerCase(Locale.ENGLISH)) { + case "single": density = 32; break; + case "double": density = 33; break; + case "triple": density = 39; break; + // negative: legacy mode + case "single-legacy": density = -32; break; + case "double-legacy": density = -33; break; + } + } else { + density = 32; //default + } + } + iw.setDotDensity(density); + + //EPL only + iw.setxPos(opt.optInt("x", 0)); + iw.setyPos(opt.optInt("y", 0)); + + // PGL only + iw.setLogoId(opt.optString("logoId", "")); + iw.setIgpDots(opt.optBoolean("igpDots", false)); + + return iw; + } + + @Override + public void print(PrintOutput output, PrintOptions options) throws PrintException { + PrintOptions.Raw rawOpts = options.getRawOptions(); + + List pages; + if (rawOpts.getSpoolSize() > 0 && rawOpts.getSpoolEnd() != null && !rawOpts.getSpoolEnd().isEmpty()) { + try { + pages = ByteUtilities.splitByteArray(commands.getByteArray(), rawOpts.getSpoolEnd().getBytes(destEncoding), rawOpts.getSpoolSize()); + } + catch(UnsupportedEncodingException e) { + throw new PrintException(e); + } + } else { + pages = new ArrayList<>(); + pages.add(commands); + } + + List tempFiles = null; + for(int i = 0; i < rawOpts.getCopies(); i++) { + for(int j = 0; j < pages.size(); j++) { + ByteArrayBuilder bab = pages.get(j); + try { + if (output.isSetHost()) { + printToHost(output.getHost(), output.getPort(), bab.getByteArray()); + } else if (output.isSetFile()) { + printToFile(output.getFile(), bab.getByteArray(), true); + } else { + if (rawOpts.isForceRaw()) { + if(tempFiles == null) { + tempFiles = new ArrayList<>(pages.size()); + } + File tempFile; + if(tempFiles.size() <= j) { + tempFile = File.createTempFile("qz_raw_", null); + tempFiles.add(j, tempFile); + printToFile(tempFile, bab.getByteArray(), false); + } else { + tempFile = tempFiles.get(j); + } + if(SystemUtilities.isWindows()) { + // Placeholder only; not yet supported + printToBackend(output.getNativePrinter(), tempFile, Backend.WIN32_WMI); + } else { + // Try CUPS backend first, fallback to LPR + printToBackend(output.getNativePrinter(), tempFile, Backend.CUPS_RSS, Backend.CUPS_LPR); + } + } else { + printToPrinter(output.getPrintService(), bab.getByteArray(), rawOpts); + } + } + } + catch(IOException e) { + cleanupTempFiles(rawOpts.isRetainTemp(), tempFiles); + throw new PrintException(e); + } + } + } + cleanupTempFiles(rawOpts.isRetainTemp(), tempFiles); + } + + private void cleanupTempFiles(boolean retainTemp, List tempFiles) { + if(tempFiles != null) { + if (!retainTemp) { + for(File tempFile : tempFiles) { + if(tempFile != null) { + if(!tempFile.delete()) { + tempFile.deleteOnExit(); + } + } + } + } else { + log.warn("Temp file(s) retained: {}", Arrays.toString(tempFiles.toArray())); + } + } + } + + /** + * A brute-force, however surprisingly elegant way to send a file to a networked printer. + *

+ * Please note that this will completely bypass the Print Spooler, + * so the Operating System will have absolutely no printer information. + * This is printing "blind". + */ + private void printToHost(String host, int port, byte[] cmds) throws IOException { + log.debug("Printing to host {}:{}", host, port); + + //throws any exception and auto-closes socket and stream + try(Socket socket = new Socket(host, port); DataOutputStream out = new DataOutputStream(socket.getOutputStream())) { + out.write(cmds); + } + } + + /** + * Writes the raw commands directly to a file. + * + * @param file File to be written + */ + private void printToFile(File file, byte[] cmds, boolean locationRestricted) throws IOException { + if(file == null) throw new IOException("No file specified"); + + if(locationRestricted && !PrefsSearch.getBoolean(ArgValue.SECURITY_PRINT_TOFILE)) { + log.error("Printing to file '{}' is not permitted. Configure property '{}' to modify this behavior.", + file, ArgValue.SECURITY_PRINT_TOFILE.getMatch()); + throw new IOException(String.format("Printing to file '%s' is not permitted", file)); + } + + log.debug("Printing to file: {}", file.getName()); + + //throws any exception and auto-closes stream + try(OutputStream out = new FileOutputStream(file)) { + out.write(cmds); + } + } + + /** + * Constructs a {@code SimpleDoc} with the {@code commands} byte array. + */ + private void printToPrinter(PrintService service, byte[] cmds, PrintOptions.Raw rawOpts) throws PrintException { + if (service == null) { throw new NullPrintServiceException("Service cannot be null"); } + if (cmds == null || cmds.length == 0) { throw new NullCommandException("No commands found to send to the printer"); } + + SimpleDoc doc = new SimpleDoc(cmds, DocFlavor.BYTE_ARRAY.AUTOSENSE, null); + + PrintRequestAttributeSet attributes = new HashPrintRequestAttributeSet(); + attributes.add(new JobName(rawOpts.getJobName(Constants.RAW_PRINT), Locale.getDefault())); + + DocPrintJob printJob = service.createPrintJob(); + + waitForPrint(printJob, doc, attributes); + } + + protected void waitForPrint(DocPrintJob printJob, Doc doc, PrintRequestAttributeSet attributes) throws PrintException { + final AtomicBoolean finished = new AtomicBoolean(false); + printJob.addPrintJobListener(new PrintJobListener() { + @Override + public void printDataTransferCompleted(PrintJobEvent printJobEvent) { + log.debug("{}", printJobEvent); + finished.set(true); + } + + @Override + public void printJobCompleted(PrintJobEvent printJobEvent) { + log.debug("{}", printJobEvent); + finished.set(true); + } + + @Override + public void printJobFailed(PrintJobEvent printJobEvent) { + log.error("{}", printJobEvent); + finished.set(true); + } + + @Override + public void printJobCanceled(PrintJobEvent printJobEvent) { + log.warn("{}", printJobEvent); + finished.set(true); + } + + @Override + public void printJobNoMoreEvents(PrintJobEvent printJobEvent) { + log.debug("{}", printJobEvent); + finished.set(true); + } + + @Override + public void printJobRequiresAttention(PrintJobEvent printJobEvent) { + log.info("{}", printJobEvent); + } + }); + + log.trace("Sending print job to printer"); + printJob.print(doc, attributes); + + while(!finished.get()) { + try { Thread.sleep(100); } catch(Exception ignore) {} + } + + log.trace("Print job received by printer"); + } + + /** + * Direct/backend printing modes for forced raw printing + */ + public void printToBackend(NativePrinter printer, File tempFile, Backend... backends) throws IOException, PrintException { + boolean success = false; + + for(Backend backend : backends) { + switch(backend) { + case CUPS_LPR: + // Use command line "lp" on Linux, BSD, Solaris, OSX, etc. + String[] lpCmd = new String[] {"lp", "-d", printer.getPrinterId(), "-o", "raw", tempFile.getAbsolutePath()}; + if (!(success = ShellUtilities.execute(lpCmd))) { + log.debug(StringUtils.join(lpCmd, ' ')); + } + break; + case CUPS_RSS: + // Submit job via cupsDoRequest(...) via JNA against localhost:631\ + success = CupsUtils.sendRawFile(printer, tempFile); + break; + case WIN32_WMI: + default: + throw new UnsupportedOperationException("Raw backend \"" + backend + "\" is not yet supported."); + } + if(success) { + break; + } + } + if (!success) { + throw new PrintException("Forced raw printing failed"); + } + } + + @Override + public void cleanup() { + commands.clear(); + destEncoding = null; + } + +} diff --git a/old code/tray/src/qz/printer/action/ProcessorFactory.java b/old code/tray/src/qz/printer/action/ProcessorFactory.java new file mode 100755 index 0000000..73164b6 --- /dev/null +++ b/old code/tray/src/qz/printer/action/ProcessorFactory.java @@ -0,0 +1,44 @@ +package qz.printer.action; + +import org.apache.commons.pool2.KeyedPooledObjectFactory; +import org.apache.commons.pool2.PooledObject; +import org.apache.commons.pool2.impl.DefaultPooledObject; +import qz.utils.PrintingUtilities; + +public class ProcessorFactory implements KeyedPooledObjectFactory { + + @Override + public PooledObject makeObject(PrintingUtilities.Format key) throws Exception { + PrintProcessor processor; + switch(key) { + case HTML: processor = new PrintHTML(); break; + case IMAGE: processor = new PrintImage(); break; + case PDF: processor = new PrintPDF(); break; + case DIRECT: processor = new PrintDirect(); break; + case COMMAND: default: processor = new PrintRaw(); break; + } + + return new DefaultPooledObject<>(processor); + } + + @Override + public boolean validateObject(PrintingUtilities.Format key, PooledObject p) { + return true; //no-op + } + + @Override + public void activateObject(PrintingUtilities.Format key, PooledObject p) throws Exception { + //no-op + } + + @Override + public void passivateObject(PrintingUtilities.Format key, PooledObject p) throws Exception { + p.getObject().cleanup(); + } + + @Override + public void destroyObject(PrintingUtilities.Format key, PooledObject p) throws Exception { + //no-op + } + +} diff --git a/old code/tray/src/qz/printer/action/html/WebApp.java b/old code/tray/src/qz/printer/action/html/WebApp.java new file mode 100755 index 0000000..b4f1da0 --- /dev/null +++ b/old code/tray/src/qz/printer/action/html/WebApp.java @@ -0,0 +1,512 @@ +package qz.printer.action.html; + +import com.github.zafarkhaja.semver.Version; +import com.sun.javafx.tk.TKPulseListener; +import com.sun.javafx.tk.Toolkit; +import javafx.animation.AnimationTimer; +import javafx.application.Application; +import javafx.application.Platform; +import javafx.beans.value.ChangeListener; +import javafx.concurrent.Worker; +import javafx.embed.swing.SwingFXUtils; +import javafx.print.PageLayout; +import javafx.print.PrinterJob; +import javafx.scene.Scene; +import javafx.scene.shape.Rectangle; +import javafx.scene.transform.Scale; +import javafx.scene.transform.Transform; +import javafx.scene.transform.Translate; +import javafx.scene.web.WebView; +import javafx.stage.Stage; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import qz.common.Constants; +import qz.utils.SystemUtilities; +import qz.ws.PrintSocketServer; + +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.IntPredicate; + +/** + * JavaFX container for taking HTML snapshots. + * Used by PrintHTML to generate printable images. + *

+ * Do not use constructor (used by JavaFX), instead call {@code WebApp.initialize()} + */ +public class WebApp extends Application { + + private static final Logger log = LogManager.getLogger(WebApp.class); + + private static WebApp instance = null; + private static Version webkitVersion = null; + private static int CAPTURE_FRAMES = 2; + private static int VECTOR_FRAMES = 1; + private static Stage stage; + private static WebView webView; + private static double pageWidth; + private static double pageHeight; + private static double pageZoom; + private static boolean raster; + private static boolean headless; + + private static CountDownLatch startupLatch; + private static CountDownLatch captureLatch; + + private static IntPredicate printAction; + private static final AtomicReference thrown = new AtomicReference<>(); + + // JDK-8283686: Printing WebView may results in empty page + private static final Version JDK_8283686_START = Version.valueOf(/* WebKit */ "609.1.0"); + private static final Version JDK_8283686_END = Version.valueOf(/* WebKit */ "612.1.0"); + private static final int JDK_8283686_VECTOR_FRAMES = 30; + + + //listens for a Succeeded state to activate image capture + private static ChangeListener stateListener = (ov, oldState, newState) -> { + log.trace("New state: {} > {}", oldState, newState); + + // Cancelled should probably throw exception listener, but does not + if (newState == Worker.State.CANCELLED) { + // This can happen for file downloads, e.g. "response-content-disposition=attachment" + // See https://github.com/qzind/tray/issues/1183 + unlatch(new IOException("Page load was cancelled for an unknown reason")); + } + if (newState == Worker.State.SUCCEEDED) { + boolean hasBody = (boolean)webView.getEngine().executeScript("document.body != null"); + if (!hasBody) { + log.warn("Loaded page has no body - likely a redirect, skipping state"); + return; + } + + //ensure html tag doesn't use scrollbars, clipping page instead + Document doc = webView.getEngine().getDocument(); + NodeList tags = doc.getElementsByTagName("html"); + if (tags != null && tags.getLength() > 0) { + Node base = tags.item(0); + Attr applied = (Attr)base.getAttributes().getNamedItem("style"); + if (applied == null) { + applied = doc.createAttribute("style"); + } + applied.setValue(applied.getValue() + "; overflow: hidden;"); + base.getAttributes().setNamedItem(applied); + } + + //width was resized earlier (for responsive html), then calculate the best fit height + // FIXME: Should only be needed when height is unknown but fixes blank vector prints + double fittedHeight = findHeight(); + boolean heightNeeded = pageHeight <= 0; + + if (heightNeeded) { + pageHeight = fittedHeight; + } + + // find and set page zoom for increased quality + double usableZoom = calculateSupportedZoom(pageWidth, pageHeight); + if (usableZoom < pageZoom) { + log.warn("Zoom level {} decreased to {} due to physical memory limitations", pageZoom, usableZoom); + pageZoom = usableZoom; + } + webView.setZoom(pageZoom); + log.trace("Zooming in by x{} for increased quality", pageZoom); + + adjustSize(pageWidth * pageZoom, pageHeight * pageZoom); + + //need to check for height again as resizing can cause partial results + if (heightNeeded) { + fittedHeight = findHeight(); + if (fittedHeight != pageHeight) { + adjustSize(pageWidth * pageZoom, fittedHeight * pageZoom); + } + } + + log.trace("Set HTML page height to {}", pageHeight); + + autosize(webView); + + Platform.runLater(() -> new AnimationTimer() { + int frames = 0; + + @Override + public void handle(long l) { + if (printAction.test(++frames)) { + stop(); + } + } + }.start()); + } + }; + + //listens for load progress + private static ChangeListener workDoneListener = (ov, oldWork, newWork) -> log.trace("Done: {} > {}", oldWork, newWork); + + private static ChangeListener msgListener = (ov, oldMsg, newMsg) -> log.trace("New status: {}", newMsg); + + //listens for failures + private static ChangeListener exceptListener = (obs, oldExc, newExc) -> { + if (newExc != null) { unlatch(newExc); } + }; + + + /** Called by JavaFX thread */ + public WebApp() { + instance = this; + } + + /** Starts JavaFX thread if not already running */ + public static synchronized void initialize() throws IOException { + if (instance == null) { + startupLatch = new CountDownLatch(1); + // For JDK8 compat + headless = false; + + // JDK11+ depends bundled javafx + if (Constants.JAVA_VERSION.getMajorVersion() >= 11) { + // Monocle default for unit tests + boolean useMonocle = true; + if (PrintSocketServer.getTrayManager() != null) { + // Honor user monocle override + useMonocle = PrintSocketServer.getTrayManager().isMonoclePreferred(); + // Trust TrayManager's headless detection + headless = PrintSocketServer.getTrayManager().isHeadless(); + } else { + // Fallback for JDK11+ + headless = true; + } + if (useMonocle && SystemUtilities.hasMonocle()) { + log.trace("Initializing monocle platform"); + System.setProperty("javafx.platform", "monocle"); + // Don't set glass.platform on Linux per https://github.com/qzind/tray/issues/702 + switch(SystemUtilities.getOs()) { + case WINDOWS: + case MAC: + System.setProperty("glass.platform", "Monocle"); + break; + default: + // don't set "glass.platform" + } + + //software rendering required headless environments + if (headless) { + System.setProperty("prism.order", "sw"); + } + } else { + log.warn("Monocle platform will not be used"); + } + } + + new Thread(() -> Application.launch(WebApp.class)).start(); + } + + if (startupLatch.getCount() > 0) { + try { + log.trace("Waiting for JavaFX.."); + if (!startupLatch.await(60, TimeUnit.SECONDS)) { + throw new IOException("JavaFX did not start"); + } else { + log.trace("Running a test snapshot to size the stage..."); + try { + raster(new WebAppModel("

startup

", true, 0, 0, true, 2)); + log.trace("JFX initialized successfully"); + } + catch(Throwable t) { + throw new IOException(t); + } + } + } + catch(InterruptedException ignore) {} + } + } + + @Override + public void start(Stage st) throws Exception { + startupLatch.countDown(); + log.debug("Started JavaFX"); + + webView = new WebView(); + + // JDK-8283686: Printing WebView may results in empty page + // See also https://github.com/qzind/tray/issues/778 + if(getWebkitVersion() == null || + (getWebkitVersion().greaterThan(JDK_8283686_START) && + getWebkitVersion().lessThan(JDK_8283686_END))) { + VECTOR_FRAMES = JDK_8283686_VECTOR_FRAMES; // Additional pulses needed for vector graphics + } + + st.setScene(new Scene(webView)); + stage = st; + stage.setWidth(1); + stage.setHeight(1); + + Worker worker = webView.getEngine().getLoadWorker(); + worker.stateProperty().addListener(stateListener); + worker.workDoneProperty().addListener(workDoneListener); + worker.exceptionProperty().addListener(exceptListener); + worker.messageProperty().addListener(msgListener); + + //prevents JavaFX from shutting down when hiding window + Platform.setImplicitExit(false); + } + + /** + * Prints the loaded source specified in the passed {@code model}. + * + * @param job A setup JavaFx {@code PrinterJob} + * @param model The model specifying the web page parameters + * @throws Throwable JavaFx will throw a generic {@code Throwable} class for any issues + */ + public static synchronized void print(final PrinterJob job, final WebAppModel model) throws Throwable { + model.setZoom(1); //vector prints do not need to use zoom + raster = false; + + load(model, (int frames) -> { + if(frames == VECTOR_FRAMES) { + try { + double printScale = 72d / 96d; + webView.getTransforms().add(new Scale(printScale, printScale)); + + PageLayout layout = job.getJobSettings().getPageLayout(); + if (model.isScaled()) { + double viewWidth = webView.getWidth() * printScale; + double viewHeight = webView.getHeight() * printScale; + + double scale; + if ((viewWidth / viewHeight) >= (layout.getPrintableWidth() / layout.getPrintableHeight())) { + scale = (layout.getPrintableWidth() / viewWidth); + } else { + scale = (layout.getPrintableHeight() / viewHeight); + } + webView.getTransforms().add(new Scale(scale, scale)); + } + + Platform.runLater(() -> { + double useScale = 1; + for(Transform t : webView.getTransforms()) { + if (t instanceof Scale) { useScale *= ((Scale)t).getX(); } + } + + PageLayout page = job.getJobSettings().getPageLayout(); + Rectangle printBounds = new Rectangle(0, 0, page.getPrintableWidth(), page.getPrintableHeight()); + log.debug("Paper area: {},{}:{},{}", (int)page.getLeftMargin(), (int)page.getTopMargin(), + (int)page.getPrintableWidth(), (int)page.getPrintableHeight()); + + Translate activePage = new Translate(); + webView.getTransforms().add(activePage); + + int columnsNeed = Math.max(1, (int)Math.ceil(webView.getWidth() / printBounds.getWidth() * useScale - 0.1)); + int rowsNeed = Math.max(1, (int)Math.ceil(webView.getHeight() / printBounds.getHeight() * useScale - 0.1)); + log.debug("Document will be printed across {} pages", columnsNeed * rowsNeed); + + try { + for(int row = 0; row < rowsNeed; row++) { + for(int col = 0; col < columnsNeed; col++) { + activePage.setX((-col * printBounds.getWidth()) / useScale); + activePage.setY((-row * printBounds.getHeight()) / useScale); + + job.printPage(webView); + } + } + + unlatch(null); + } + catch(Exception e) { + unlatch(e); + } + finally { + //reset state + webView.getTransforms().clear(); + } + }); + } + catch(Exception e) { unlatch(e); } + } + return frames >= VECTOR_FRAMES; + }); + + log.trace("Waiting on print.."); + captureLatch.await(); //released when unlatch is called + + if (thrown.get() != null) { throw thrown.get(); } + } + + public static synchronized BufferedImage raster(final WebAppModel model) throws Throwable { + AtomicReference capture = new AtomicReference<>(); + + //ensure JavaFX has started before we run + if (startupLatch.getCount() > 0) { + throw new IOException("JavaFX has not been started"); + } + + //raster still needs to show stage for valid capture + Platform.runLater(() -> { + stage.show(); + stage.toBack(); + }); + + raster = true; + + load(model, (int frames) -> { + if (frames == CAPTURE_FRAMES) { + log.debug("Attempting image capture"); + + Toolkit.getToolkit().addPostSceneTkPulseListener(new TKPulseListener() { + @Override + public void pulse() { + try { + // TODO: Revert to Callback once JDK-8244588/SUPQZ-5 is avail (JDK11+ only) + capture.set(SwingFXUtils.fromFXImage(webView.snapshot(null, null), null)); + unlatch(null); + } + catch(Exception e) { + unlatch(e); + } + finally { + Toolkit.getToolkit().removePostSceneTkPulseListener(this); + } + } + }); + Toolkit.getToolkit().requestNextPulse(); + } + + return frames >= CAPTURE_FRAMES; + }); + + log.trace("Waiting on capture.."); + captureLatch.await(); //released when unlatch is called + + if (thrown.get() != null) { throw thrown.get(); } + + return capture.get(); + } + + /** + * Prints the loaded source specified in the passed {@code model}. + * + * @param model The model specifying the web page parameters. + * @param action EventHandler that will be ran when the WebView completes loading. + */ + private static synchronized void load(WebAppModel model, IntPredicate action) { + captureLatch = new CountDownLatch(1); + thrown.set(null); + + Platform.runLater(() -> { + //zoom should only be factored on raster prints + pageZoom = model.getZoom(); + pageWidth = model.getWebWidth(); + pageHeight = model.getWebHeight(); + + log.trace("Setting starting size {}:{}", pageWidth, pageHeight); + adjustSize(pageWidth * pageZoom, pageHeight * pageZoom); + + if (pageHeight == 0) { + webView.setMinHeight(1); + webView.setPrefHeight(1); + webView.setMaxHeight(1); + } + + autosize(webView); + + printAction = action; + + if (model.isPlainText()) { + webView.getEngine().loadContent(model.getSource(), "text/html"); + } else { + webView.getEngine().load(model.getSource()); + } + }); + } + + private static double findHeight() { + String heightText = webView.getEngine().executeScript("Math.max(document.body.offsetHeight, document.body.scrollHeight)").toString(); + return Double.parseDouble(heightText); + } + + private static void adjustSize(double toWidth, double toHeight) { + webView.setMinSize(toWidth, toHeight); + webView.setPrefSize(toWidth, toHeight); + webView.setMaxSize(toWidth, toHeight); + } + + /** + * Fix blank page after autosize is called + */ + public static void autosize(WebView webView) { + webView.autosize(); + + if (!raster) { + // Call updatePeer; fixes a bug with webView resizing + // Can be avoided by calling stage.show() but breaks headless environments + // See: https://github.com/qzind/tray/issues/513 + String[] methods = {"impl_updatePeer" /*jfx8*/, "doUpdatePeer" /*jfx11*/}; + try { + for(Method m : webView.getClass().getDeclaredMethods()) { + for(String method : methods) { + if (m.getName().equals(method)) { + m.setAccessible(true); + m.invoke(webView); + return; + } + } + } + } + catch(SecurityException | ReflectiveOperationException e) { + log.warn("Unable to update peer; Blank pages may occur.", e); + } + } + } + + private static double calculateSupportedZoom(double width, double height) { + long memory = Runtime.getRuntime().maxMemory(); + int allowance = (memory / 1048576L) > 1024? 3:2; + if (headless) { allowance--; } + long availSpace = memory << allowance; + + // Memory needed for print is roughly estimated as + // (width * height) [pixels needed] * (pageZoom * 72d) [print density used] * 3 [rgb channels] + return Math.sqrt(availSpace / ((width * height) * (pageZoom * 72d) * 3)); + } + + /** + * Final cleanup when no longer capturing + */ + public static void unlatch(Throwable t) { + if (t != null) { + thrown.set(t); + } + + captureLatch.countDown(); + stage.hide(); + } + + public static Version getWebkitVersion() { + if(webkitVersion == null) { + if(webView != null) { + String userAgent = webView.getEngine().getUserAgent(); + String[] parts = userAgent.split("WebKit/"); + if (parts.length > 1) { + String[] split = parts[1].split(" "); + if (split.length > 0) { + try { + webkitVersion = Version.valueOf(split[0]); + log.info("WebKit version {} detected", webkitVersion); + } catch(Exception ignore) {} + } + } + if(webkitVersion == null) { + log.warn("WebKit version couldn't be parsed from UserAgent: {}", userAgent); + } + } else { + log.warn("Can't get WebKit version, JavaFX hasn't started yet."); + } + } + return webkitVersion; + } +} diff --git a/old code/tray/src/qz/printer/action/html/WebAppModel.java b/old code/tray/src/qz/printer/action/html/WebAppModel.java new file mode 100755 index 0000000..77aa370 --- /dev/null +++ b/old code/tray/src/qz/printer/action/html/WebAppModel.java @@ -0,0 +1,81 @@ +package qz.printer.action.html; + +public class WebAppModel { + + private String source; + private boolean plainText; + + private double width, webWidth; + private double height, webHeight; + private boolean isScaled; + private double zoom; + + public WebAppModel(String source, boolean plainText, double width, double height, boolean isScaled, double zoom) { + this.source = source; + this.plainText = plainText; + this.width = width; + this.height = height; + this.webWidth = width * (96d / 72d); + this.webHeight = height * (96d / 72d); + this.isScaled = isScaled; + this.zoom = zoom; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public boolean isPlainText() { + return plainText; + } + + public void setPlainText(boolean plainText) { + this.plainText = plainText; + } + + public double getWidth() { + return width; + } + + public void setWidth(double width) { + this.width = width; + this.webWidth = width * (96d / 72d); + } + + public double getHeight() { + return height; + } + + public void setHeight(double height) { + this.height = height; + this.webHeight = height * (96d / 72d); + } + + public double getWebWidth() { + return webWidth; + } + + public double getWebHeight() { + return webHeight; + } + + public boolean isScaled() { + return isScaled; + } + + public void setScaled(boolean scaled) { + isScaled = scaled; + } + + public double getZoom() { + return zoom; + } + + public void setZoom(double zoom) { + this.zoom = zoom; + } +} diff --git a/old code/tray/src/qz/printer/action/pdf/BookBundle.java b/old code/tray/src/qz/printer/action/pdf/BookBundle.java new file mode 100755 index 0000000..38fb382 --- /dev/null +++ b/old code/tray/src/qz/printer/action/pdf/BookBundle.java @@ -0,0 +1,80 @@ +package qz.printer.action.pdf; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.awt.print.Book; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.awt.print.PrinterException; + +/** + * Wrapper of the {@code Book} class as a {@code Printable} type, + * since PrinterJob implementations do not seem to handle the {@code Pageable} interface properly. + */ +public class BookBundle extends Book { + + private static final Logger log = LogManager.getLogger(BookBundle.class); + + private Printable lastPrint; + private int lastStarted; + + public BookBundle() { + super(); + } + + /** + * Wrapper of the wrapper class so that PrinterJob implementations will handle it as proper pageable + */ + public Book wrapAndPresent() { + Book cover = new Book(); + for(int i = 0; i < getNumberOfPages(); i++) { + cover.append(new PrintingPress(), getPageFormat(i)); + } + + return cover; + } + + public Book wrapAndPresent(int offset, int length) { + Book coverSubset = new Book(); + for(int i = offset; i < offset + length && i < getNumberOfPages(); i++) { + coverSubset.append(new PrintingPress(offset), getPageFormat(i)); + } + + return coverSubset; + } + + + /** Printable wrapper to ensure proper reading of multiple documents across spooling */ + private class PrintingPress implements Printable { + private int pageOffset; + + public PrintingPress() { + this(0); + } + + public PrintingPress(int offset) { + pageOffset = offset; + } + + @Override + public int print(Graphics g, PageFormat format, int pageIndex) throws PrinterException { + pageIndex += pageOffset; + log.trace("Requested page {} for printing", pageIndex); + + if (pageIndex < getNumberOfPages()) { + Printable printable = getPrintable(pageIndex); + if (printable != lastPrint) { + lastPrint = printable; + lastStarted = pageIndex; + } + + return printable.print(g, format, pageIndex - lastStarted); + } + + return NO_SUCH_PAGE; + } + } + +} diff --git a/old code/tray/src/qz/printer/action/pdf/PDFWrapper.java b/old code/tray/src/qz/printer/action/pdf/PDFWrapper.java new file mode 100755 index 0000000..42297a1 --- /dev/null +++ b/old code/tray/src/qz/printer/action/pdf/PDFWrapper.java @@ -0,0 +1,90 @@ +package qz.printer.action.pdf; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.printing.PDFPrintable; +import org.apache.pdfbox.printing.Scaling; +import org.apache.pdfbox.rendering.PDFRenderer; +import qz.printer.PrintOptions; +import qz.utils.SystemUtilities; + +import javax.print.attribute.standard.OrientationRequested; +import java.awt.*; +import java.awt.print.PageFormat; +import java.awt.print.Printable; +import java.awt.print.PrinterException; + +public class PDFWrapper implements Printable { + + private static final Logger log = LogManager.getLogger(PDFWrapper.class); + + private PDDocument document; + private Scaling scaling; + private OrientationRequested orientation = OrientationRequested.PORTRAIT; + + private PDFPrintable printable; + + public PDFWrapper(PDDocument document, Scaling scaling, boolean showPageBorder, boolean ignoreTransparency, boolean useAlternateFontRendering, float dpi, boolean center, PrintOptions.Orientation orientation, RenderingHints hints) { + this.document = document; + this.scaling = scaling; + if (orientation != null) { + this.orientation = orientation.getAsOrientRequested(); + } + + PDFRenderer renderer = new ParamPdfRenderer(document, useAlternateFontRendering, ignoreTransparency); + printable = new PDFPrintable(document, scaling, showPageBorder, dpi, center, renderer); + printable.setRenderingHints(hints); + } + + + @Override + public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException { + log.debug("Paper area: {},{}:{},{}", (int)pageFormat.getImageableX(), (int)pageFormat.getImageableY(), + (int)pageFormat.getImageableWidth(), (int)pageFormat.getImageableHeight()); + + graphics.drawString(" ", 0, 0); + + //reverse fix for OSX + if (SystemUtilities.isMac() && orientation == OrientationRequested.REVERSE_LANDSCAPE) { + adjustPrintForOrientation(graphics, pageFormat, pageIndex); + } + + return printable.print(graphics, pageFormat, pageIndex); + } + + private void adjustPrintForOrientation(Graphics g, PageFormat format, int page) { + PDRectangle bounds = document.getPage(page).getBBox(); + double docWidth = bounds.getWidth(); + double docHeight = bounds.getHeight(); + + //reports dimensions flipped if rotated + if (document.getPage(page).getRotation() % 180 == 90) { + docWidth = bounds.getHeight(); + docHeight = bounds.getWidth(); + } + + //adjust across page to account for wrong origin corner + double leftAdjust, topAdjust; + + if (scaling != Scaling.ACTUAL_SIZE) { + if ((docWidth / docHeight) >= (format.getImageableWidth() / format.getImageableHeight())) { + leftAdjust = 0; + topAdjust = format.getImageableHeight() - (docHeight / (docWidth / format.getImageableWidth())); + } else { + leftAdjust = format.getImageableWidth() - (docWidth / (docHeight / format.getImageableHeight())); + topAdjust = 0; + } + } else { + leftAdjust = format.getImageableWidth() - docWidth; + topAdjust = format.getImageableHeight() - docHeight; + } + + log.info("Adjusting image by {},{} for selected orientation", leftAdjust, topAdjust); + + //reverse landscape will have only rotated doc, this adjusts page so [0,0] appears to come from correct corner + g.translate((int)leftAdjust, (int)topAdjust); + } + +} diff --git a/old code/tray/src/qz/printer/action/pdf/ParamPdfRenderer.java b/old code/tray/src/qz/printer/action/pdf/ParamPdfRenderer.java new file mode 100755 index 0000000..4236e2f --- /dev/null +++ b/old code/tray/src/qz/printer/action/pdf/ParamPdfRenderer.java @@ -0,0 +1,47 @@ +package qz.printer.action.pdf; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.apache.pdfbox.rendering.PageDrawer; +import org.apache.pdfbox.rendering.PageDrawerParameters; +import qz.printer.rendering.OpaqueDrawObject; +import qz.printer.rendering.OpaqueGraphicStateParameters; +import qz.printer.rendering.PdfFontPageDrawer; + +import java.io.IOException; + +public class ParamPdfRenderer extends PDFRenderer { + + private boolean useAlternateFontRendering; + private boolean ignoreTransparency; + + public ParamPdfRenderer(PDDocument document, boolean useAlternateFontRendering, boolean ignoreTransparency) { + super(document); + + this.useAlternateFontRendering = useAlternateFontRendering; + this.ignoreTransparency = ignoreTransparency; + } + + @Override + protected PageDrawer createPageDrawer(PageDrawerParameters parameters) throws IOException { + if (useAlternateFontRendering) { + return new PdfFontPageDrawer(parameters, ignoreTransparency); + } else if(ignoreTransparency) { + return new OpaquePageDrawer(parameters); + } + // Fallback to default PageDrawer + return new PageDrawer(parameters); + } + + // override drawer to make use of customized draw object + private static class OpaquePageDrawer extends PageDrawer { + public OpaquePageDrawer(PageDrawerParameters parameters) throws IOException { + super(parameters); + + // Note: These must match PdfFontPageDrawer's ignoreTransparency condition + addOperator(new OpaqueDrawObject()); + addOperator(new OpaqueGraphicStateParameters()); + } + } +} + diff --git a/old code/tray/src/qz/printer/action/raw/ImageWrapper.java b/old code/tray/src/qz/printer/action/raw/ImageWrapper.java new file mode 100755 index 0000000..7ea4514 --- /dev/null +++ b/old code/tray/src/qz/printer/action/raw/ImageWrapper.java @@ -0,0 +1,801 @@ +/* + * + * Copyright (C) 2013 Tres Finocchiaro, QZ Industries + * Copyright (C) 2013 Antoni Ten Monro's + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.printer.action.raw; + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.ByteArrayBuilder; +import qz.exception.InvalidRawImageException; +import qz.utils.ByteUtilities; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URL; +import java.nio.charset.Charset; +import java.util.ArrayList; + +/** + * Abstract wrapper for images to be printed with thermal printers. + * + * @author Tres Finocchiaro + * @author Antoni Ten Monro's + *

+ * Changelog: + *

+ * 20130805 (Tres Finocchiaro) Merged Antoni's changes with original keeping + * Antoni's better instantiation, "black" pixel logic, removing class abstraction + * (uses LanguageType Enum switching instead for smaller codebase) + *

+ * 20130710 (Antoni Ten Monro's) Refactored the original, to have the + * actual implementation of the different ImageWrapper classes in derived + * classes, while leaving common functionality here. + * @author Oleg Morozov 02/21/2013 (via public domain) + * @author Tres Finocchiaro 10/01/2013 + */ +@SuppressWarnings("UnusedDeclaration") //Library class +public class ImageWrapper { + + private static final Logger log = LogManager.getLogger(ImageWrapper.class); + + /** + * Represents the CHECK_BLACK quantization method, where only fully black + * pixels are considered black when translating them to printer format. + */ + public static final int CHECK_BLACK = 0; + /** + * Represents the CHECK_LUMA quantization method, pixels are considered + * black if their luma is less than a set threshold. Transparent pixels, and + * pixels whose alpha channel is less than the threshold are considered not + * black. + */ + public static final int CHECK_LUMA = 1; + /** + * Represents the CHECK_ALPHA quantization method, pixels are considered + * black if their alpha is more than a set threshold. Color information is + * discarded. + */ + public static final int CHECK_ALPHA = 2; + + private int lumaThreshold = 127; + private boolean[] imageAsBooleanArray; //Image representation as an array of boolean, with true values representing imageAsBooleanArray dots + private int[] imageAsIntArray; //Image representation as an array of ints, with each bit representing a imageAsBooleanArray dot + private ByteArrayBuilder byteBuffer = new ByteArrayBuilder(); + private int alphaThreshold = 127; + private BufferedImage bufferedImage; + private LanguageType languageType; + private Charset charset = Charset.defaultCharset(); + private int imageQuantizationMethod = CHECK_LUMA; + private int xPos = 0; // X coordinate used for EPL2, CPCL. Irrelevant for ZPLII, ESC/POS, etc + private int yPos = 0; // Y coordinate used for EPL2, CPCL. Irrelevant for ZPLII, ESC/POS, etc + private String logoId = ""; // PGL only, the logo ID + private boolean igpDots = false; // PGL only, toggle IGP/PGL default resolution of 72dpi + private int dotDensity = 32; // Generally 32 = Single (normal) 33 = Double (higher res) for ESC/POS. Irrelevant for all other languages. + + private boolean legacyMode = false; // Use newlines for ESC/POS spacing; simulates <=2.0.11 behavior + + /** + * Creates a new + * ImageWrapper from a + * BufferedImage. + * + * @param bufferedImage The image to convert for thermal printing + */ + public ImageWrapper(BufferedImage bufferedImage, LanguageType languageType) { + this.bufferedImage = bufferedImage; + this.languageType = languageType; + log.info("Loading BufferedImage"); + log.info("Dimensions: {}x{}", bufferedImage.getWidth(), bufferedImage.getHeight()); + init(); + + if (languageType.requiresImageWidthValidated()) { + validateImageWidth(); + } + } + + /** + * Returns the luma threshold used for the CHECK_LUMA quantization method. + * Pixels that are more transparent than this, or that have a luma greater + * than this will be considered white. The threshold goes from 0 (black) to + * 255 (white). + * + * @return the current threshold + */ + public int getLumaThreshold() { + return lumaThreshold; + } + + /** + * Sets the luma threshold used for the CHECK_LUMA quantization method. + * Pixels that are more transparent than this, or that have a luma greater + * than this will be considered white. The threshold goes from 0 (black) to + * 255 (white). + * + * @param lumaThreshold the threshold to set + */ + public void setLumaThreshold(int lumaThreshold) { + this.lumaThreshold = lumaThreshold; + } + + /** + * Get the method used to convert the image to monochrome. Currently + * implemented methods are:

  • CHECK_BLACK: Pixels are + * considered black if and only if they are completely black and opaque + *
  • CHECK_LUMA: Pixels are considered black if and only if + * their luma is under a threshold, and their opacity is over a threshold. + * This threshold is set with + * setLumaThreshold
  • CHECK_ALPHA: Pixels are + * considered black if and only if their opacity (alpha) is over a + * threshold,. This threshold is set with + * setAlphaThreshold
+ *

+ * Default quantization method is + * CHECK_BLACK. + * + * @return the current quantization method + */ + public int getImageQuantizationMethod() { + return imageQuantizationMethod; + } + + /** + * Sets the method used to convert the image to monochrome. Currently + * implemented methods are:

  • CHECK_BLACK: Pixels are + * considered black if and only if they are completely black and opaque + *
  • CHECK_LUMA: Pixels are considered black if and only if + * their luma is under a threshold, and their opacity is over a threshold. + * This threshold is set with + * setLumaThreshold
  • CHECK_ALPHA: Pixels are + * considered black if and only if their opacity (alpha) is over a + * threshold,. This threshold is set with + * setAlphaThreshold
+ *

+ * Default (and fallback) quantization method is + * CHECK_BLACK. + * + * @param imageQuantizationMethod the quantization method to set + */ + public void setImageQuantizationMethod(int imageQuantizationMethod) { + this.imageQuantizationMethod = imageQuantizationMethod; + } + + /** + * Returns the transparency (alpha) threshold used for the CHECK_ALPHA + * quantization method. Pixels that are more transparent than this will be + * considered white. The threshold goes from 0 (fully transparent) to 255 + * (fully opaque) + * + * @return the current threshold + */ + public int getAlphaThreshold() { + return alphaThreshold; + } + + /** + * Sets the transparency (alpha) threshold used for the CHECK_ALPHA + * quantization method. Pixels that are more transparent than this will be + * considered white. The threshold goes from 0 (fully transparent) to 255 + * (fully opaque) + * + * @param alphaThreshold the Threshold to set + */ + public void setAlphaThreshold(int alphaThreshold) { + this.alphaThreshold = alphaThreshold; + } + + public int getDotDensity() { + return dotDensity; + } + + public void setDotDensity(int dotDensity) { + this.legacyMode = dotDensity < 0; + this.dotDensity = Math.abs(dotDensity); + } + + public void setLogoId(String logoId) { + this.logoId = logoId; + } + + public String getLogoId() { + return logoId; + } + + public void setIgpDots(boolean igpDots) { + this.igpDots = igpDots; + } + + public boolean getIgpDots() { + return igpDots; + } + + public int getxPos() { + return xPos; + } + + public void setxPos(int xPos) { + this.xPos = xPos; + } + + public int getyPos() { + return yPos; + } + + public void setyPos(int yPos) { + this.yPos = yPos; + } + + /** + * Tests if a given pixel should be black. Multiple quantization algorithms + * are available. The quantization method should be adjusted with + * setQuantizationMethod. Should an invalid value be set as the + * quantization method, CHECK_BLACK will be used + * + * @param rgbPixel the color of the pixel as defined in getRGB() + * @return true if the pixel should be black, false otherwise + */ + private boolean isBlack(int rgbPixel) { + Color color = new Color(rgbPixel, true); + + int r = color.getRed(); + int g = color.getGreen(); + int b = color.getBlue(); + int a = color.getAlpha(); + switch(getImageQuantizationMethod()) { + case CHECK_LUMA: + if (a < getLumaThreshold()) { + return false; // assume pixels that are less opaque than the luma threshold should be considered to be white + } + + int luma = ((r * 299) + (g * 587) + (b * 114)) / 1000; //luma formula + return luma < getLumaThreshold(); //pixels that have less luma than the threshold are black + case CHECK_ALPHA: + return a > getAlphaThreshold(); //pixels that are more opaque than the threshold are black + case CHECK_BLACK: //only fully black pixels are black + default: + return color.equals(Color.BLACK); //The default + + } + } + + /** + * Sets ImageAsBooleanArray. boolean is used instead of int for memory + * considerations. + */ + private boolean[] generateBlackPixels(BufferedImage bi) { + log.info("Converting image to monochrome"); + int h = bi.getHeight(); + int w = bi.getWidth(); + int[] rgbPixels = bi.getRGB(0, 0, w, h, null, 0, w); + + /* + * It makes most sense to have black pixels as 1's and white pixels + * as zero's, however some printer manufacturers had this reversed + * and used 0's for the black pixels. EPL is a common language that + * uses 0's for black pixels. + * See also: https://support.zebra.com/cpws/docs/eltron/gw_command.htm + */ + boolean[] pixels = new boolean[rgbPixels.length]; + for(int i = 0; i < rgbPixels.length; i++) { + pixels[i] = languageType.requiresImageOutputInverted() != isBlack(rgbPixels[i]); + } + + return pixels; + } + + /** + * Converts the internal representation of the image into an array of bytes, + * suitable to be sent to a raw printer. + * + * @return The raw bytes that compose the image + */ + private byte[] getBytes() { + log.info("Generating byte array"); + int[] ints = getImageAsIntArray(); + byte[] bytes = new byte[ints.length]; + for(int i = 0; i < ints.length; i++) { + bytes[i] = (byte)ints[i]; + } + + return bytes; + } + + private void generateIntArray() { + log.info("Packing bits"); + imageAsIntArray = new int[imageAsBooleanArray.length / 8]; + // Convert every eight zero's to a full byte, in decimal + for(int i = 0; i < imageAsIntArray.length; i++) { + for(int k = 0; k < 8; k++) { + imageAsIntArray[i] += (imageAsBooleanArray[8 * i + k]? 1:0) << 7 - k; + } + } + } + + /** + * Generates the EPL2 commands to print an image. One command is emitted per + * line of the image. This avoids issues with commands being too long. + * + * @return The commands to print the image as an array of bytes, ready to be + * sent to the printer + */ + public byte[] getImageCommand(JSONObject opt) throws InvalidRawImageException, UnsupportedEncodingException { + getByteBuffer().clear(); + + switch(languageType) { + case ESCP: + appendEpsonSlices(getByteBuffer()); + break; + case ZPL: + String zplHexAsString = ByteUtilities.getHexString(getImageAsIntArray()); + int byteLen = zplHexAsString.length() / 2; + int perRow = byteLen / getHeight(); + StringBuilder zpl = new StringBuilder("^GFA,") + .append(byteLen).append(",").append(byteLen).append(",") + .append(perRow).append(",").append(zplHexAsString); + + getByteBuffer().append(zpl, charset); + break; + case EPL: + StringBuilder epl = new StringBuilder("GW") + .append(getxPos()).append(",") + .append(getyPos()).append(",") + .append(getWidth() / 8).append(",") + .append(getHeight()).append(","); + + getByteBuffer().append(epl, charset).append(getBytes()).append(new byte[] {10}); + break; + case CPCL: + String cpclHexAsString = ByteUtilities.getHexString(getImageAsIntArray()); + StringBuilder cpcl = new StringBuilder("EG ") + .append(getWidth() / 8).append(" ") + .append(getHeight()).append(" ") + .append(getxPos()).append(" ") + .append(getyPos()).append(" ") + .append(cpclHexAsString); + + getByteBuffer().append(cpcl, charset).append(new byte[] {13, 10}); + break; + case EVOLIS: + try { + ArrayList cymkData = convertToCYMK(); + int precision = opt.optInt("precision", 128); + + // Y,M,C,K,O ribbon + generateRibbonData('y', precision, cymkData.get(1)); + generateRibbonData('m', precision, cymkData.get(2)); + generateRibbonData('c', precision, cymkData.get(0)); + + //K(black) and O(overlay) are always precision 2 + generateRibbonData('k', 2, cymkData.get(3)); + + if (opt.has("overlay")) { + try { generateRibbonData('o', 2, parseOverlay(opt.get("overlay"))); } + catch(Exception e) { + log.error("Failed to parse overlay data: {}", e.getMessage()); + } + } + } + catch(IOException ioe) { + throw new InvalidRawImageException(ioe.getMessage(), ioe); + } + + break; + case SBPL: + String sbplHexAsString = ByteUtilities.getHexString(getImageAsIntArray()); + StringBuilder sbpl = new StringBuilder("GH") + .append(String.format("%03d", getWidth() / 8)) + .append(String.format("%03d", getHeight() / 8)) + .append(sbplHexAsString); + + getByteBuffer().append(new byte[] {27}).append(sbpl, charset); + break; + case PGL: + if(logoId.isEmpty()) { + throw new InvalidRawImageException("Printronix graphics require a logoId"); + } + if(igpDots) { + // IGP images cannot exceed 240x252 + if(getWidth() > 240 || getHeight() > 252) { + throw new InvalidRawImageException("IGP dots is enabled; Size values HL/VL cannot exceed 240x252"); + } + } + + // igpDots: true: Use IGP standard 60dpi/72dpi graphics (removes ";DOTS" from raw command) + // igpDots: false: Use the printer's native resolution (appends ";DOTS" to raw command) + StringBuilder pgl = new StringBuilder("~LOGO;").append(logoId).append(";") + .append(getHeight()).append(";").append(getWidth()).append(igpDots ? ";DOTS" : "").append("\n") + .append(getImageAsPGLDots()) + .append("END").append("\n"); + + getByteBuffer().append(pgl, charset); + break; + default: + throw new InvalidRawImageException(languageType + " image conversion is not yet supported."); + } + + return getByteBuffer().getByteArray(); + } + + /** + * @return the width of the image + */ + public int getWidth() { + return bufferedImage.getWidth(); + } + + /** + * @return the height of the image + */ + public int getHeight() { + return bufferedImage.getHeight(); + } + + /** + * @return the image as an array of booleans + */ + private boolean[] getImageAsBooleanArray() { + return imageAsBooleanArray; + } + + /** + * Printronix format + * [line];[black dots range];[more black dots range][newline] + * e.g + * 1;1-12;19-22;38-39 + * + */ + private String getImageAsPGLDots() { + StringBuilder pglDots = new StringBuilder(); + int pixelIndex = 0; + for(int h = 1; h <= getHeight(); h++) { + StringBuilder line = new StringBuilder(); + + int start = -1; + int end = -1; + + for(int w = 1; w <= getWidth(); w++) { + if(imageAsBooleanArray[pixelIndex]) { + System.out.print("."); + if(start == -1) { + start = w; + } + } else { + System.out.print(" "); + if(start != -1) { + end = w - 1; + } + } + + // Handle trailing pixel + if(w == getWidth()) { + end = w; + } + + if(start != -1 && end != -1) { + if(start == end) { + // append a single dot + line.append(start).append(";"); + } else { + // append a range of dots + line.append(start).append("-").append(end).append(";"); + } + start = -1; + end = -1; + } + pixelIndex++; + } + System.out.print("\n"); + if(line.length() > 0) { + // Remove trailing ";" + if(line.charAt(line.length() -1) == ';') { + line.replace(line.length() - 1, line.length(), ""); + } + // Add line number + line.insert(0, h + ";"); + + // Add to final commands + pglDots.append(line).append("\n"); + } + + } + + return pglDots.toString(); + } + + /** + * @param imageAsBooleanArray the imageAsBooleanArray to set + */ + private void setImageAsBooleanArray(boolean[] imageAsBooleanArray) { + this.imageAsBooleanArray = imageAsBooleanArray; + } + + /** + * @return the imageAsIntArray + */ + private int[] getImageAsIntArray() { + return imageAsIntArray; + } + + /** + * @param imageAsIntArray the imageAsIntArray to set + */ + private void setImageAsIntArray(int[] imageAsIntArray) { + this.imageAsIntArray = imageAsIntArray; + } + + /** + * Initializes the ImageWrapper. This populates the internal structures with + * the data created from the original image. It is normally called by the + * constructor, but if for any reason you change the image contents (for + * example, if you resize the image), it must be initialized again prior to + * calling getImageCommand() + */ + private void init() { + log.info("Initializing Image Fields"); + setImageAsBooleanArray(generateBlackPixels(bufferedImage)); + generateIntArray(); + } + + public Charset getCharset() { + return charset; + } + + public void setCharset(Charset charset) { + this.charset = charset; + } + + /** + * @return the byteBuffer + */ + private ByteArrayBuilder getByteBuffer() { + return byteBuffer; + } + + /** + * @return the buffer + */ + private BufferedImage getBufferedImage() { + return bufferedImage; + } + + /** + * @param buffer the buffer to set + */ + private void setBufferedImage(BufferedImage buffer) { + bufferedImage = buffer; + } + + /** + * http://android-essential-devtopics.blogspot.com/2013/02/sending-bit-image-to-epson-printer.html + *

+ * Images are read as one long array of black or white pixels, as scanned top to bottom and left to right. + * Printer format needs this sent in height chunks in bytes (normally 3, for 24 pixels at a time) for each x position along a segment, + * and repeated for each segment of height over the byte limit. + * + * @param builder the ByteArrayBuilder to use + */ + private void appendEpsonSlices(ByteArrayBuilder builder) { + // set line height to the size of each chunk we will be sending + int segmentHeight = dotDensity > 1 ? 24 : (dotDensity == 1 ? 8 : 16); // height will be handled explicitly below if striping + // Impact printers (U220, etc) benefit from double-pass striping (odd/even) for higher quality (dotDensity = 1) + boolean stripe = dotDensity == 1; + int bytesNeeded = (dotDensity <= 1 || stripe)? 1:3; + + if(legacyMode) { + // Temporarily set line spacing to 24 dots + builder.append(new byte[] { 0x1B, 0x33, 24}); + } + + int offset = 0; // keep track of chunk offset currently being written + boolean zeroPass = true; // track if this segment get rewritten with 1 pixel offset, always true if not striping + + while(offset < getHeight()) { + // compute 2 byte value of the image width (documentation states width is 'nL' + ('nH' * 256)) + byte nL = (byte)((getWidth() % 256)); + byte nH = (byte)((getWidth() / 256)); + builder.append(new byte[] {0x1B, 0x2A, (byte)dotDensity, nL, nH}); + + for(int x = 0; x < getWidth(); x++) { + for(int bite = 0; bite < bytesNeeded; bite++) { + byte slice = 0; + + //iterate bit for the byte - striping spans 2 bytes (taking every other bit) to be compacted down into one + for(int bit = (zeroPass? 0:1); bit < 8 * (stripe? 2:1); bit += (stripe? 2:1)) { + // get the y position of the current pixel being found + int y = offset + ((bite * 8) + bit); + + // calculate the location of the pixel we want in the bit array and update the slice if it is supposed to be black + int i = (y * getWidth()) + x; + if (i < imageAsBooleanArray.length && imageAsBooleanArray[i]) { + // append desired bit to current byte being built, remembering that bits go right to left + slice |= (byte)(1 << (7 - (bit - (zeroPass? 0:1)) / (stripe? 2:1))); + } + } + + builder.append(new byte[] {slice}); + } + } + + // move print head down to next segment (or offset by one if striping) + if (stripe) { + if (zeroPass) { + builder.append(new byte[] {0x1B, 0x4A, 0x01}); // only shift down 1 pixel for the next pass + } else { + builder.append(new byte[] {0x1B, 0x4A, (byte)(segmentHeight - 1)}); // shift down remaining pixels + offset += 8 * bytesNeeded; // only shift offset on every other pass (along with segments) + } + + zeroPass = !zeroPass; + } else { + if(legacyMode) { + // render a newline to bump the print head down + builder.append(new byte[] {10}); + } else { + //shift down for next segment + builder.append(new byte[] {0x1B, 0x4A, (byte)segmentHeight}); + } + offset += 8 * bytesNeeded; + } + } + + if(legacyMode) { + // Restore line spacing to 30 dots + builder.append(new byte[] { 0x1B, 0x33, 30}); + } + } + + private ArrayList convertToCYMK() throws IOException { + int[] pixels = bufferedImage.getRGB(0, 0, getWidth(), getHeight(), null, 0, getWidth()); + + float[] cyan = new float[pixels.length]; + float[] yellow = new float[pixels.length]; + float[] magenta = new float[pixels.length]; + float[] black = new float[pixels.length]; + + for(int i = 0; i < pixels.length; i++) { + float rgb[] = new Color(pixels[i]).getRGBColorComponents(null); + if (rgb[0] == 0.0f && rgb[1] == 0.0f && rgb[2] == 0.0f) { + black[i] = 1.0f; + } else { + cyan[i] = 1.0f - rgb[0]; + magenta[i] = 1.0f - rgb[1]; + yellow[i] = 1.0f - rgb[2]; + } + } + + ArrayList colorData = new ArrayList<>(); + colorData.add(cyan); + colorData.add(yellow); + colorData.add(magenta); + colorData.add(black); + + return colorData; + } + + private float[] parseOverlay(Object overlay) throws IOException, JSONException { + float[] overlayData = new float[getWidth() * getHeight()]; + + if (overlay instanceof JSONArray) { + //array of rectangles + JSONArray masterBlock = (JSONArray)overlay; + for(int i = 0; i < masterBlock.length(); i++) { + JSONArray block = masterBlock.getJSONArray(i); + if (block != null && block.length() == 4) { + for(int y = block.getInt(1) - 1; y < block.getInt(3); y++) { + int off = (y * getWidth()); + for(int x = block.getInt(0) - 1; x < block.getInt(2); x++) { + if ((off + x) >= 0 && (off + x) < overlayData.length) { + overlayData[off + x] = 1.0f; + } + } + } + } + } + } else if (overlay instanceof String) { + //image mask + boolean[] mask = generateBlackPixels(ImageIO.read(new URL((String)overlay))); + for(int i = 0; i < overlayData.length; i++) { + overlayData[i] = (mask[i]? 1.0f:0.0f); + } + } else if (overlay instanceof Boolean && (boolean)overlay) { + //boolean coat + for(int i = 0; i < overlayData.length; i++) { + overlayData[i] = 1.0f; + } + } + + return overlayData; + } + + private void generateRibbonData(char ribbon, int precision, float[] colorData) throws UnsupportedEncodingException { + log.debug("Building ribbon 'Db;{};{};..'", ribbon, precision); + + getByteBuffer().append("\u001BDb;" + ribbon + ";" + precision + ";", charset); + getByteBuffer().append(compactBits(precision, colorData)); + getByteBuffer().append(new byte[] {0x0D}); + } + + private ArrayList compactBits(int precision, float[] colorData) { + ArrayList bytes = new ArrayList<>(); + + int bits = precisionBits(precision); + int empty = 8 - bits; + + for(int i = 0; i < colorData.length; i++) { + byte b = 0; + int captured = 0; + + b |= byteValue(colorData[i], precision) << empty; + captured += 8 - empty; + + while(captured < 8 && (i + 1) < colorData.length) { + int excess = bits - empty; + + if (excess > 0) { //because negative shifts don't go backwards + b |= byteValue(colorData[i + 1], precision) >> excess; + } else { + b |= byteValue(colorData[i + 1], precision) << Math.abs(excess); + } + captured += bits - Math.max(0, excess); + if (captured < 8 && excess <= 0) { i++; } //if we've eaten an entire color point but haven't filled the byte, increase index looking at + + empty = 8 - excess; + if (empty > 8) { empty -= 8; } //wrap around so we never shift over a byte length + } + + bytes.add(b); + } + + return bytes; + } + + private int precisionBits(int precision) { + precision--; // "128" is actually 0-127, subtract one + int ones = 0; + while(precision > 0) { + if (precision % 2 != 0) { ones++; } + precision /= 2; + } + + return ones; + } + + private byte byteValue(float value, int precision) { + return (byte)(value * (precision - 1)); + } + + /** + * Checks if the image width is a multiple of 8, and if it's not, + * pads the image on the right side with blank pixels.
+ * Due to limitations on the EPL2 language, image widths must be a multiple + * of 8. + */ + private void validateImageWidth() { + BufferedImage oldBufferedImage = bufferedImage; + int height = oldBufferedImage.getHeight(); + int width = oldBufferedImage.getWidth(); + if (width % 8 != 0) { + int newWidth = (width / 8 + 1) * 8; + BufferedImage newBufferedImage = new BufferedImage(newWidth, height, + BufferedImage.TYPE_INT_ARGB); + + Graphics2D g = newBufferedImage.createGraphics(); + g.drawImage(oldBufferedImage, 0, 0, null); + g.dispose(); + setBufferedImage(newBufferedImage); + init(); + } + } +} diff --git a/old code/tray/src/qz/printer/action/raw/LanguageType.java b/old code/tray/src/qz/printer/action/raw/LanguageType.java new file mode 100755 index 0000000..28e8a9e --- /dev/null +++ b/old code/tray/src/qz/printer/action/raw/LanguageType.java @@ -0,0 +1,84 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.printer.action.raw; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Enum for print languages, such as ZPL, EPL, etc. + * + * @author tfino + */ +public enum LanguageType { + + ZPL(false, true, 203, "ZPL", "ZPL2", "ZPLII", "ZEBRA"), + EPL(true, true, 203, "EPL", "EPL2", "EPLII"), + CPCL(false, true, 203), + ESCP(false, false, 180, "ESCP", "ESCP2", "ESCPOS", "ESC", "ESC/P", "ESC/P2", "ESCP/P2", "ESC/POS", "ESC\\P", "EPSON"), + EVOLIS(false, false, 300), + SBPL(false, true, 203, "SATO"), + PGL(false, false, 203, "IGP/PGL", "PRINTRONIX"), + UNKNOWN(false, false, 72); + + + private boolean imgOutputInvert = false; + private boolean imgWidthValidated = false; + private double defaultDensity = 72; + private List altNames; + + LanguageType(boolean imgOutputInvert, boolean imgWidthValidated, double defaultDensity, String... altNames) { + this.imgOutputInvert = imgOutputInvert; + this.imgWidthValidated = imgWidthValidated; + this.defaultDensity = defaultDensity; + + this.altNames = new ArrayList<>(); + Collections.addAll(this.altNames, altNames); + } + + public static LanguageType getType(String type) { + for(LanguageType lang : LanguageType.values()) { + if (lang.name().equalsIgnoreCase(type) || lang.altNames.contains(type)) { + return lang; + } + } + + return UNKNOWN; + } + + + /** + * Returns whether or not this {@code LanguageType} + * inverts the black and white pixels before sending to the printer. + * + * @return {@code true} if language type flips black and white pixels + */ + public boolean requiresImageOutputInverted() { + return imgOutputInvert; + } + + /** + * Returns whether or not the specified {@code LanguageType} requires + * the image width to be validated prior to processing output. This + * is required for image formats that normally require the image width to + * be a multiple of 8 + * + * @return {@code true} if the printer requires image width validation + */ + public boolean requiresImageWidthValidated() { + return imgWidthValidated; + } + + public double getDefaultDensity() { + return defaultDensity; + } + +} diff --git a/old code/tray/src/qz/printer/info/CachedPrintService.java b/old code/tray/src/qz/printer/info/CachedPrintService.java new file mode 100755 index 0000000..83408e3 --- /dev/null +++ b/old code/tray/src/qz/printer/info/CachedPrintService.java @@ -0,0 +1,133 @@ +package qz.printer.info; + +import qz.common.CachedObject; + +import javax.print.*; +import javax.print.attribute.Attribute; +import javax.print.attribute.AttributeSet; +import javax.print.attribute.PrintServiceAttribute; +import javax.print.attribute.PrintServiceAttributeSet; +import javax.print.event.PrintServiceAttributeListener; +import java.util.HashMap; +import java.util.function.Supplier; + +/** + * PrintService.getName() is slow, and gets increasingly slower the more times it's called. + * + * By overriding and caching the PrintService attributes, we're able to help suppress/delay the + * performance loss of this bug. + * + * See also JDK-7001133 + */ +public class CachedPrintService implements PrintService { + private final PrintService printService; + private final long lifespan; + private final CachedObject cachedName; + private final CachedObject cachedAttributeSet; + private final HashMap, CachedObject> cachedAttributes = new HashMap<>(); + + public CachedPrintService(PrintService printService, long lifespan) { + this.printService = printService; + this.lifespan = lifespan; + cachedName = new CachedObject<>(this.printService::getName, lifespan); + cachedAttributeSet = new CachedObject<>(this.printService::getAttributes, lifespan); + } + + public CachedPrintService(PrintService printService) { + this(printService, CachedObject.DEFAULT_LIFESPAN); + } + + @Override + public String getName() { + return cachedName.get(); + } + + @Override + public DocPrintJob createPrintJob() { + return printService.createPrintJob(); + } + + @Override + public void addPrintServiceAttributeListener(PrintServiceAttributeListener listener) { + printService.addPrintServiceAttributeListener(listener); + } + + @Override + public void removePrintServiceAttributeListener(PrintServiceAttributeListener listener) { + printService.removePrintServiceAttributeListener(listener); + } + + @Override + public PrintServiceAttributeSet getAttributes() { + return cachedAttributeSet.get(); + } + + public PrintService getJavaxPrintService() { + return printService; + } + + @Override + public T getAttribute(Class category) { + if (!cachedAttributes.containsKey(category)) { + Supplier supplier = () -> printService.getAttribute(category); + CachedObject cachedObject = new CachedObject<>(supplier, lifespan); + cachedAttributes.put(category, cachedObject); + } + return category.cast(cachedAttributes.get(category).get()); + } + + @Override + public DocFlavor[] getSupportedDocFlavors() { + return printService.getSupportedDocFlavors(); + } + + @Override + public boolean isDocFlavorSupported(DocFlavor flavor) { + return printService.isDocFlavorSupported(flavor); + } + + @Override + public Class[] getSupportedAttributeCategories() { + return printService.getSupportedAttributeCategories(); + } + + @Override + public boolean isAttributeCategorySupported(Class category) { + return printService.isAttributeCategorySupported(category); + } + + @Override + public Object getDefaultAttributeValue(Class category) { + return printService.getDefaultAttributeValue(category); + } + + @Override + public Object getSupportedAttributeValues(Class category, DocFlavor flavor, AttributeSet attributes) { + return printService.getSupportedAttributeValues(category, flavor, attributes); + } + + @Override + public boolean isAttributeValueSupported(Attribute attrval, DocFlavor flavor, AttributeSet attributes) { + return printService.isAttributeValueSupported(attrval, flavor, attributes); + } + + @Override + public AttributeSet getUnsupportedAttributes(DocFlavor flavor, AttributeSet attributes) { + return printService.getUnsupportedAttributes(flavor, attributes); + } + + @Override + public ServiceUIFactory getServiceUIFactory() { + return printService.getServiceUIFactory(); + } + + public boolean equals(Object obj) { + return (obj == this || + (obj instanceof PrintService && + ((PrintService)obj).getName().equals(getName()))); + } + + public int hashCode() { + return this.getClass().hashCode()+getName().hashCode(); + } +} diff --git a/old code/tray/src/qz/printer/info/CachedPrintServiceLookup.java b/old code/tray/src/qz/printer/info/CachedPrintServiceLookup.java new file mode 100755 index 0000000..248527b --- /dev/null +++ b/old code/tray/src/qz/printer/info/CachedPrintServiceLookup.java @@ -0,0 +1,81 @@ +package qz.printer.info; + +import qz.common.CachedObject; + +import javax.print.PrintService; +import javax.print.PrintServiceLookup; + +/** + * PrintService[] cache to workaround JDK-7001133 + * + * See also CachedPrintService + */ +public class CachedPrintServiceLookup { + private static final CachedObject cachedDefault = new CachedObject<>(CachedPrintServiceLookup::wrapDefaultPrintService); + private static final CachedObject cachedPrintServices = new CachedObject<>(CachedPrintServiceLookup::wrapPrintServices); + + // Keep CachedPrintService object references between calls to supplier + private static CachedPrintService[] cachedPrintServicesCopy = {}; + + static { + setLifespan(CachedObject.DEFAULT_LIFESPAN); + } + + public static PrintService lookupDefaultPrintService() { + return cachedDefault.get(); + } + + public static void setLifespan(long lifespan) { + cachedDefault.setLifespan(lifespan); + cachedPrintServices.setLifespan(lifespan); + } + + public static PrintService[] lookupPrintServices() { + return cachedPrintServices.get(); + } + + private static CachedPrintService wrapDefaultPrintService() { + PrintService javaxPrintService = PrintServiceLookup.lookupDefaultPrintService(); + // CachedObject's supplier returns null + if(javaxPrintService == null) { + return null; + } + // If this CachedPrintService already exists, reuse it rather than wrapping a new one + CachedPrintService cachedPrintService = getMatch(cachedPrintServicesCopy, javaxPrintService); + if (cachedPrintService == null) { + // Wrap a new one + cachedPrintService = new CachedPrintService(javaxPrintService); + } + return cachedPrintService; + } + + private static CachedPrintService[] wrapPrintServices() { + PrintService[] javaxPrintServices = PrintServiceLookup.lookupPrintServices(null, null); + CachedPrintService[] cachedPrintServices = new CachedPrintService[javaxPrintServices.length]; + for (int i = 0; i < javaxPrintServices.length; i++) { + // If this CachedPrintService already exists, reuse it rather than wrapping a new one + cachedPrintServices[i] = getMatch(cachedPrintServicesCopy, javaxPrintServices[i]); + if (cachedPrintServices[i] == null) { + cachedPrintServices[i] = new CachedPrintService(javaxPrintServices[i]); + } + } + cachedPrintServicesCopy = cachedPrintServices; + // Immediately invalidate the defaultPrinter cache + cachedDefault.get(true); + + return cachedPrintServices; + } + + private static CachedPrintService getMatch(CachedPrintService[] array, PrintService javaxPrintService) { + if(array != null) { + for(CachedPrintService cps : array) { + // Note: Order of operations can cause the defaultService pointer to differ if lookupDefaultPrintService() + // is called before lookupPrintServices(...) because the provider will invalidate on refreshServices() if + // "sun.java2d.print.polling" is set to "false". We're OK with this because worst case, we just + // call "lpstat" a second time. + if (cps.getJavaxPrintService() == javaxPrintService) return cps; + } + } + return null; + } +} diff --git a/old code/tray/src/qz/printer/info/CupsPrinterMap.java b/old code/tray/src/qz/printer/info/CupsPrinterMap.java new file mode 100755 index 0000000..25f74a9 --- /dev/null +++ b/old code/tray/src/qz/printer/info/CupsPrinterMap.java @@ -0,0 +1,192 @@ +package qz.printer.info; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import javax.print.PrintService; +import javax.print.attribute.standard.PrinterResolution; +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.util.*; + +public class CupsPrinterMap extends NativePrinterMap { + private static final String DEFAULT_CUPS_DRIVER = "TEXTONLY.ppd"; + private static final String AIRPRINT_DRIVER = "AirPrint"; + private static final Logger log = LogManager.getLogger(CupsPrinterMap.class); + private Map> resolutionMap = new HashMap<>(); + + public synchronized NativePrinterMap putAll(boolean exhaustive, PrintService... services) { + ArrayList missing = findMissing(exhaustive, services); + if (missing.isEmpty()) { return this; } + + String output = "\n" + ShellUtilities.executeRaw(new String[] {"lpstat", "-l", "-p"}); + String[] devices = output.split("[\\r\\n]printer "); + + for (String device : devices) { + if (device.trim().isEmpty()) { + continue; + } + NativePrinter printer = null; + String[] lines = device.split("\\r?\\n"); + for(String line : lines) { + line = line.trim(); + if (printer == null) { + printer = new NativePrinter(line.split("\\s+")[0]); + printer.getDescription().set(); + printer.getDriverFile().set(); + } else { + String match = "Description:"; + if (printer.getDescription().isNull() && line.startsWith(match)) { + printer.setDescription(line.substring(line.indexOf(match) + match.length()).trim()); + } + match = "Interface:"; + if (printer.getDriverFile().isNull() && line.startsWith(match)) { + printer.setDriverFile(line.substring(line.indexOf(match) + match.length()).trim()); + } + if (!printer.getDescription().isNull() && !printer.getDriverFile().isNull()) { + break; + } + } + } + + for (PrintService service : missing) { + if ((SystemUtilities.isMac() && printer.getDescription().equals(service.getName())) + || (SystemUtilities.isLinux() && printer.getPrinterId().equals(service.getName()))) { + printer.setPrintService(service); + missing.remove(service); + break; + } + } + + if (!printer.getPrintService().isNull()) { + put(printer.getPrinterId(), printer); + } + } + return this; + } + + synchronized void addResolution(NativePrinter printer, PrinterResolution resolution) { + List resolutions = resolutionMap.get(printer); + if(resolutions == null) { + resolutions = new ArrayList<>(); + resolutionMap.put(printer, resolutions); + } + if(!resolutions.contains(resolution)) { + resolutions.add(resolution); + } + } + + synchronized List getResolutions(NativePrinter printer) { + if(resolutionMap.get(printer) == null) { + fillAttributes(printer); + } + return resolutionMap.get(printer); + } + + @Override + public boolean remove(Object key, Object value) { + if(value instanceof NativePrinter) { + resolutionMap.remove(value); + } + return super.remove(key, value); + } + + /** + * Parse "*DefaultResolution" line from CUPS .ppd file + * @param line + * @return + */ + public static PrinterResolution parseDefaultResolution(String line) { + try { + String[] parts = line.split("x"); + int cross = Integer.parseInt(parts[0].replaceAll("\\D+", "")); + int feed = parts.length > 1? Integer.parseInt(parts[1].replaceAll("\\D+", "")):cross; + int type = line.toLowerCase(Locale.ENGLISH).contains("dpi")? PrinterResolution.DPI:PrinterResolution.DPCM; + return new PrinterResolution(cross, feed, type); + } catch(NumberFormatException nfe) { + log.warn("Could not parse density from \"{}\"", line); + } + return null; + } + + /** + * Parse "/HWResolution[" line from CUPS .ppd file + * @param line + * @return + */ + public static PrinterResolution parseAdditionalResolution(String line) { + try { + String[] parts = line.split("/HWResolution\\[")[1].split("\\D"); // split on non-digits + int cross = Integer.parseInt(parts[0]); + int feed = parts.length > 1? Integer.parseInt(parts[1]) : cross; + return new PrinterResolution(cross, feed, PrinterResolution.DPI); // always dpi per https://www.cups.org/doc/spec-ppd.html + } catch(NumberFormatException nfe) { + log.warn("Could not parse density from \"{}\"", line, nfe); + } + return null; + } + + synchronized void fillAttributes(NativePrinter printer) { + String options = ShellUtilities.executeRaw("lpoptions", "-p", printer.getPrinterId()); + String connection = null; + int start; + int end; + String section; + if((start = options.indexOf("device-uri=")) != -1) { + section = options.substring(start); + if((end = section.indexOf(' ')) > 0) { + connection = section.substring(section.indexOf("=") + 1, end); + } else { + connection = section.substring(section.indexOf("=") + 1); + } + } + printer.setConnection(connection); + + if (!printer.getDriverFile().isNull()) { + File ppdFile = new File(printer.getDriverFile().value()); + try { + BufferedReader buffer = new BufferedReader(new FileReader(ppdFile)); + String line; + + while((line = buffer.readLine()) != null) { + if (line.contains("*DefaultResolution:")) { + // Parse default printer resolution + PrinterResolution defaultRes = parseDefaultResolution(line); + if(defaultRes != null) { + printer.setResolution(defaultRes); + addResolution(printer, defaultRes); + } + } else if(line.contains("/HWResolution[")) { + PrinterResolution additionalRes = parseAdditionalResolution(line); + if(additionalRes != null) { + addResolution(printer, additionalRes); + } + } else if(line.contains("*APAirPrint:")) { + // Detect AirPrint driver + String[] split = line.split("\\*APAirPrint:"); + String value = split[split.length - 1].replace("\"", "").trim(); + if(Boolean.parseBoolean(value)) { + printer.setDriver(AIRPRINT_DRIVER); + } + } else if(printer.getDriver().isNull() && line.contains("*PCFileName:")) { + // Parse driver name if not found through other means + String[] split = line.split("\\*PCFileName:"); + printer.setDriver(split[split.length - 1].replace("\"", "").trim()); + } + } + } catch(IOException e) { + log.error("Something went wrong while reading " + printer.getDriverFile()); + } + } + if (printer.getDriver().isNull()) { + printer.setDriver(DEFAULT_CUPS_DRIVER); + } + if(resolutionMap.get(printer) == null) { + addResolution(printer, null); // create empty list + } + } +} diff --git a/old code/tray/src/qz/printer/info/NativePrinter.java b/old code/tray/src/qz/printer/info/NativePrinter.java new file mode 100755 index 0000000..9c663e2 --- /dev/null +++ b/old code/tray/src/qz/printer/info/NativePrinter.java @@ -0,0 +1,209 @@ +package qz.printer.info; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.SystemUtilities; + +import javax.print.PrintService; +import javax.print.attribute.standard.Media; +import javax.print.attribute.standard.PrinterName; +import javax.print.attribute.standard.PrinterResolution; +import java.util.Arrays; +import java.util.List; + +public class NativePrinter { + private static final Logger log = LogManager.getLogger(NativePrinter.class); + /** + * Simple object wrapper allowing lazy fetching of values + * @param + */ + public class PrinterProperty { + T value; + boolean set; + + public PrinterProperty() { + this.set = false; + } + + @Override + public String toString() { + if (value == null) { + return null; + } else if (value instanceof String) { + return (String)value; + } return value.toString(); + } + + public void set() { + this.set = true; + } + + public void set(T content) { + this.value = content; + this.set = true; + } + + public T value() { + return value; + } + + public boolean isSet() { + return set; + } + + public boolean isNull() { + return value == null; + } + + @Override + public boolean equals(Object o) { + if (value != null) { + return value.equals(o); + } + return false; + } + } + + private final String printerId; + + private boolean outdated; + private PrinterProperty description; + private PrinterProperty printService; + private PrinterProperty driver; + private PrinterProperty connection; + private PrinterProperty resolution; + private PrinterProperty driverFile; + + public NativePrinter(String printerId) { + this.printerId = printerId; + this.description = new PrinterProperty<>(); + this.printService = new PrinterProperty<>(); + this.driverFile = new PrinterProperty<>(); + this.driver = new PrinterProperty<>(); + this.connection = new PrinterProperty<>(); + this.resolution = new PrinterProperty<>(); + this.outdated = false; + } + + public PrinterProperty getDescription() { + return description; + } + + public void setDescription(String description) { + this.description.set(description); + } + + public PrinterProperty getDriverFile() { + return driverFile; + } + + public void setDriverFile(String driverFile) { + this.driverFile.set(driverFile); + } + + public PrinterProperty getDriver() { + if (!driver.isSet()) { + getDriverAttributes(this); + } + return driver; + } + + public String getName() { + if (printService != null && printService.value() != null) { + return printService.value().getName(); + } + return null; + } + + public PrinterName getLegacyName() { + if (printService != null && printService.value() != null) { + return printService.value().getAttribute(PrinterName.class); + } + return null; + } + + public void setDriver(String driver) { + this.driver.set(driver); + } + + public void setConnection(String connection) { + this.connection.set(connection); + } + + public String getConnection() { + if (!connection.isSet()) { + getDriverAttributes(this); + } + return connection.value(); + } + + public PrinterProperty getPrintService() { + return printService; + } + + public void setPrintService(PrintService printService) { + this.printService.set(printService); + } + + public String getPrinterId() { + return printerId; + } + + public void setResolution(PrinterResolution resolution) { + this.resolution.set(resolution); + } + + public PrinterProperty getResolution() { + if (!resolution.isSet()) { + // Fetch resolution, if available + try { + Object resolution = printService.value().getDefaultAttributeValue(PrinterResolution.class); + if (resolution != null) { + this.resolution.set((PrinterResolution)resolution); + } + } + catch(IllegalArgumentException e) { + log.warn("Unable to obtain PrinterResolution from {}", printService.value().getName(), e); + } + } + + return resolution; + } + + public List getResolutions() { + PrintService ps = getPrintService().value(); + PrinterResolution[] resSupport = (PrinterResolution[])ps.getSupportedAttributeValues(PrinterResolution.class, ps.getSupportedDocFlavors()[0], null); + if (resSupport == null || resSupport.length == 0) { + NativePrinterMap printerMap = NativePrinterMap.getInstance(); + // CUPS doesn't report resolutions properly, instead return the values scraped from console + if(printerMap instanceof CupsPrinterMap) { + return ((CupsPrinterMap)printerMap).getResolutions(this); + } + resSupport = new PrinterResolution[]{ getResolution().value() }; + } + + return Arrays.asList(resSupport); + } + + public static void getDriverAttributes(NativePrinter printer) { + // First, perform slow JDK operations, see issues #940, #932 + printer.getPrintService().value().getSupportedAttributeValues(Media.class, null, null); // cached by JDK + printer.getResolution(); + + // Mark properties as "found" so we don't attempt to gather them again + printer.driver.set(); + printer.resolution.set(); + printer.connection.set(); + + // Gather properties not exposed by the JDK + NativePrinterMap.getInstance().fillAttributes(printer); + } + + public boolean isOutdated() { + return outdated; + } + + public void setOutdated(boolean outdated) { + this.outdated = outdated; + } +} diff --git a/old code/tray/src/qz/printer/info/NativePrinterMap.java b/old code/tray/src/qz/printer/info/NativePrinterMap.java new file mode 100755 index 0000000..6f0e208 --- /dev/null +++ b/old code/tray/src/qz/printer/info/NativePrinterMap.java @@ -0,0 +1,96 @@ +package qz.printer.info; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.SystemUtilities; + +import javax.print.PrintService; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public abstract class NativePrinterMap extends ConcurrentHashMap { + private static final Logger log = LogManager.getLogger(NativePrinterMap.class); + + private static NativePrinterMap instance; + + public abstract NativePrinterMap putAll(boolean exhaustive, PrintService... services); + + abstract void fillAttributes(NativePrinter printer); + + public static NativePrinterMap getInstance() { + if (instance == null) { + switch(SystemUtilities.getOs()) { + case WINDOWS: + instance = new WindowsPrinterMap(); + break; + default: + instance = new CupsPrinterMap(); + } + } + return instance; + } + + public String lookupPrinterId(String description) { + for(Map.Entry entry : entrySet()) { + NativePrinter info = entry.getValue(); + if (description.equals(info.getPrintService().value().getName())) { + return entry.getKey(); + } + } + log.warn("Could not find printerId for " + description); + return null; + } + + /** + * WARNING: Despite the function's name, if exhaustive is true, it will treat the listing as exhaustive and remove + * any PrintServices that are not part of this HashMap. + */ + public ArrayList findMissing(boolean exhaustive, PrintService[] services) { + ArrayList serviceList = new ArrayList<>(Arrays.asList(services)); // shrinking list drastically improves performance + + for(NativePrinter printer : values()) { + int index = serviceList.indexOf(printer.getPrintService()); + if (index >= 0) { + // Java's `PrintService.equals(o)` method uses getName().equals(). This causes issues if a stale PrintService has been replaced + // by a new PrintService of the same name. For that reason, we always refresh the PrintService reference in NativePrinter. + // See: https://github.com/qzind/tray/issues/1259 + printer.setPrintService(serviceList.get(index)); + serviceList.remove(printer.getPrintService()); // existing match + } else { + if(exhaustive) { + printer.setOutdated(true); // no matches, mark to be removed + } + } + } + + // remove outdated + for (Map.Entry entry : entrySet()) { + if(entry.getValue().isOutdated()) { + remove(entry.getKey()); + } + } + + // any remaining services are new/missing + return serviceList; + } + + public boolean contains(PrintService service) { + for (NativePrinter printer : values()) { + if (printer.getPrintService().equals(service)) { + return true; + } + } + return false; + } + + public NativePrinter get(PrintService service) { + for (NativePrinter printer : values()) { + if (printer.getPrintService().equals(service)) { + return printer; + } + } + return null; + } +} diff --git a/old code/tray/src/qz/printer/info/WindowsPrinterMap.java b/old code/tray/src/qz/printer/info/WindowsPrinterMap.java new file mode 100755 index 0000000..628315b --- /dev/null +++ b/old code/tray/src/qz/printer/info/WindowsPrinterMap.java @@ -0,0 +1,49 @@ +package qz.printer.info; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.WindowsUtilities; + +import javax.print.PrintService; + +import static com.sun.jna.platform.win32.WinReg.*; + +public class WindowsPrinterMap extends NativePrinterMap { + private static final Logger log = LogManager.getLogger(WindowsPrinterMap.class); + + public synchronized NativePrinterMap putAll(boolean exhaustive, PrintService... services) { + for(PrintService service : findMissing(exhaustive, services)) { + String name = service.getName(); + if (name.equals("PageManager PDF Writer")) { + log.warn("Printer \"{}\" is blacklisted, removing", name); // Per https://github.com/qzind/tray/issues/599 + continue; + } + NativePrinter printer = new NativePrinter(name); + printer.setDescription(name); + printer.setPrintService(service); + put(printer.getPrinterId(), printer); + } + return this; + } + + synchronized void fillAttributes(NativePrinter printer) { + String keyName = printer.getPrinterId().replaceAll("\\\\", ","); + String key = "SYSTEM\\CurrentControlSet\\Control\\Print\\Printers\\" + keyName; + String driver = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "Printer Driver"); + String port = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "Port"); + if (driver == null) { + key = "Printers\\Connections\\" + keyName; + String guid = WindowsUtilities.getRegString(HKEY_CURRENT_USER, key, "GuidPrinter"); + if (guid != null) { + String serverName = keyName.replaceAll(",,(.+),.+", "$1"); + key = "Software\\Microsoft\\Windows NT\\CurrentVersion\\Print\\Providers\\Client Side Rendering Print Provider\\Servers\\" + serverName + "\\Printers\\" + guid; + driver = WindowsUtilities.getRegString(HKEY_LOCAL_MACHINE, key, "Printer Driver"); + // In testing, "Port" simply pointed back to the guid; assume "DsSpooler\portName" instead + port = StringUtils.join(WindowsUtilities.getRegMultiString(HKEY_LOCAL_MACHINE, key + "\\DsSpooler", "portName")); + } + } + printer.setDriver(driver); + printer.setConnection(port); + } +} diff --git a/old code/tray/src/qz/printer/rendering/FontManager.java b/old code/tray/src/qz/printer/rendering/FontManager.java new file mode 100755 index 0000000..8dc1829 --- /dev/null +++ b/old code/tray/src/qz/printer/rendering/FontManager.java @@ -0,0 +1,248 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package qz.printer.rendering; + +import java.awt.*; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Properties; + +/** + * FontManager class pulled from PDFBOX 1.8 + * with the help of Alexander Scherbatiy + */ + +public class FontManager { + // HashMap with all known fonts + private static HashMap envFonts = new HashMap<>(); + private static Properties fontMapping = new Properties(); + + static { + loadFonts(); + loadBasefontMapping(); + loadFontMapping(); + } + + private FontManager() {} + + /** + * Get the font for the given fontname. + * + * @param font The name of the font. + * @return The font we are looking for or a similar font or null if nothing is found. + */ + public static java.awt.Font getAwtFont(String font) { + String fontname = normalizeFontname(font); + if (envFonts.containsKey(fontname)) { + return envFonts.get(fontname); + } + + return null; + } + + /** + * Load all available fonts from the environment. + */ + private static void loadFonts() { + for(Font font : GraphicsEnvironment.getLocalGraphicsEnvironment().getAllFonts()) { + String family = normalizeFontname(font.getFamily()); + String psname = normalizeFontname(font.getPSName()); + + if (isBoldItalic(font)) { + envFonts.put(family + "bolditalic", font); + } else if (isBold(font)) { + envFonts.put(family + "bold", font); + } else if (isItalic(font)) { + envFonts.put(family + "italic", font); + } else { + envFonts.put(family, font); + } + + if (!family.equals(psname)) { + envFonts.put(normalizeFontname(font.getPSName()), font); + } + } + } + + /** + * Normalize the fontname. + * + * @param fontname The name of the font. + * @return The normalized name of the font. + */ + private static String normalizeFontname(String fontname) { + // Terminate all whitespaces, commas and hyphens + String normalizedFontname = fontname.toLowerCase().replaceAll(" ", "").replaceAll(",", "").replaceAll("-", ""); + // Terminate trailing characters up to the "+". + // As far as I know, these characters are used in names of embedded fonts + // If the embedded font can't be read, we'll try to find it here + if (normalizedFontname.contains("+")) { + normalizedFontname = normalizedFontname.substring(normalizedFontname.indexOf("+") + 1); + } + // normalize all kinds of fonttypes. There are several possible version which have to be normalized + // e.g. Arial,Bold Arial-BoldMT Helevtica-oblique ... + boolean isBold = normalizedFontname.contains("bold"); + boolean isItalic = normalizedFontname.contains("italic") || normalizedFontname.contains("oblique"); + normalizedFontname = normalizedFontname.toLowerCase().replaceAll("bold", "") + .replaceAll("italic", "").replaceAll("oblique", ""); + if (isBold) { + normalizedFontname += "bold"; + } + if (isItalic) { + normalizedFontname += "italic"; + } + + return normalizedFontname; + } + + + /** + * Add a font-mapping. + * + * @param font The name of the font. + * @param mappedName The name of the mapped font. + */ + private static boolean addFontMapping(String font, String mappedName) { + String fontname = normalizeFontname(font); + // is there already a font mapping ? + if (envFonts.containsKey(fontname)) { + return false; + } + String mappedFontname = normalizeFontname(mappedName); + // is the mapped font available ? + if (!envFonts.containsKey(mappedFontname)) { + return false; + } + envFonts.put(fontname, envFonts.get(mappedFontname)); + return true; + } + + /** + * Load the mapping for the well knwon font-substitutions. + */ + private static void loadFontMapping() { + boolean addedMapping = true; + // There could be some recursive mappings in the fontmapping, so that we have to + // read the list until no more additional mapping is added to it + while(addedMapping) { + int counter = 0; + Enumeration keys = fontMapping.keys(); + while(keys.hasMoreElements()) { + String key = (String)keys.nextElement(); + if (addFontMapping(key, (String)fontMapping.get(key))) { + counter++; + } + } + if (counter == 0) { + addedMapping = false; + } + } + } + + /** + * Mapping for the basefonts. + */ + private static void loadBasefontMapping() { + // use well known substitutions if the environments doesn't provide native fonts for the 14 standard fonts + // Times-Roman -> Serif + if (!addFontMapping("Times-Roman", "TimesNewRoman")) { + addFontMapping("Times-Roman", "Serif"); + } + if (!addFontMapping("Times-Bold", "TimesNewRoman,Bold")) { + addFontMapping("Times-Bold", "Serif.bold"); + } + if (!addFontMapping("Times-Italic", "TimesNewRoman,Italic")) { + addFontMapping("Times-Italic", "Serif.italic"); + } + if (!addFontMapping("Times-BoldItalic", "TimesNewRoman,Bold,Italic")) { + addFontMapping("Times-BoldItalic", "Serif.bolditalic"); + } + // Helvetica -> SansSerif + if (!addFontMapping("Helvetica", "Helvetica")) { + addFontMapping("Helvetica", "SansSerif"); + } + if (!addFontMapping("Helvetica-Bold", "Helvetica,Bold")) { + addFontMapping("Helvetica-Bold", "SansSerif.bold"); + } + if (!addFontMapping("Helvetica-Oblique", "Helvetica,Italic")) { + addFontMapping("Helvetica-Oblique", "SansSerif.italic"); + } + if (!addFontMapping("Helvetica-BoldOblique", "Helvetica,Bold,Italic")) { + addFontMapping("Helvetica-BoldOblique", "SansSerif.bolditalic"); + } + // Courier -> Monospaced + if (!addFontMapping("Courier", "Courier")) { + addFontMapping("Courier", "Monospaced"); + } + if (!addFontMapping("Courier-Bold", "Courier,Bold")) { + addFontMapping("Courier-Bold", "Monospaced.bold"); + } + if (!addFontMapping("Courier-Oblique", "Courier,Italic")) { + addFontMapping("Courier-Oblique", "Monospaced.italic"); + } + if (!addFontMapping("Courier-BoldOblique", "Courier,Bold,Italic")) { + addFontMapping("Courier-BoldOblique", "Monospaced.bolditalic"); + } + // some well known (??) substitutions found on fedora linux + addFontMapping("Symbol", "StandardSymbolsL"); + addFontMapping("ZapfDingbats", "Dingbats"); + } + + /** + * Try to determine if the font has both a BOLD and an ITALIC-type. + * + * @param font The font. + * @return font has BOLD and ITALIC-type or not + */ + private static boolean isBoldItalic(java.awt.Font font) { + return isBold(font) && isItalic(font); + } + + /** + * Try to determine if the font has a BOLD-type. + * + * @param font The font. + * @return font has BOLD-type or not + */ + private static boolean isBold(java.awt.Font font) { + String name = font.getName().toLowerCase(); + if (name.contains("bold")) { + return true; + } + + String psname = font.getPSName().toLowerCase(); + return psname.contains("bold"); + } + + /** + * Try to determine if the font has an ITALIC-type. + * + * @param font The font. + * @return font has ITALIC-type or not + */ + private static boolean isItalic(java.awt.Font font) { + String name = font.getName().toLowerCase(); + // oblique is the same as italic + if (name.contains("italic") || name.contains("oblique")) { + return true; + } + + String psname = font.getPSName().toLowerCase(); + return psname.contains("italic") || psname.contains("oblique"); + } + +} diff --git a/old code/tray/src/qz/printer/rendering/OpaqueDrawObject.java b/old code/tray/src/qz/printer/rendering/OpaqueDrawObject.java new file mode 100755 index 0000000..2b73b0f --- /dev/null +++ b/old code/tray/src/qz/printer/rendering/OpaqueDrawObject.java @@ -0,0 +1,60 @@ +package qz.printer.rendering; + +import org.apache.pdfbox.contentstream.operator.MissingOperandException; +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.contentstream.operator.graphics.GraphicsOperatorProcessor; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.MissingResourceException; +import org.apache.pdfbox.pdmodel.graphics.PDXObject; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; + +import java.io.IOException; +import java.util.List; + +// override draw object to remove any calls to show transparency +public class OpaqueDrawObject extends GraphicsOperatorProcessor { + + public OpaqueDrawObject() { } + + public void process(Operator operator, List operands) throws IOException { + if (operands.isEmpty()) { + throw new MissingOperandException(operator, operands); + } else { + COSBase base0 = operands.get(0); + if (base0 instanceof COSName) { + COSName objectName = (COSName)base0; + PDXObject xobject = context.getResources().getXObject(objectName); + + if (xobject == null) { + throw new MissingResourceException("Missing XObject: " + objectName.getName()); + } else { + if (xobject instanceof PDImageXObject) { + PDImageXObject image = (PDImageXObject)xobject; + context.drawImage(image); + } else if (xobject instanceof PDFormXObject) { + try { + context.increaseLevel(); + if (context.getLevel() <= 25) { + PDFormXObject form = (PDFormXObject)xobject; + context.showForm(form); + } + + //LOG.error("recursion is too deep, skipping form XObject"); + } + finally { + context.decreaseLevel(); + } + } + + } + } + } + } + + public String getName() { + return "Do"; + } + +} diff --git a/old code/tray/src/qz/printer/rendering/OpaqueGraphicStateParameters.java b/old code/tray/src/qz/printer/rendering/OpaqueGraphicStateParameters.java new file mode 100755 index 0000000..f7532e7 --- /dev/null +++ b/old code/tray/src/qz/printer/rendering/OpaqueGraphicStateParameters.java @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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. + */ +package qz.printer.rendering; + +import java.io.IOException; +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.pdfbox.contentstream.operator.OperatorProcessor; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; +import org.apache.pdfbox.contentstream.operator.Operator; +import org.apache.pdfbox.contentstream.operator.OperatorName; +import org.apache.pdfbox.contentstream.PDFStreamEngine; +import org.apache.pdfbox.contentstream.operator.MissingOperandException; + +/** + * gs: Set parameters from graphics state parameter dictionary. + * + * @author Ben Litchfield + */ +public class OpaqueGraphicStateParameters extends OperatorProcessor +{ + private static final Log LOG = LogFactory.getLog(OpaqueGraphicStateParameters.class); + + @Override + public void process(Operator operator, List arguments) throws IOException + { + if (arguments.isEmpty()) + { + throw new MissingOperandException(operator, arguments); + } + COSBase base0 = arguments.get(0); + if (!(base0 instanceof COSName)) + { + return; + } + + // set parameters from graphics state parameter dictionary + COSName graphicsName = (COSName) base0; + PDFStreamEngine context = getContext(); + PDExtendedGraphicsState gs = context.getResources().getExtGState(graphicsName); + if (gs == null) + { + LOG.error("name for 'gs' operator not found in resources: /" + graphicsName.getName()); + return; + } + + // PDFBOX-5605: Disable alpha for lines, etc + gs.setNonStrokingAlphaConstant(1f); + gs.setStrokingAlphaConstant(1f); + + gs.copyIntoGraphicsState( context.getGraphicsState() ); + } + + @Override + public String getName() + { + return OperatorName.SET_GRAPHICS_STATE_PARAMS; + } +} \ No newline at end of file diff --git a/old code/tray/src/qz/printer/rendering/PdfFontPageDrawer.java b/old code/tray/src/qz/printer/rendering/PdfFontPageDrawer.java new file mode 100755 index 0000000..cfb9694 --- /dev/null +++ b/old code/tray/src/qz/printer/rendering/PdfFontPageDrawer.java @@ -0,0 +1,198 @@ +package qz.printer.rendering; + + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.pdfbox.pdmodel.common.PDStream; +import org.apache.pdfbox.pdmodel.font.*; +import org.apache.pdfbox.rendering.PageDrawer; +import org.apache.pdfbox.rendering.PageDrawerParameters; +import org.apache.pdfbox.util.Matrix; +import org.apache.pdfbox.util.Vector; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * PageDrawer overrides derived from PDFBOX 1.8 + * with the help of Alexander Scherbatiy + */ + +public class PdfFontPageDrawer extends PageDrawer { + + private static final Logger log = LogManager.getLogger(PdfFontPageDrawer.class); + + private String fallbackFont = "helvetica"; //todo - definable parameter? + private final Map fonts = new HashMap<>(); + + public PdfFontPageDrawer(PageDrawerParameters parameters, boolean ignoresTransparency) throws IOException { + super(parameters); + + if (ignoresTransparency) { + // Note: These must match ParamPdfRenderer's OpaquePageDrawer + addOperator(new OpaqueDrawObject()); + addOperator(new OpaqueGraphicStateParameters()); + } + } + + @Override + protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vector displacement) throws IOException { + // fall-back to draw Glyph when awt font has not been found + + AffineTransform at = textRenderingMatrix.createAffineTransform(); + at.concatenate(font.getFontMatrix().createAffineTransform()); + + Graphics2D graphics = getGraphics(); + setClip(); + + AffineTransform prevTx = graphics.getTransform(); + stretchNonEmbeddedFont(at, font, code, displacement); + // Probably relates to DEFAULT_FONT_MATRIX transform from PDFont + at.scale(100, -100); + graphics.transform(at); + + graphics.setComposite(getGraphicsState().getNonStrokingJavaComposite()); + graphics.setPaint(getNonStrokingPaint()); + + Font prevFont = graphics.getFont(); + Font awtFont = getAwtFont(font); + graphics.setFont(awtFont); + + graphics.drawString(font.toUnicode(code), 0, 0); + + graphics.setFont(prevFont); + graphics.setTransform(prevTx); + } + + private void stretchNonEmbeddedFont(AffineTransform at, PDFont font, int code, Vector displacement) throws IOException { + // Stretch non-embedded glyph if it does not match the height/width contained in the PDF. + // Vertical fonts have zero X displacement, so the following code scales to 0 if we don't skip it. + if (!font.isEmbedded() && !font.isVertical() && !font.isStandard14() && font.hasExplicitWidth(code)) { + float fontWidth = font.getWidthFromFont(code); + if (fontWidth > 0 && Math.abs(fontWidth - displacement.getX() * 1000) > 0.0001) { + float pdfWidth = displacement.getX() * 1000; + at.scale(pdfWidth / fontWidth, 1); + } + } + } + + private Font cacheFont(PDFont font, Font awtFont) { + fonts.put(font, awtFont); + return awtFont; + } + + private Font getAwtFont(PDFont font) throws IOException { + Font awtFont = fonts.get(font); + + if (awtFont != null) { + return awtFont; + } + + if (font instanceof PDType0Font) { + return cacheFont(font, getPDType0AwtFont((PDType0Font)font)); + } + + if (font instanceof PDType1Font) { + return cacheFont(font, getPDType1AwtFont((PDType1Font)font)); + } + + String msg = String.format("Not yet implemented: %s", font.getClass().getName()); + throw new UnsupportedOperationException(msg); + } + + public Font getPDType0AwtFont(PDType0Font font) throws IOException { + Font awtFont = null; + PDCIDFont descendantFont = font.getDescendantFont(); + + if (descendantFont != null) { + + if (descendantFont instanceof PDCIDFontType2) { + awtFont = getPDCIDAwtFontType2((PDCIDFontType2)descendantFont); + } + if (awtFont != null) { + /* + * Fix Oracle JVM Crashes. + * Tested with Oracle JRE 6.0_45-b06 and 7.0_21-b11 + */ + awtFont.canDisplay(1); + } + } + + if (awtFont == null) { + awtFont = FontManager.getAwtFont(fallbackFont); + log.debug("Using font {} instead of {}", awtFont.getName(), descendantFont.getFontDescriptor().getFontName()); + } + + return awtFont.deriveFont(10f); + } + + private Font getPDType1AwtFont(PDType1Font font) throws IOException { + Font awtFont = null; + String baseFont = font.getBaseFont(); + PDFontDescriptor fd = font.getFontDescriptor(); + + if (fd != null) { + if (fd.getFontFile() != null) { + try { + // create a type1 font with the embedded data + awtFont = Font.createFont(Font.TYPE1_FONT, fd.getFontFile().createInputStream()); + } + catch(java.awt.FontFormatException e) { + log.debug("Can't read the embedded type1 font {}", fd.getFontName()); + } + } + if (awtFont == null) { + // check if the font is part of our environment + if (fd.getFontName() != null) { + awtFont = FontManager.getAwtFont(fd.getFontName()); + } + if (awtFont == null) { + log.debug("Can't find the specified font {}", fd.getFontName()); + } + } + } else { + // check if the font is part of our environment + awtFont = FontManager.getAwtFont(baseFont); + if (awtFont == null) { + log.debug("Can't find the specified basefont {}", baseFont); + } + } + + if (awtFont == null) { + // we can't find anything, so we have to use the standard font + awtFont = FontManager.getAwtFont(fallbackFont); + log.debug("Using font {} instead", awtFont.getName()); + } + + return awtFont.deriveFont(20f); + } + + public Font getPDCIDAwtFontType2(PDCIDFontType2 font) throws IOException { + Font awtFont = null; + PDFontDescriptor fd = font.getFontDescriptor(); + PDStream ff2Stream = fd.getFontFile2(); + + if (ff2Stream != null) { + try { + // create a font with the embedded data + awtFont = Font.createFont(Font.TRUETYPE_FONT, ff2Stream.createInputStream()); + } + catch(java.awt.FontFormatException f) { + log.debug("Can't read the embedded font {}", fd.getFontName()); + } + if (awtFont == null) { + if (fd.getFontName() != null) { + awtFont = FontManager.getAwtFont(fd.getFontName()); + } + if (awtFont != null) { + log.debug("Using font {} instead", awtFont.getName()); + } + } + } + + return awtFont; + } +} diff --git a/old code/tray/src/qz/printer/status/Cups.java b/old code/tray/src/qz/printer/status/Cups.java new file mode 100755 index 0000000..c9d8fde --- /dev/null +++ b/old code/tray/src/qz/printer/status/Cups.java @@ -0,0 +1,78 @@ +package qz.printer.status; + +import com.sun.jna.*; + +/** + * Created by kyle on 3/14/17. + */ +@SuppressWarnings("unused") +public interface Cups extends Library { + + Cups INSTANCE = Native.load("cups", Cups.class); + + /** + * Static class to facilitate readability of values + */ + class IPP { + public static int PORT = INSTANCE.ippPort(); + public static int TAG_OPERATION = INSTANCE.ippTagValue("Operation"); + public static int TAG_URI = INSTANCE.ippTagValue("uri"); + public static int TAG_NAME = INSTANCE.ippTagValue("Name"); + public static int TAG_TEXT = INSTANCE.ippTagValue("Text"); + public static int TAG_INTEGER = INSTANCE.ippTagValue("Integer"); + public static int TAG_KEYWORD = INSTANCE.ippTagValue("keyword"); + public static int TAG_ENUM = INSTANCE.ippTagValue("enum"); + public static int TAG_SUBSCRIPTION = INSTANCE.ippTagValue("Subscription"); + public static int TAG_MIMETYPE = INSTANCE.ippTagValue("mimetype"); + public static int GET_PRINTERS = INSTANCE.ippOpValue("CUPS-Get-Printers"); + public static int GET_PRINTER_ATTRIBUTES = INSTANCE.ippOpValue("Get-Printer-Attributes"); + public static int GET_JOB_ATTRIBUTES = INSTANCE.ippOpValue("Get-Job-Attributes"); + public static int GET_SUBSCRIPTIONS = INSTANCE.ippOpValue("Get-Subscriptions"); + public static int GET_NOTIFICATIONS = INSTANCE.ippOpValue("Get-Notifications"); + public static int CREATE_PRINTER_SUBSCRIPTION = INSTANCE.ippOpValue("Create-Printer-Subscription"); + public static int CREATE_JOB_SUBSCRIPTION = INSTANCE.ippOpValue("Create-Job-Subscription"); + public static int CANCEL_SUBSCRIPTION = INSTANCE.ippOpValue("Cancel-Subscription"); + public static int GET_JOBS = INSTANCE.ippOpValue("Get-Jobs"); + public static int CANCEL_JOB = INSTANCE.ippOpValue("Cancel-Job"); + + public static final int OP_PRINT_JOB = 0x02; + public static final int INT_ERROR = 0; + public static final int INT_UNDEFINED = -1; + + public static final String CUPS_FORMAT_TEXT = "application/vnd.cups-raw"; + } + + //See https://www.cups.org/doc/api-cups.html and https://www.cups.org/doc/api-httpipp.html for usage + + Pointer cupsEncryption(); + Pointer httpConnectEncrypt(String host, int port, Pointer encryption); + Pointer cupsDoFileRequest(Pointer http, Pointer request, String resource, String filename); + Pointer cupsDoRequest(Pointer http, Pointer request, String resource); + Pointer ippNewRequest(int op); + Pointer ippGetString(Pointer attr, int element, Pointer dataLen); + Pointer ippFirstAttribute(Pointer ipp); + Pointer ippNextAttribute(Pointer ipp); + Pointer ippFindAttribute(Pointer ipp, String name, int type); + Pointer ippFindNextAttribute(Pointer ipp, String name, int type); + + String cupsServer(); + String ippTagString(int tag); + String ippGetName(Pointer attr); + String ippGetString(Pointer attr, int element, String language); + String ippEnumString (String attrname, int enumvalue); + + int ippPort(); + int httpAssembleURI(int encoding, Memory uri, int urilen, String sceme, String username, String host, int port, String resourcef); + int ippTagValue(String name); + int ippEnumValue(String attrname, String enumstring); + int ippOpValue(String name); + int ippAddString(Pointer ipp, int group, int tag, String name, String charset, String value); + int ippAddStrings(Pointer ipp, int group, int tag, String name, int num_values, String language, StringArray values); + int ippAddInteger (Pointer ipp, int group, int tag, String name, int value); + int ippGetCount(Pointer attr); + int ippGetValueTag(Pointer ipp); + int ippGetInteger(Pointer attr, int element); + + void ippDelete(Pointer ipp); + void httpClose(Pointer http); +} diff --git a/old code/tray/src/qz/printer/status/CupsStatusHandler.java b/old code/tray/src/qz/printer/status/CupsStatusHandler.java new file mode 100755 index 0000000..ffcea29 --- /dev/null +++ b/old code/tray/src/qz/printer/status/CupsStatusHandler.java @@ -0,0 +1,114 @@ +package qz.printer.status; + +import com.sun.jna.Pointer; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.printer.status.job.NativeJobStatus; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +/** + * Created by kyle on 4/27/17. + */ +public class CupsStatusHandler extends AbstractHandler { + private static final Logger log = LogManager.getLogger(CupsStatusHandler.class); + + private static Cups cups = Cups.INSTANCE; + private int lastEventNumber = 0; + private HashMap> lastPrinterStatusMap = new HashMap<>(); + private HashMap> lastJobStatusMap = new HashMap<>(); + + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + baseRequest.setHandled(true); + if (request.getReader().readLine() != null) { + getNotifications(); + } + } + + private synchronized void getNotifications() { + Pointer response = CupsUtils.getStatuses(lastEventNumber + 1); + + Pointer eventNumberAttr = cups.ippFindAttribute(response, "notify-sequence-number", Cups.IPP.TAG_INTEGER); + Pointer eventTypeAttr = cups.ippFindAttribute(response, "notify-subscribed-event", Cups.IPP.TAG_KEYWORD); + ArrayList statuses = new ArrayList<>(); + + while (eventNumberAttr != Pointer.NULL) { + lastEventNumber = cups.ippGetInteger(eventNumberAttr, 0); + Pointer printerNameAttr = cups.ippFindNextAttribute(response, "printer-name", Cups.IPP.TAG_NAME); + + String printer = cups.ippGetString(printerNameAttr, 0, ""); + String eventType = cups.ippGetString(eventTypeAttr, 0, ""); + if (eventType.startsWith("job")) { + Pointer JobIdAttr = cups.ippFindNextAttribute(response, "notify-job-id", Cups.IPP.TAG_INTEGER); + Pointer jobStateAttr = cups.ippFindNextAttribute(response, "job-state", Cups.IPP.TAG_ENUM); + Pointer jobNameAttr = cups.ippFindNextAttribute(response, "job-name", Cups.IPP.TAG_NAME); + Pointer jobStateReasonsAttr = cups.ippFindNextAttribute(response, "job-state-reasons", Cups.IPP.TAG_KEYWORD); + int jobId = cups.ippGetInteger(JobIdAttr, 0); + String jobState = Cups.INSTANCE.ippEnumString("job-state", Cups.INSTANCE.ippGetInteger(jobStateAttr, 0)); + String jobName = cups.ippGetString(jobNameAttr, 0, ""); + // Statuses come in blocks eg. {printing, toner_low} We only want to display a status if it didn't exist in the last block + // Get the list of statuses from the last block associated with this printer + // '/' Is a documented invalid character for CUPS printer names. We will use that as a separator + String jobKey = printer + "/" + jobId; + ArrayList oldStatuses = lastJobStatusMap.getOrDefault(jobKey, new ArrayList<>()); + ArrayList newStatuses = new ArrayList<>(); + + boolean completed = false; + int attrCount = cups.ippGetCount(jobStateReasonsAttr); + for (int i = 0; i < attrCount; i++) { + String reason = cups.ippGetString(jobStateReasonsAttr, i, ""); + Status pending = NativeStatus.fromCupsJobStatus(reason, jobState, printer, jobId, jobName); + // If this status was one we didn't see last block, send it + if (!oldStatuses.contains(pending)) statuses.add(pending); + // If the job is complete, we need to remove it from our map + if ((pending.getCode() == NativeJobStatus.COMPLETE) || + (pending.getCode() == NativeJobStatus.CANCELED)) { + completed = true; + } + // regardless, remember the status for the next block + newStatuses.add(pending); + } + if (completed) { + lastJobStatusMap.remove(jobKey); + } else { + // Replace the old list with the new one + lastJobStatusMap.put(jobKey, newStatuses); + } + } else if (eventType.startsWith("printer")) { + Pointer printerStateAttr = cups.ippFindNextAttribute(response, "printer-state", Cups.IPP.TAG_ENUM); + Pointer printerStateReasonsAttr = cups.ippFindNextAttribute(response, "printer-state-reasons", Cups.IPP.TAG_KEYWORD); + String state = Cups.INSTANCE.ippEnumString("printer-state", Cups.INSTANCE.ippGetInteger(printerStateAttr, 0)); + // Statuses come in blocks eg. {printing, toner_low} We only want to display a status if it didn't exist in the last block + // Get the list of statuses from the last block associated with this printer + ArrayList oldStatuses = lastPrinterStatusMap.getOrDefault(printer, new ArrayList<>()); + ArrayList newStatuses = new ArrayList<>(); + + int attrCount = cups.ippGetCount(printerStateReasonsAttr); + for (int i = 0; i < attrCount; i++) { + String reason = cups.ippGetString(printerStateReasonsAttr, i, ""); + Status pending = NativeStatus.fromCupsPrinterStatus(reason, state, printer); + // If this status was one we didn't see last block, send it + if (!oldStatuses.contains(pending)) statuses.add(pending); + // regardless, remember the status for the next block + newStatuses.add(pending); + } + // Replace the old list with the new one + lastPrinterStatusMap.put(printer, newStatuses); + } else { + log.debug("Unknown CUPS event type {}.", eventType); + } + eventNumberAttr = cups.ippFindNextAttribute(response, "notify-sequence-number", Cups.IPP.TAG_INTEGER); + eventTypeAttr = cups.ippFindNextAttribute(response, "notify-subscribed-event", Cups.IPP.TAG_KEYWORD); + } + + cups.ippDelete(response); + StatusMonitor.statusChanged(statuses.toArray(new Status[statuses.size()])); + } +} diff --git a/old code/tray/src/qz/printer/status/CupsStatusServer.java b/old code/tray/src/qz/printer/status/CupsStatusServer.java new file mode 100755 index 0000000..b695020 --- /dev/null +++ b/old code/tray/src/qz/printer/status/CupsStatusServer.java @@ -0,0 +1,69 @@ +package qz.printer.status; + +import org.eclipse.jetty.server.Server; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + + +/** + * Created by kyle on 4/27/17. + */ +public class CupsStatusServer { + private static final Logger log = LogManager.getLogger(CupsStatusServer.class); + + public static final List CUPS_RSS_PORTS = Collections.unmodifiableList(Arrays.asList(Constants.CUPS_RSS_PORTS)); + + public static int cupsRSSPort = -1; + private static Server server; + + public static synchronized void runServer() { + CupsUtils.clearSubscriptions(); + boolean started = false; + for(int p = 0; p < CUPS_RSS_PORTS.size(); p++) { + server = new Server(CUPS_RSS_PORTS.get(p)); + server.setHandler(new CupsStatusHandler()); + + try { + server.start(); + cupsRSSPort = CUPS_RSS_PORTS.get(p); + CupsUtils.startSubscription(cupsRSSPort); + started = true; + } + catch(Exception e) { + log.warn("Could not start CUPS status server on port {}, using fallback port.", p); + } + if (started) { + break; + } + } + if (!started) { + log.warn("Could not start CUPS status server. No printer status changes will be reported."); + } + } + + public static synchronized boolean isRunning() { + return server != null && server.isRunning(); + } + + public static synchronized void stopServer() { + if (server != null) { + CupsUtils.freeIppObjs(); + server.setStopTimeout(10000); + new Thread(() -> { + try { + log.warn("Stopping CUPS status server"); + server.stop(); + } + catch(Exception ex) { + log.warn("Failed to stop CUPS status server."); + } + }).start(); + } + } +} + diff --git a/old code/tray/src/qz/printer/status/CupsUtils.java b/old code/tray/src/qz/printer/status/CupsUtils.java new file mode 100755 index 0000000..c198935 --- /dev/null +++ b/old code/tray/src/qz/printer/status/CupsUtils.java @@ -0,0 +1,295 @@ +package qz.printer.status; + +import com.sun.jna.Pointer; +import com.sun.jna.StringArray; +import org.eclipse.jetty.util.URIUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.printer.info.NativePrinter; +import qz.printer.status.Cups.IPP; + +import javax.print.PrintException; +import java.io.File; +import java.io.IOException; +import java.net.URI; +import java.util.ArrayList; + +/** + * Created by kyle on 5/17/17. + */ +public class CupsUtils { + private static final Logger log = LogManager.getLogger(CupsUtils.class); + + public static String USER = System.getProperty("user.name"); + public static String CHARSET = ""; + + private static Cups cups = Cups.INSTANCE; + + private static Pointer http; + private static int subscriptionID = IPP.INT_UNDEFINED; + + static Pointer getCupsHttp() { + if (http == null) http = cups.httpConnectEncrypt(cups.cupsServer(), IPP.PORT, cups.cupsEncryption()); + return http; + } + + static synchronized Pointer doRequest(Pointer request, String resource) { + return cups.cupsDoRequest(getCupsHttp(), request, resource); + } + + static synchronized Pointer doFileRequest(Pointer request, String resource, String fileName) { + return cups.cupsDoFileRequest(getCupsHttp(), request, resource, fileName); + } + + static Pointer listSubscriptions() { + Pointer request = cups.ippNewRequest(IPP.GET_SUBSCRIPTIONS); + + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET, + URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/printers/")); + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER); + + return doRequest(request, "/"); + } + + public static boolean sendRawFile(NativePrinter nativePrinter, File file) throws PrintException, IOException { + Pointer fileResponse = null; + try { + String printer = nativePrinter == null? null:nativePrinter.getPrinterId(); + if (printer == null || printer.trim().isEmpty()) { + throw new UnsupportedOperationException("Printer name is blank or invalid"); + } + + Pointer request = cups.ippNewRequest(IPP.OP_PRINT_JOB); + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET, URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/printers/" + printer)); + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER); + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_MIMETYPE, "document-format", null, IPP.CUPS_FORMAT_TEXT); + // request is automatically closed + fileResponse = doFileRequest(request, "/ipp/print", file.getCanonicalPath()); + + // For debugging: + // parseResponse(fileResponse); + if (cups.ippFindAttribute(fileResponse, "job-id", IPP.TAG_INTEGER) == Pointer.NULL) { + Pointer statusMessage = cups.ippFindAttribute(fileResponse, "status-message", IPP.TAG_TEXT); + if (statusMessage != Pointer.NULL) { + String exception = Cups.INSTANCE.ippGetString(statusMessage, 0, ""); + if (exception != null && !exception.trim().isEmpty()) { + throw new PrintException(exception); + } + } + throw new PrintException("An unknown printer exception has occurred"); + } + } + finally{ + if (fileResponse != null) { + cups.ippDelete(fileResponse); + } + } + return true; + } + + /** + * Gets all statuses relating to our subscriptionId with a sequence number greater than eventNumber + */ + public static Pointer getStatuses(int eventNumber) { + Pointer request = cups.ippNewRequest(IPP.GET_NOTIFICATIONS); + + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET, + URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/")); + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER); + cups.ippAddInteger(request, IPP.TAG_OPERATION, IPP.TAG_INTEGER, "notify-subscription-ids", subscriptionID); + cups.ippAddInteger(request, IPP.TAG_OPERATION, IPP.TAG_INTEGER, "notify-sequence-numbers", eventNumber); + + return doRequest(request, "/"); + } + + public static ArrayList getAllStatuses() { + ArrayList statuses = new ArrayList<>(); + Pointer request = cups.ippNewRequest(IPP.GET_PRINTERS); + + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER); + + Pointer response = doRequest(request, "/"); + Pointer stateAttr = cups.ippFindAttribute(response, "printer-state", IPP.TAG_ENUM); + Pointer reasonAttr = cups.ippFindAttribute(response, "printer-state-reasons", IPP.TAG_KEYWORD); + Pointer nameAttr = cups.ippFindAttribute(response, "printer-name", IPP.TAG_NAME); + + while(stateAttr != Pointer.NULL) { + + //save reasons until we have name, we need to go through the attrs in order + String[] reasons = new String[cups.ippGetCount(reasonAttr)]; + for(int i = 0; i < reasons.length; i++) { + reasons[i] = cups.ippGetString(reasonAttr, i, ""); + } + String state = Cups.INSTANCE.ippEnumString("printer-state", Cups.INSTANCE.ippGetInteger(stateAttr, 0)); + String printer = cups.ippGetString(nameAttr, 0, ""); + + for(String reason : reasons) { + statuses.add(NativeStatus.fromCupsPrinterStatus(reason, state, printer)); + } + + //for next loop iteration + stateAttr = cups.ippFindNextAttribute(response, "printer-state", IPP.TAG_ENUM); + reasonAttr = cups.ippFindNextAttribute(response, "printer-state-reasons", IPP.TAG_KEYWORD); + nameAttr = cups.ippFindNextAttribute(response, "printer-name", IPP.TAG_NAME); + } + + cups.ippDelete(response); + return statuses; + } + + public static boolean clearSubscriptions() { + Pointer response = listSubscriptions(); + Pointer attr = cups.ippFindAttribute(response, "notify-recipient-uri", IPP.TAG_URI); + + while(true) { + if (attr == Pointer.NULL) { + break; + } + try { + String data = cups.ippGetString(attr, 0, ""); + + int port = new URI(data).getPort(); + if (CupsStatusServer.CUPS_RSS_PORTS.contains(port)) { + Pointer idAttr = cups.ippFindNextAttribute(response, "notify-subscription-id", IPP.TAG_INTEGER); + + int id = cups.ippGetInteger(idAttr, 0); + log.warn("Ending CUPS subscription #{}", id); + endSubscription(id); + } + } + catch(Exception ignore) { } + + attr = cups.ippFindNextAttribute(response, "notify-recipient-uri", IPP.TAG_URI); + } + + cups.ippDelete(response); + return false; + } + + static void startSubscription(int rssPort) { + Runtime.getRuntime().addShutdownHook(new Thread(CupsUtils::freeIppObjs)); + + String[] subscriptions = {"job-state-changed", "printer-state-changed"}; + Pointer request = cups.ippNewRequest(IPP.CREATE_JOB_SUBSCRIPTION); + + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET, + URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/printers")); + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER); + cups.ippAddString(request, IPP.TAG_SUBSCRIPTION, IPP.TAG_URI, "notify-recipient-uri", CHARSET, + URIUtil.encodePath("rss://localhost:" + rssPort)); + cups.ippAddStrings(request, IPP.TAG_SUBSCRIPTION, IPP.TAG_KEYWORD, "notify-events", subscriptions.length, CHARSET, + new StringArray(subscriptions)); + cups.ippAddInteger(request, IPP.TAG_SUBSCRIPTION, IPP.TAG_INTEGER, "notify-lease-duration", 0); + + Pointer response = doRequest(request, "/"); + + Pointer attr = cups.ippFindAttribute(response, "notify-subscription-id", IPP.TAG_INTEGER); + if (attr != Pointer.NULL) { subscriptionID = cups.ippGetInteger(attr, 0); } + + cups.ippDelete(response); + } + + static void endSubscription(int id) { + switch (id) { + case IPP.INT_ERROR: + case IPP.INT_UNDEFINED: + return; // no subscription to end + } + Pointer request = cups.ippNewRequest(IPP.CANCEL_SUBSCRIPTION); + + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET, + URIUtil.encodePath("ipp://localhost:" + IPP.PORT)); + cups.ippAddInteger(request, IPP.TAG_OPERATION, IPP.TAG_INTEGER, "notify-subscription-id", id); + + Pointer response = doRequest(request, "/"); + cups.ippDelete(response); + } + + public static ArrayList listJobs(String printerName) { + Pointer request = cups.ippNewRequest(IPP.GET_JOBS); + + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_NAME, "requesting-user-name", CHARSET, USER); + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET, + URIUtil.encodePath("ipp://localhost:" + IPP.PORT + "/printers/" + printerName)); + + Pointer response = doRequest(request, "/"); + ArrayList ret = parseJobIds(response); + cups.ippDelete(response); + return ret; + } + + public static void cancelJob(int jobId) { + Pointer request = cups.ippNewRequest(IPP.CANCEL_JOB); + + cups.ippAddString(request, IPP.TAG_OPERATION, IPP.TAG_URI, "printer-uri", CHARSET, + URIUtil.encodePath("ipp://localhost:" + IPP.PORT)); + cups.ippAddInteger(request, IPP.TAG_OPERATION, IPP.TAG_INTEGER, "job-id", jobId); + Pointer response = doRequest(request, "/"); + cups.ippDelete(response); + } + + public synchronized static void freeIppObjs() { + if (http != null) { + endSubscription(subscriptionID); + subscriptionID = IPP.INT_UNDEFINED; + cups.httpClose(http); + http = null; + } + } + + static ArrayList parseJobIds(Pointer response) { + ArrayList attributes = getAttributes(response); + ArrayList ret = new ArrayList<>(); + for (Pointer attribute : attributes) { + if (cups.ippGetName(attribute) != null && cups.ippGetName(attribute).equals("job-id")) { + ret.add(cups.ippGetInteger(attribute, 0)); + } + } + return ret; + } + + static ArrayList getAttributes(Pointer response) { + ArrayList attributes = new ArrayList<>(); + Pointer attr = Cups.INSTANCE.ippFirstAttribute(response); + while(attr != Pointer.NULL) { + attributes.add(attr); + attr = Cups.INSTANCE.ippNextAttribute(response); + } + return attributes; + } + + @SuppressWarnings("unused") + static void parseResponse(Pointer response) { + ArrayList attributes = getAttributes(response); + for (Pointer attribute : attributes) { + System.out.println(parseAttr(attribute)); + } + System.out.println("------------------------"); + } + + static String parseAttr(Pointer attr){ + int valueTag = Cups.INSTANCE.ippGetValueTag(attr); + int attrCount = Cups.INSTANCE.ippGetCount(attr); + StringBuilder data = new StringBuilder(); + String attrName = Cups.INSTANCE.ippGetName(attr); + for (int i = 0; i < attrCount; i++) { + if (valueTag == Cups.INSTANCE.ippTagValue("Integer")) { + data.append(Cups.INSTANCE.ippGetInteger(attr, i)); + } else if (valueTag == Cups.INSTANCE.ippTagValue("Boolean")) { + data.append(Cups.INSTANCE.ippGetInteger(attr, i) == 1); + } else if (valueTag == Cups.INSTANCE.ippTagValue("Enum")) { + data.append(Cups.INSTANCE.ippEnumString(attrName, Cups.INSTANCE.ippGetInteger(attr, i))); + } else { + data.append(Cups.INSTANCE.ippGetString(attr, i, "")); + } + if (i + 1 < attrCount) { + data.append(", "); + } + } + + if (attrName == null){ + return "------------------------"; + } + return String.format("%s: %d %s {%s}", attrName, attrCount, Cups.INSTANCE.ippTagString(valueTag), data); + } +} diff --git a/old code/tray/src/qz/printer/status/NativeStatus.java b/old code/tray/src/qz/printer/status/NativeStatus.java new file mode 100755 index 0000000..7e4a0c0 --- /dev/null +++ b/old code/tray/src/qz/printer/status/NativeStatus.java @@ -0,0 +1,76 @@ +package qz.printer.status; + +import org.apache.logging.log4j.Level; +import qz.printer.status.job.CupsJobStatusMap; +import qz.printer.status.job.NativeJobStatus; +import qz.printer.status.job.WmiJobStatusMap; +import qz.printer.status.printer.CupsPrinterStatusMap; +import qz.printer.status.printer.NativePrinterStatus; +import qz.printer.status.printer.WmiPrinterStatusMap; +import qz.utils.ByteUtilities; + +public interface NativeStatus { + interface NativeMap { + NativeStatus getParent(); + Object getRawCode(); + } + + NativeStatus getDefault(); //static + String name(); + Level getLevel(); + + /** + * Printers/Jobs generally have a single status at a time however, bitwise + * operators allow multiple statuses so we'll prepare an array to accommodate + */ + static Status[] fromWmiJobStatus(int bitwiseCode, String printer, int jobId, String jobName) { + int[] rawCodes = ByteUtilities.unwind(bitwiseCode); + NativeJobStatus[] parentCodes = new NativeJobStatus[rawCodes.length]; + for(int i = 0; i < rawCodes.length; i++) { + parentCodes[i] = WmiJobStatusMap.match(rawCodes[i]); + } + + Status[] statusArray = new Status[rawCodes.length]; + for(int i = 0; i < rawCodes.length; i++) { + statusArray[i] = new Status(parentCodes[i], printer, rawCodes[i], jobId, jobName); + } + return statusArray; + } + + static Status[] fromWmiPrinterStatus(int bitwiseCode, String printer) { + int[] rawCodes = ByteUtilities.unwind(bitwiseCode); + // WmiPrinterStatusMap has an explicit 0x00000000 = OK, so we'll need to shim that + if(rawCodes.length == 0) { + rawCodes = new int[] { (Integer)WmiPrinterStatusMap.OK.getRawCode() }; + } + NativePrinterStatus[] parentCodes = new NativePrinterStatus[rawCodes.length]; + for(int i = 0; i < rawCodes.length; i++) { + parentCodes[i] = WmiPrinterStatusMap.match(rawCodes[i]); + } + + Status[] statusArray = new Status[rawCodes.length]; + for(int i = 0; i < rawCodes.length; i++) { + statusArray[i] = new Status(parentCodes[i], printer, rawCodes[i]); + } + return statusArray; + } + + + static Status fromCupsJobStatus(String reason, String state, String printer, int jobId, String jobName) { + // First check known job-state-reason pairs + NativeJobStatus cupsJobStatus = CupsJobStatusMap.matchReason(reason); + if(cupsJobStatus == null) { + // Don't return the raw job-state-reason if we couldn't find it mapped, return job-state instead + return new Status(CupsJobStatusMap.matchState(state), printer, state, jobId, jobName); + } else if(cupsJobStatus == NativeJobStatus.UNMAPPED) { + // Still lookup the job-state, but let the user know what the unmapped job-state-reason was + return new Status(CupsJobStatusMap.matchState(state), printer, reason, jobId, jobName); + } + return new Status(cupsJobStatus, printer, reason, jobId, jobName); + } + + + static Status fromCupsPrinterStatus(String reason, String state, String printer) { + return CupsPrinterStatusMap.createStatus(reason, state, printer); + } +} diff --git a/old code/tray/src/qz/printer/status/Status.java b/old code/tray/src/qz/printer/status/Status.java new file mode 100755 index 0000000..ffd2ad9 --- /dev/null +++ b/old code/tray/src/qz/printer/status/Status.java @@ -0,0 +1,98 @@ +package qz.printer.status; + +import qz.printer.PrintServiceMatcher; +import qz.printer.info.NativePrinter; +import qz.printer.status.job.NativeJobStatus; +import qz.printer.status.printer.NativePrinterStatus; +import qz.utils.SystemUtilities; + +/** + * Container object for both printer and job statuses + */ +public class Status { + private NativeStatus code; + private String printer; + private Object rawCode; + private EventType eventType; + private int jobId; // job statuses only + private String jobName; // job status only + + enum EventType { + JOB, + JOB_DATA, + PRINTER; + } + + public Status(NativePrinterStatus code, String printer, Object rawCode) { + this.code = code; + this.printer = printer; + this.rawCode = rawCode; + this.jobId = -1; + this.eventType = EventType.PRINTER; + } + + public Status(NativeJobStatus code, String printer, Object rawCode, int jobId, String jobName) { + this.code = code; + this.printer = printer; + this.rawCode = rawCode; + this.jobId = jobId; + this.jobName = jobName; + this.eventType = EventType.JOB; + } + + public String sanitizePrinterName() { + if(!SystemUtilities.isMac()) { + return printer; + } + + // On MacOS the description is used as the printer name + NativePrinter nativePrinter = PrintServiceMatcher.matchPrinter(printer, true); + if (nativePrinter == null) { + // If the printer description is missing from the map (usually because the printer was deleted), use the cups id instead + return printer; + } + return nativePrinter.getPrintService().value().getName(); + } + + public NativeStatus getCode() { + return code; + } + + public Object getRawCode() { + return rawCode; + } + + public String getPrinter() { + return printer; + } + + public EventType getEventType() { + return eventType; + } + + public String getJobName() { + return jobName; + } + + public int getJobId() { + return jobId; + } + + @Override + public boolean equals(Object obj) { + if(obj != null && obj instanceof Status) { + Status status = (Status)obj; + return status.eventType == eventType && status.printer.equals(printer) && status.jobId == jobId && rawCode.equals(status.rawCode); + } + return super.equals(obj); + } + + public String toString() { + return code.name() + ": Level: " + code.getLevel() + + ", From: " + sanitizePrinterName() + + ", EventType: " + eventType + + ", Code: " + rawCode + + (jobId > 0 ? ", JobId: " + jobId : "") + + (jobName != null ? ", Job Name: " + jobName : ""); + } +} diff --git a/old code/tray/src/qz/printer/status/StatusMonitor.java b/old code/tray/src/qz/printer/status/StatusMonitor.java new file mode 100755 index 0000000..d420fe5 --- /dev/null +++ b/old code/tray/src/qz/printer/status/StatusMonitor.java @@ -0,0 +1,230 @@ +package qz.printer.status; + +import com.sun.jna.platform.win32.Winspool; +import com.sun.jna.platform.win32.WinspoolUtil; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.eclipse.jetty.util.MultiMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.jetty.websocket.api.Session; +import qz.printer.PrintServiceMatcher; +import qz.printer.info.NativePrinterMap; +import qz.utils.PrintingUtilities; +import qz.utils.SystemUtilities; +import qz.ws.SocketConnection; + +import java.nio.channels.ClosedChannelException; +import java.util.*; + +import static qz.utils.SystemUtilities.isWindows; + +/** + * Created by Kyle on 2/23/2017. + */ +public class StatusMonitor { + private static final Logger log = LogManager.getLogger(StatusMonitor.class); + + public static final String ALL_PRINTERS = ""; + + private static Thread printerConnectionsThread; + private static Thread statusEventDispatchThread; + private static final HashMap notificationThreadCollection = new HashMap<>(); + private static final HashMap statusSessions = new HashMap<>(); + private static final MultiMap clientPrinterConnections = new MultiMap<>(); + private static final LinkedList statusQueue = new LinkedList<>(); + + public synchronized static boolean launchNotificationThreads() { + ArrayList printerNameList = new ArrayList<>(); + + Winspool.PRINTER_INFO_2[] printers = WinspoolUtil.getAllPrinterInfo2(); + for (Winspool.PRINTER_INFO_2 printer : printers) { + printerNameList.add(printer.pPrinterName); + if (!notificationThreadCollection.containsKey(printer.pPrinterName)) { + Thread notificationThread = new WmiPrinterStatusThread(printer); + notificationThreadCollection.put(printer.pPrinterName, notificationThread); + notificationThread.start(); + } + } + //interrupt threads that don't have associated printers + for (Map.Entry e : notificationThreadCollection.entrySet()) { + if (!printerNameList.contains(e.getKey())) { + e.getValue().interrupt(); + notificationThreadCollection.remove(e.getKey()); + } + } + + if (printerConnectionsThread == null) { + printerConnectionsThread = new WmiPrinterConnectionsThread(); + printerConnectionsThread.start(); + } + + return true; + } + + public synchronized static void relaunchThreads() { + launchNotificationThreads(); + } + + public synchronized static void closeNotificationThreads() { + for (Thread t : notificationThreadCollection.values()) { + t.interrupt(); + } + notificationThreadCollection.clear(); + + if (printerConnectionsThread != null) { + printerConnectionsThread.interrupt(); + printerConnectionsThread = null; + } + } + + public synchronized static boolean isListening(SocketConnection connection) { + return statusSessions.containsKey(connection); + } + + public synchronized static boolean startListening(SocketConnection connection, Session session, JSONObject params) throws JSONException { + JSONArray printerNames = params.getJSONArray("printerNames"); + statusSessions.putIfAbsent(connection, new StatusSession(session)); + + if (printerNames.isNull(0)) { //listen to all printers + addClientPrinterConnection(ALL_PRINTERS, connection, params); + } else { // listen to specific printer(s) + for (int i = 0; i < printerNames.length(); i++) { + String printerName = printerNames.getString(i); + if (SystemUtilities.isMac()) printerName = macNameFix(printerName); + + if (printerName == null || printerName.equals("")) { + throw new IllegalArgumentException(); + } + addClientPrinterConnection(printerName, connection, params); + } + } + + if (isWindows()) { + return launchNotificationThreads(); + } else { + if (!CupsStatusServer.isRunning()) { CupsStatusServer.runServer(); } + return true; + } + } + + public synchronized static void stopListening(SocketConnection connection) { + statusSessions.remove(connection); + closeListener(connection); + } + + private synchronized static void addClientPrinterConnection(String printerName, SocketConnection connection, JSONObject params) { + boolean jobData = params.optBoolean("jobData", false); + int maxJobData = params.optInt("maxJobData", -1); + PrintingUtilities.Flavor dataFlavor = PrintingUtilities.Flavor.parse(params, PrintingUtilities.Flavor.PLAIN); + + if (jobData) { + statusSessions.get(connection).enableJobDataOnPrinter(printerName, maxJobData, dataFlavor); + } + if (!clientPrinterConnections.containsKey(printerName)) { + clientPrinterConnections.add(printerName, connection); + } else if (!clientPrinterConnections.getValues(printerName).contains(connection)) { + clientPrinterConnections.add(printerName, connection); + } + } + + public synchronized static void sendStatuses(SocketConnection connection) { + boolean sendForAllPrinters = false; + ArrayList statuses = isWindows() ? WmiPrinterStatusThread.getAllStatuses(): CupsUtils.getAllStatuses(); + + // First check if we're listening on all printers for this connection + List connections = clientPrinterConnections.get(ALL_PRINTERS); + if (connections != null) { + sendForAllPrinters = connections.contains(connection); + } + + for (Status status : statuses) { + if (sendForAllPrinters) { + statusSessions.get(connection).statusChanged(status, () -> stopListening(connection)); + } else { + // Only send the status of the printers requested + connections = clientPrinterConnections.get(status.getPrinter()); + if ((connections != null) && connections.contains(connection)) { + statusSessions.get(connection).statusChanged(status, () -> stopListening(connection)); + } + } + } + } + + public synchronized static void closeListener(SocketConnection connection) { + clientPrinterConnections.entrySet().removeIf((Map.Entry> entry) -> ( + entry.getValue().contains(connection) + )); + if (clientPrinterConnections.isEmpty()) { + if (isWindows()) { + closeNotificationThreads(); + } else { + CupsStatusServer.stopServer(); + } + } + } + + private synchronized static void launchStatusEventDispatchThread() { + // Null is our main test to see if the thread needs to restart. If the thread was suspended, it won't be null, so check to see if it is alive as well. + if (statusEventDispatchThread != null && statusEventDispatchThread.isAlive()) return; + statusEventDispatchThread = new Thread(() -> { + while (!Thread.currentThread().isInterrupted() && dispatchStatusEvent()) { + // If we don't yield, this will constantly run dispatchStatusEvent and lock up the class, even though this thread isn't synchronized. + Thread.yield(); + } + if (Thread.currentThread().isInterrupted()) log.warn("statusEventDispatchThread Interrupted"); + }, "statusEventDispatchThread"); + statusEventDispatchThread.start(); + } + + public synchronized static void statusChanged(Status[] statuses) { + // Add statuses to the queue, statusEventDispatchThread will resolve these one at a time until the queue is empty + Collections.addAll(statusQueue, statuses); + if (!statusQueue.isEmpty()) { + // If statusEventDispatchThread isn't already running, launch it + launchStatusEventDispatchThread(); + } + } + + // This is the main body of the statusEventDispatchThread. + // Dispatch one status event to n clients connection, based on clientPrinterConnections + // Returns false when there are no more statuses in the queue + private synchronized static boolean dispatchStatusEvent() { + if (statusQueue.isEmpty()) { + // Returning false will kill statusEventDispatchThread, but we also want to null out the value while we are still in a synchronized method + statusEventDispatchThread = null; + return false; + } + Status status = statusQueue.removeFirst(); + + HashSet listeningConnections = new HashSet<>(); + if (clientPrinterConnections.containsKey(status.getPrinter())) { + // Find every client that subscribed to this printer + listeningConnections.addAll(clientPrinterConnections.get(status.getPrinter())); + } + if (clientPrinterConnections.containsKey(ALL_PRINTERS)) { + // And find every client that subscribed to all printers + listeningConnections.addAll(clientPrinterConnections.get(ALL_PRINTERS)); + } + + // Notify each client subscription + for (SocketConnection connection : listeningConnections) { + statusSessions.get(connection).statusChanged(status, () -> stopListening(connection)); + } + + return true; + } + + private static String macNameFix(String printerName) { + // Since 2.0: Mac printers use descriptions as printer names; Find CUPS ID by Description + String returnString = NativePrinterMap.getInstance().lookupPrinterId(printerName); + // Handle edge-case where printer was recently renamed/added + if (returnString == null) { + // Call PrintServiceLookup.lookupPrintServices again + PrintServiceMatcher.getNativePrinterList(true); + returnString = NativePrinterMap.getInstance().lookupPrinterId(printerName); + } + return returnString; + } +} diff --git a/old code/tray/src/qz/printer/status/StatusSession.java b/old code/tray/src/qz/printer/status/StatusSession.java new file mode 100755 index 0000000..d2d4ae4 --- /dev/null +++ b/old code/tray/src/qz/printer/status/StatusSession.java @@ -0,0 +1,136 @@ +package qz.printer.status; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.jetty.websocket.api.Session; +import qz.App; +import qz.printer.status.job.WmiJobStatusMap; +import qz.utils.*; +import qz.ws.PrintSocketClient; +import qz.ws.StreamEvent; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; + +import static qz.printer.status.StatusMonitor.ALL_PRINTERS; + +public class StatusSession { + private static final Logger log = LogManager.getLogger(StatusSession.class); + private Session session; + private HashMap printerSpoolerMap = new HashMap<>(); + + private class Spooler implements Cloneable { + public Path path; + public int maxJobData; + public PrintingUtilities.Flavor dataFlavor; + + public Spooler() { + this(null, -1, PrintingUtilities.Flavor.PLAIN); + } + + public Spooler(Path path, int maxJobData, PrintingUtilities.Flavor dataFlavor) { + this.path = path; + this.maxJobData = maxJobData; + this.dataFlavor = dataFlavor; + } + + @Override + public Spooler clone() { + return new Spooler(path, maxJobData, dataFlavor); + } + } + + public StatusSession(Session session) { + this.session = session; + } + + public void statusChanged(Status status, Runnable closeHandler) { + PrintSocketClient.sendStream(session, createStatusStream(status), closeHandler); + // If this statusSession has printers flagged to return jobData, issue a jobData event after any 'retained' job events + if (status.getCode() == WmiJobStatusMap.RETAINED.getParent() && isDataPrinter(status.getPrinter())) { + PrintSocketClient.sendStream(session, createJobDataStream(status), closeHandler); + } + } + + public void enableJobDataOnPrinter(String printer, int maxJobData, PrintingUtilities.Flavor dataFlavor) throws UnsupportedOperationException { + if (!SystemUtilities.isWindows()) { + throw new UnsupportedOperationException("Job data listeners are only supported on Windows"); + } + if (!PrefsSearch.getBoolean(ArgValue.PRINTER_STATUS_JOB_DATA, false, App.getTrayProperties())) { + throw new UnsupportedOperationException("Job data listeners are currently disabled"); + } + if (printerSpoolerMap.containsKey(printer)) { + printerSpoolerMap.get(printer).maxJobData = maxJobData; + } else { + // Lookup spooler path lazily + printerSpoolerMap.put(printer, new Spooler(null, maxJobData, dataFlavor)); + } + if (printer.equals(ALL_PRINTERS)) { + // If we have started job-data listening on all printer, the new parameters need to be added to all existing printers + for(Map.Entry entry : printerSpoolerMap.entrySet()) { + entry.getValue().maxJobData = maxJobData; + } + } + } + + private StreamEvent createJobDataStream(Status status) { + StreamEvent streamEvent = new StreamEvent(StreamEvent.Stream.PRINTER, StreamEvent.Type.ACTION) + .withData("printerName", status.sanitizePrinterName()) + .withData("eventType", Status.EventType.JOB_DATA) + .withData("jobID", status.getJobId()) + .withData("jobName", status.getJobName()) + .withData("data", getJobData(status.getJobId(), status.getPrinter())); + return streamEvent; + } + + private StreamEvent createStatusStream(Status status) { + StreamEvent streamEvent = new StreamEvent(StreamEvent.Stream.PRINTER, StreamEvent.Type.ACTION) + .withData("printerName", status.sanitizePrinterName()) + .withData("eventType", status.getEventType()) + .withData("statusText", status.getCode().name()) + .withData("severity", status.getCode().getLevel()) + .withData("statusCode", status.getRawCode()) + .withData("message", status.toString()); + if(status.getJobId() > 0) { + streamEvent.withData("jobId", status.getJobId()); + } + if(status.getJobName() != null) { + streamEvent.withData("jobName", status.getJobName()); + } + return streamEvent; + } + + private String getJobData(int jobId, String printer) { + String data = null; + try { + if (!printerSpoolerMap.containsKey(printer)) { + // If not listening on this printer, assume we're listening on ALL_PRINTERS + Spooler spooler; + if(printerSpoolerMap.containsKey(ALL_PRINTERS)) { + spooler = printerSpoolerMap.get(ALL_PRINTERS).clone(); + } else { + // we should never get here + spooler = new Spooler(); + } + printerSpoolerMap.put(printer, spooler); + } + Spooler spooler = printerSpoolerMap.get(printer); + if (spooler.path == null) spooler.path = WindowsUtilities.getSpoolerLocation(printer); + if (spooler.maxJobData != -1 && Files.size(spooler.path) > spooler.maxJobData) { + throw new IOException("File too large, omitting result. Size:" + Files.size(spooler.path) + " MaxJobData:" + spooler.maxJobData); + } + data = spooler.dataFlavor.toString(Files.readAllBytes(spooler.path.resolve(String.format("%05d", jobId) + ".SPL"))); + } + catch(IOException e) { + log.error("Failed to retrieve job data from job #{}", jobId, e); + } + return data; + } + + private boolean isDataPrinter(String printer) { + return (printerSpoolerMap.containsKey(ALL_PRINTERS) || printerSpoolerMap.containsKey(printer)); + } +} diff --git a/old code/tray/src/qz/printer/status/WmiPrinterConnectionsThread.java b/old code/tray/src/qz/printer/status/WmiPrinterConnectionsThread.java new file mode 100755 index 0000000..8a4214c --- /dev/null +++ b/old code/tray/src/qz/printer/status/WmiPrinterConnectionsThread.java @@ -0,0 +1,52 @@ +package qz.printer.status; + +import com.sun.jna.platform.win32.Winspool; +import com.sun.jna.platform.win32.WinspoolUtil; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class WmiPrinterConnectionsThread extends Thread { + + private static final Logger log = LogManager.getLogger(WmiPrinterConnectionsThread.class); + + private boolean running = true; + + public WmiPrinterConnectionsThread() { + super("Printer Connection Monitor"); + } + + @Override + public void run() { + Winspool.PRINTER_INFO_1[] currentPrinterList = WinspoolUtil.getPrinterInfo1(); + + while(running) { + try { sleep(1000); } catch(Exception ignore) {} + + Winspool.PRINTER_INFO_1[] newPrinterList = WinspoolUtil.getPrinterInfo1(); + + if (!arrayEquiv(currentPrinterList, newPrinterList)) { + StatusMonitor.relaunchThreads(); + } + + currentPrinterList = newPrinterList; + } + } + + private boolean arrayEquiv(Winspool.PRINTER_INFO_1[] a, Winspool.PRINTER_INFO_1[] b) { + if (a.length != b.length) { return false; } + + for(int i = 0; i < a.length; i++) { + if (!a[i].pName.equals(b[i].pName)) { + return false; + } + } + + return true; + } + + @Override + public void interrupt() { + running = false; + super.interrupt(); + } +} diff --git a/old code/tray/src/qz/printer/status/WmiPrinterStatusThread.java b/old code/tray/src/qz/printer/status/WmiPrinterStatusThread.java new file mode 100755 index 0000000..adbc78a --- /dev/null +++ b/old code/tray/src/qz/printer/status/WmiPrinterStatusThread.java @@ -0,0 +1,268 @@ +package qz.printer.status; + +import com.sun.jna.Structure; +import com.sun.jna.platform.win32.*; +import com.sun.jna.ptr.PointerByReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.printer.status.job.WmiJobStatusMap; +import qz.printer.status.printer.NativePrinterStatus; +import qz.printer.status.printer.WmiPrinterStatusMap; + +import java.util.*; + +import static qz.printer.status.printer.WmiPrinterStatusMap.*; + +public class WmiPrinterStatusThread extends Thread { + + private static final Logger log = LogManager.getLogger(StatusMonitor.class); + private final Winspool spool = Winspool.INSTANCE; + private final String printerName; + private final HashMap docNames = new HashMap<>(); + private final HashMap> pendingJobStatuses = new HashMap<>(); + private final HashMap lastJobStatusCodes = new HashMap<>(); + + private boolean holdsJobs; + private int statusField; + private int attributeField; + // Last "combined" printer status, see also combineStatus() + private int lastPrinterStatus; + + private boolean wasOk = false; + private boolean closing = false; + + private WinNT.HANDLE hChangeObject; + private WinDef.DWORDByReference pdwChangeResult; + + Winspool.PRINTER_NOTIFY_OPTIONS listenOptions; + Winspool.PRINTER_NOTIFY_OPTIONS statusOptions; + + // Honor translated strings, if available + private static final ArrayList invalidNames = new ArrayList<>(); + static { + try { + invalidNames.add(User32Util.loadString("%SystemRoot%\\system32\\localspl.dll,108")); + invalidNames.add(User32Util.loadString("%SystemRoot%\\system32\\localspl.dll,107")); + } catch(Exception e) { + log.warn("Unable to obtain strings, defaulting to en-US values.", e); + invalidNames.add("Local Downlevel Document"); + invalidNames.add("Remote Downlevel Document"); + } + } + + public WmiPrinterStatusThread(Winspool.PRINTER_INFO_2 printerInfo2) { + super("Printer Status Monitor " + printerInfo2.pPrinterName); + printerName = printerInfo2.pPrinterName; + holdsJobs = (printerInfo2.Attributes & Winspool.PRINTER_ATTRIBUTE_KEEPPRINTEDJOBS) > 0; + statusField = printerInfo2.Status; + attributeField = printerInfo2.Attributes; + lastPrinterStatus = combineStatus(statusField, attributeField); + + listenOptions = new Winspool.PRINTER_NOTIFY_OPTIONS(); + listenOptions.Version = 2; + listenOptions.Flags = Winspool.PRINTER_NOTIFY_OPTIONS_REFRESH; + listenOptions.Count = 2; + + Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference[] mem = (Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference[]) + new Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference().toArray(2); + mem[0].Type = Winspool.JOB_NOTIFY_TYPE; + mem[0].setFields(new short[] {Winspool.JOB_NOTIFY_FIELD_STATUS, Winspool.JOB_NOTIFY_FIELD_DOCUMENT }); + mem[1].Type = Winspool.PRINTER_NOTIFY_TYPE; + mem[1].setFields(new short[] {Winspool.PRINTER_NOTIFY_FIELD_STATUS, Winspool.PRINTER_NOTIFY_FIELD_ATTRIBUTES }); + listenOptions.pTypes = mem[0]; + + statusOptions = new Winspool.PRINTER_NOTIFY_OPTIONS(); + statusOptions.Version = 2; + // Status option 'refresh' leads to a loss of data associated with our lock. I don't know why. + // statusOptions.Flags = Winspool.PRINTER_NOTIFY_OPTIONS_REFRESH; + statusOptions.Count = 2; + + mem = (Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference[]) + new Winspool.PRINTER_NOTIFY_OPTIONS_TYPE.ByReference().toArray(2); + mem[0].Type = Winspool.JOB_NOTIFY_TYPE; + mem[0].setFields(new short[] { Winspool.JOB_NOTIFY_FIELD_STATUS, Winspool.JOB_NOTIFY_FIELD_DOCUMENT }); + mem[1].Type = Winspool.PRINTER_NOTIFY_TYPE; + mem[1].setFields(new short[] { Winspool.PRINTER_NOTIFY_FIELD_STATUS, Winspool.PRINTER_NOTIFY_FIELD_ATTRIBUTES }); + statusOptions.pTypes = mem[0]; + } + + @Override + public void run() { + + attachToSystem(); + + if (hChangeObject != null) { + while(!closing) { + waitOnChange(); + if (closing) { break; } + ingestChange(); + } + } + } + + private void attachToSystem() { + WinNT.HANDLEByReference phPrinterObject = new WinNT.HANDLEByReference(); + spool.OpenPrinter(printerName, phPrinterObject, null); + + pdwChangeResult = new WinDef.DWORDByReference(); + //The second param determines what kind of event releases our lock + //See https://msdn.microsoft.com/en-us/library/windows/desktop/dd162722(v=vs.85).aspx + hChangeObject = spool.FindFirstPrinterChangeNotification(phPrinterObject.getValue(), Winspool.PRINTER_CHANGE_JOB, 0, listenOptions); + } + + private void waitOnChange() { + Kernel32.INSTANCE.WaitForSingleObject(hChangeObject, WinBase.INFINITE); + } + + private void ingestChange() { + PointerByReference dataPointer = new PointerByReference(); + if (spool.FindNextPrinterChangeNotification(hChangeObject, pdwChangeResult, statusOptions, dataPointer)) { + // Many events fire with dataPointer == null, see also https://stackoverflow.com/questions/16283827 + if (dataPointer.getValue() != null) { + Winspool.PRINTER_NOTIFY_INFO data = Structure.newInstance(Winspool.PRINTER_NOTIFY_INFO.class, dataPointer.getValue()); + data.read(); + + for (Winspool.PRINTER_NOTIFY_INFO_DATA d: data.aData) { + decodeStatus(d); + } + sendPendingStatuses(); + Winspool.INSTANCE.FreePrinterNotifyInfo(data.getPointer()); + } + } else { + issueError(); + } + } + + private void decodeStatus(Winspool.PRINTER_NOTIFY_INFO_DATA d) { + if (d.Type == Winspool.PRINTER_NOTIFY_TYPE) { + if (d.Field == Winspool.PRINTER_NOTIFY_FIELD_STATUS) { // Printer Status Changed + statusField = d.NotifyData.adwData[0]; + } else if (d.Field == Winspool.PRINTER_NOTIFY_FIELD_ATTRIBUTES) { // Printer Attributes Changed + attributeField = d.NotifyData.adwData[0]; + holdsJobs = (d.NotifyData.adwData[0] & Winspool.PRINTER_ATTRIBUTE_KEEPPRINTEDJOBS) != 0; + } else { + log.warn("Unknown event field {}", d.Field); + } + + int combinedStatus = combineStatus(statusField, attributeField); + if (combinedStatus != lastPrinterStatus) { + Status[] statuses = NativeStatus.fromWmiPrinterStatus(combinedStatus, printerName); + StatusMonitor.statusChanged(statuses); + + // If the printer was in an error state before and is not now, send an 'OK' + boolean isOk = (combinedStatus & NOT_OK_MASK) == 0; + if (isOk && !wasOk) { + // If the status is 0x00000000, fromWmiPrinterStatus returns 'OK'. We don't want to send a duplicate. + if (combinedStatus != 0) StatusMonitor.statusChanged(new Status[]{new Status(NativePrinterStatus.OK, printerName, 0)}); + } + wasOk = isOk; + + lastPrinterStatus = combinedStatus; + } + } else if (d.Type == Winspool.JOB_NOTIFY_TYPE) { + // Job Name Set or Changed + if (d.Field == Winspool.JOB_NOTIFY_FIELD_DOCUMENT) { + // The element containing our Doc name is not always the first item of the event + // The Job name is only sent once, catalog it for later statuses + docNames.put(d.Id, d.NotifyData.Data.pBuf.getWideString(0)); + // Job Status Changed + } else if (d.Field == Winspool.JOB_NOTIFY_FIELD_STATUS) { + //If there is no list for a given ID, create a new one and add it to the collection under said ID + ArrayList statusList = pendingJobStatuses.computeIfAbsent(d.Id, k -> new ArrayList<>()); + statusList.add(d.NotifyData.adwData[0]); + } + } + } + + /** + * Bitwise-safe combination of statusField and attributeField's PRINTER_ATTRIBUTE_WORK_OFFLINE. + * + * Due to PRINTER_ATTRIBUTE_WORK_OFFLINE's overlapping bitwise value, we must use a + * non-overlapping value, ATTRIBUTE_WORK_OFFLINE. + * + * See also: https://stackoverflow.com/questions/41437023 + */ + private static int combineStatus(int statusField, int attributeField) { + int workOfflineFlag = (attributeField & Winspool.PRINTER_ATTRIBUTE_WORK_OFFLINE) == 0 ? 0 : (int)WmiPrinterStatusMap.ATTRIBUTE_WORK_OFFLINE.getRawCode(); + return statusField | workOfflineFlag; + } + + private void sendPendingStatuses() { + if (pendingJobStatuses.size() == 0) return; + for (Iterator>> i = pendingJobStatuses.entrySet().iterator(); i.hasNext();) { + Map.Entry> jobCodesEntry = i.next(); + ArrayList codes = jobCodesEntry.getValue(); + int jobId = jobCodesEntry.getKey(); + + // Wait until we have a real docName + if (invalidNames.contains(docNames.get(jobId))) continue; + + // Workaround for double 'printed' statuses + if (holdsJobs && docNames.get(jobId) == null && codes.size() == 1 && codes.get(0) == (int)WmiJobStatusMap.PRINTED.getRawCode()) { + i.remove(); + lastJobStatusCodes.remove(jobId); + continue; + } + + for (int code: codes) { + int oldStatusCode = lastJobStatusCodes.getOrDefault(jobId, 0); + + // This only sets status flags if they are not in oldStatusCode + int statusToReport = code & (~oldStatusCode); + if (statusToReport != 0) { + StatusMonitor.statusChanged(NativeStatus.fromWmiJobStatus(statusToReport, printerName, jobId, docNames.get(jobId))); + } + lastJobStatusCodes.put(jobId, code); + } + i.remove(); + + + int code = codes.get(codes.size() - 1); + boolean isFinalCode = (code & (int)WmiJobStatusMap.DELETED.getRawCode()) > 0; + + // If the printer holds jobs, the last event we will see is 'printed' or 'deleted' and not 'printing', otherwise it will be just 'deleted'. + if (holdsJobs) { + isFinalCode |= (code & (int)WmiJobStatusMap.PRINTED.getRawCode()) > 0; + isFinalCode &= (code & (int)WmiJobStatusMap.PRINTING.getRawCode()) == 0; + } + // If that was the last status we will see from a job, remove it from our lists. + if (isFinalCode) { + docNames.remove(jobId); + lastJobStatusCodes.remove(jobId); + } + } + } + + private void issueError() { + int errorCode = Kernel32.INSTANCE.GetLastError(); + log.error("WMI Error number: {}, This should be reported", errorCode); + Status[] unknownError = { new Status(NativePrinterStatus.UNMAPPED, printerName, WmiPrinterStatusMap.UNKNOWN_STATUS.getRawCode()) }; + StatusMonitor.statusChanged(unknownError); + try { + //if the error repeats, we don't want to lock up the cpu + Thread.sleep(1000); + } + catch(Exception ignore) {} + } + + @Override + public void interrupt() { + closing = true; + spool.FindClosePrinterChangeNotification(hChangeObject); + super.interrupt(); + } + + public static ArrayList getAllStatuses() { + ArrayList statuses = new ArrayList<>(); + Winspool.PRINTER_INFO_2[] wmiPrinters = WinspoolUtil.getAllPrinterInfo2(); + for(Winspool.PRINTER_INFO_2 printerInfo2 : wmiPrinters) { + WinNT.HANDLEByReference phPrinter = new WinNT.HANDLEByReference(); + Winspool.INSTANCE.OpenPrinter(printerInfo2.pPrinterName, phPrinter, null); + for(Winspool.JOB_INFO_1 info : WinspoolUtil.getJobInfo1(phPrinter)) { + Collections.addAll(statuses, NativeStatus.fromWmiJobStatus(info.Status, printerInfo2.pPrinterName, info.JobId, info.pDocument)); + } + Collections.addAll(statuses, NativeStatus.fromWmiPrinterStatus(combineStatus(printerInfo2.Status, printerInfo2.Attributes), printerInfo2.pPrinterName)); + } + return statuses; + } +} diff --git a/old code/tray/src/qz/printer/status/job/CupsJobStatusMap.java b/old code/tray/src/qz/printer/status/job/CupsJobStatusMap.java new file mode 100755 index 0000000..053ea10 --- /dev/null +++ b/old code/tray/src/qz/printer/status/job/CupsJobStatusMap.java @@ -0,0 +1,155 @@ +package qz.printer.status.job; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.printer.status.NativeStatus; + +import java.util.Locale; +import java.util.SortedMap; +import java.util.TreeMap; +import static qz.printer.status.job.CupsJobStatusMap.CupsJobStatusType.*; + +/** + * Created by Tres on 12/23/2020 + */ +public enum CupsJobStatusMap implements NativeStatus.NativeMap { + // job-state + PENDING(STATE, NativeJobStatus.SPOOLING), // pending + PENDING_HELD(STATE, NativeJobStatus.PAUSED), // pending-held + PROCESSING(STATE, NativeJobStatus.SPOOLING), // processing + PROCESSING_STOPPED(STATE, NativeJobStatus.PAUSED), // processing-stopped + CANCELED(STATE, NativeJobStatus.CANCELED), // canceled + ABORTED(STATE, NativeJobStatus.ABORTED), // aborted + COMPLETED(STATE, NativeJobStatus.COMPLETE), // completed + + // job-state-reasons. NativeJobStatus.UNMAPPED will fallback to the job-state instead + ABORTED_BY_SYSTEM(REASON, NativeJobStatus.ABORTED), // aborted-by-system + ACCOUNT_AUTHORIZATION_FAILED(REASON, NativeJobStatus.UNMAPPED), // account-authorization-failed + ACCOUNT_CLOSED(REASON, NativeJobStatus.UNMAPPED), // account-closed + ACCOUNT_INFO_NEEDED(REASON, NativeJobStatus.USER_INTERVENTION), // account-info-needed + ACCOUNT_LIMIT_REACHED(REASON, NativeJobStatus.UNMAPPED), // account-limit-reached + COMPRESSION_ERROR(REASON, NativeJobStatus.UNMAPPED), // compression-error + CONFLICTING_ATTRIBUTES(REASON, NativeJobStatus.UNMAPPED), // conflicting-attributes + CONNECTED_TO_DESTINATION(REASON, NativeJobStatus.UNMAPPED), // connected-to-destination + CONNECTING_TO_DESTINATION(REASON, NativeJobStatus.UNMAPPED), // connecting-to-destination + DESTINATION_URI_FAILED(REASON, NativeJobStatus.UNMAPPED), // destination-uri-failed + DIGITAL_SIGNATURE_DID_NOT_VERIFY(REASON, NativeJobStatus.UNMAPPED), // digital-signature-did-not-verify + DIGITAL_SIGNATURE_TYPE_NOT_SUPPORTED(REASON, NativeJobStatus.UNMAPPED), // digital-signature-type-not-supported + DOCUMENT_ACCESS_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-access-error + DOCUMENT_FORMAT_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-format-error + DOCUMENT_PASSWORD_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-password-error + DOCUMENT_PERMISSION_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-permission-error + DOCUMENT_SECURITY_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-security-error + DOCUMENT_UNPRINTABLE_ERROR(REASON, NativeJobStatus.UNMAPPED), // document-unprintable-error + ERRORS_DETECTED(REASON, NativeJobStatus.UNMAPPED), // errors-detected + JOB_CANCELED_AT_DEVICE(REASON, NativeJobStatus.CANCELED), // job-canceled-at-device + JOB_CANCELED_BY_OPERATOR(REASON, NativeJobStatus.CANCELED), // job-canceled-by-operator + JOB_CANCELED_BY_USER(REASON, NativeJobStatus.CANCELED), // job-canceled-by-user + JOB_COMPLETED_SUCCESSFULLY(REASON, NativeJobStatus.COMPLETE), // job-completed-successfully + JOB_COMPLETED_WITH_ERRORS(REASON, NativeJobStatus.COMPLETE), // job-completed-with-errors + JOB_COMPLETED_WITH_WARNINGS(REASON, NativeJobStatus.COMPLETE), // job-completed-with-warnings + JOB_DATA_INSUFFICIENT(REASON, NativeJobStatus.UNMAPPED), // job-data-insufficient + JOB_DELAY_OUTPUT_UNTIL_SPECIFIED(REASON, NativeJobStatus.SCHEDULED), // job-delay-output-until-specified + JOB_DIGITAL_SIGNATURE_WAIT(REASON, NativeJobStatus.UNMAPPED), // job-digital-signature-wait + JOB_FETCHABLE(REASON, NativeJobStatus.UNMAPPED), // job-fetchable + JOB_HELD_FOR_REVIEW(REASON, NativeJobStatus.SPOOLING), // job-held-for-review + JOB_HOLD_UNTIL_SPECIFIED(REASON, NativeJobStatus.PAUSED), // job-hold-until-specified + JOB_INCOMING(REASON, NativeJobStatus.UNMAPPED), // job-incoming + JOB_INTERPRETING(REASON, NativeJobStatus.UNMAPPED), // job-interpreting + JOB_OUTGOING(REASON, NativeJobStatus.UNMAPPED), // job-outgoing + JOB_PASSWORD_WAIT(REASON, NativeJobStatus.USER_INTERVENTION), // job-password-wait + JOB_PRINTED_SUCCESSFULLY(REASON, NativeJobStatus.COMPLETE), // job-printed-successfully + JOB_PRINTED_WITH_ERRORS(REASON, NativeJobStatus.COMPLETE), // job-printed-with-errors + JOB_PRINTED_WITH_WARNINGS(REASON, NativeJobStatus.COMPLETE), // job-printed-with-warnings + JOB_PRINTING(REASON, NativeJobStatus.PRINTING), // job-printing + JOB_QUEUED(REASON, NativeJobStatus.SPOOLING), // job-queued + JOB_QUEUED_FOR_MARKER(REASON, NativeJobStatus.SPOOLING), // job-queued-for-marker + JOB_RELEASE_WAIT(REASON, NativeJobStatus.UNMAPPED), // job-release-wait + JOB_RESTARTABLE(REASON, NativeJobStatus.UNMAPPED), // job-restartable + JOB_RESUMING(REASON, NativeJobStatus.SPOOLING), // job-resuming + JOB_SAVED_SUCCESSFULLY(REASON, NativeJobStatus.RETAINED), // job-saved-successfully + JOB_SAVED_WITH_ERRORS(REASON, NativeJobStatus.RETAINED), // job-saved-with-errors + JOB_SAVED_WITH_WARNINGS(REASON, NativeJobStatus.RETAINED), // job-saved-with-warnings + JOB_SAVING(REASON, NativeJobStatus.UNMAPPED), // job-saving + JOB_SPOOLING(REASON, NativeJobStatus.UNMAPPED), // job-spooling + JOB_STREAMING(REASON, NativeJobStatus.UNMAPPED), // job-streaming + JOB_SUSPENDED(REASON, NativeJobStatus.PAUSED), // job-suspended + JOB_SUSPENDED_BY_OPERATOR(REASON, NativeJobStatus.PAUSED), // job-suspended-by-operator + JOB_SUSPENDED_BY_SYSTEM(REASON, NativeJobStatus.PAUSED), // job-suspended-by-system + JOB_SUSPENDED_BY_USER(REASON, NativeJobStatus.PAUSED), // job-suspended-by-user + JOB_SUSPENDING(REASON, NativeJobStatus.UNMAPPED), // job-suspending + JOB_TRANSFERRING(REASON, NativeJobStatus.UNMAPPED), // job-transferring + JOB_TRANSFORMING(REASON, NativeJobStatus.UNMAPPED), // job-transforming + PRINTER_STOPPED(REASON, NativeJobStatus.PAUSED), // printer-stopped + PRINTER_STOPPED_PARTLY(REASON, NativeJobStatus.UNMAPPED), // printer-stopped-partly + PROCESSING_TO_STOP_POINT(REASON, NativeJobStatus.UNMAPPED), // processing-to-stop-point + QUEUED_IN_DEVICE(REASON, NativeJobStatus.UNMAPPED), // queued-in-device + RESOURCES_ARE_NOT_READY(REASON, NativeJobStatus.UNMAPPED), // resources-are-not-ready + RESOURCES_ARE_NOT_SUPPORTED(REASON, NativeJobStatus.UNMAPPED), // resources-are-not-supported + SERVICE_OFF_LINE(REASON, NativeJobStatus.UNMAPPED), // service-off-line + SUBMISSION_INTERRUPTED(REASON, NativeJobStatus.UNMAPPED), // submission-interrupted + UNSUPPORTED_ATTRIBUTES_OR_VALUES(REASON, NativeJobStatus.UNMAPPED), // unsupported-attributes-or-values + UNSUPPORTED_COMPRESSION(REASON, NativeJobStatus.UNMAPPED), // unsupported-compression + UNSUPPORTED_DOCUMENT_FORMAT(REASON, NativeJobStatus.UNMAPPED), // unsupported-document-format + WAITING_FOR_USER_ACTION(REASON, NativeJobStatus.USER_INTERVENTION), // waiting-for-user-action + WARNINGS_DETECTED(REASON, NativeJobStatus.UNKNOWN); // warnings-detected + + private static final Logger log = LogManager.getLogger(CupsJobStatusMap.class); + private static SortedMap sortedReasonLookupTable; + private static SortedMap sortedStateLookupTable; + + private final NativeJobStatus parent; + private final CupsJobStatusType type; + + enum CupsJobStatusType { + STATE, + REASON; + } + + CupsJobStatusMap(CupsJobStatusType type, NativeJobStatus parent) { + this.type = type; + this.parent = parent; + } + + public static NativeJobStatus matchReason(String code) { + // Initialize a sorted map to speed up lookups + if(sortedReasonLookupTable == null) { + sortedReasonLookupTable = new TreeMap<>(); + for(CupsJobStatusMap value : values()) { + if(value.type == REASON) { + sortedReasonLookupTable.put(value.name().toLowerCase(Locale.ENGLISH).replace("_", "-"), value.parent); + } + } + } + + NativeJobStatus status = sortedReasonLookupTable.get(code); + if(status == null && !code.equalsIgnoreCase("none")) { + // Don't warn for "none" + log.warn("Printer job state-reason \"{}\" was not found", code); + } + return status; + } + + public static NativeJobStatus matchState(String state) { + // Initialize a sorted map to speed up lookups + if(sortedStateLookupTable == null) { + sortedStateLookupTable = new TreeMap<>(); + for(CupsJobStatusMap value : values()) { + if(value.type == STATE) { + sortedStateLookupTable.put(value.name().toLowerCase(Locale.ENGLISH).replace("_", "-"), value.parent); + } + } + } + return sortedStateLookupTable.getOrDefault(state, NativeJobStatus.UNKNOWN); + } + + @Override + public NativeJobStatus getParent() { + return parent; + } + + @Override + public Object getRawCode() { + return name().toLowerCase(Locale.ENGLISH).replace("_", "-"); + } +} diff --git a/old code/tray/src/qz/printer/status/job/NativeJobStatus.java b/old code/tray/src/qz/printer/status/job/NativeJobStatus.java new file mode 100755 index 0000000..050a901 --- /dev/null +++ b/old code/tray/src/qz/printer/status/job/NativeJobStatus.java @@ -0,0 +1,45 @@ +package qz.printer.status.job; + +import org.apache.logging.log4j.Level; +import qz.printer.status.NativeStatus; + +/** + * Created by kyle on 7/7/17. + */ +public enum NativeJobStatus implements NativeStatus { + ABORTED(Level.ERROR), + CANCELED(Level.WARN), + COMPLETE(Level.INFO), + DELETED(Level.INFO), + DELETING(Level.INFO), + ERROR(Level.ERROR), + OFFLINE(Level.ERROR), + PRINTING(Level.INFO), + SPOOLING(Level.INFO), + SCHEDULED(Level.INFO), + PAPEROUT(Level.WARN), + RETAINED(Level.INFO), + PAUSED(Level.WARN), + SENT(Level.INFO), + RESTART(Level.WARN), + RENDERING_LOCALLY(Level.INFO), + USER_INTERVENTION(Level.WARN), + UNMAPPED(Level.FATAL), // should never make it to the user + UNKNOWN(Level.INFO); + + private Level level; + + NativeJobStatus(Level level) { + this.level = level; + } + + @Override + public Level getLevel() { + return level; + } + + @Override + public NativeStatus getDefault() { + return UNKNOWN; + } +} diff --git a/old code/tray/src/qz/printer/status/job/WmiJobStatusMap.java b/old code/tray/src/qz/printer/status/job/WmiJobStatusMap.java new file mode 100755 index 0000000..d394b65 --- /dev/null +++ b/old code/tray/src/qz/printer/status/job/WmiJobStatusMap.java @@ -0,0 +1,60 @@ +package qz.printer.status.job; + +import qz.printer.status.NativeStatus; + +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Created by tresf on 12/10/2020 + */ +public enum WmiJobStatusMap implements NativeStatus.NativeMap { + EMPTY(NativeJobStatus.UNKNOWN, -0x00000001), // Fallback for a no-status message + PAUSED(NativeJobStatus.PAUSED, 0x00000001), // Job is paused + ERROR(NativeJobStatus.ERROR, 0x00000002), // An error is associated with the job + DELETING(NativeJobStatus.DELETING, 0x00000004), // Job is being deleted + SPOOLING(NativeJobStatus.SPOOLING, 0x00000008), // Job is spooling + PRINTING(NativeJobStatus.PRINTING, 0x00000010), // Job is printing + OFFLINE(NativeJobStatus.OFFLINE, 0x00000020), // Job is printing + PAPEROUT(NativeJobStatus.PAPEROUT, 0x00000040), // Printer is out of paper + PRINTED(NativeJobStatus.COMPLETE, 0x00000080), // Job has printed + DELETED(NativeJobStatus.DELETED, 0x00000100), // Job has been deleted + BLOCKED_DEVQ(NativeJobStatus.ABORTED, 0x00000200), // The driver cannot print the job + RESTART(NativeJobStatus.RESTART, 0x00000800), // Job has been restarted + COMPLETE(NativeJobStatus.SENT, 0x00001000), // Windows XP and later: Job is sent to the printer, but the job may not be printed yet + RETAINED(NativeJobStatus.RETAINED, 0x00002000), // Windows Vista and later: Job has been retained in the print queue and cannot be deleted (Edit: it actually can https://github.com/qzind/tray/issues/1305) + RENDERING_LOCALLY(NativeJobStatus.RENDERING_LOCALLY, 0x00004000), // Job rendering locally on the client + USER_INTERVENTION(NativeJobStatus.USER_INTERVENTION, 0x40000000); // Printer has an error that requires the user to do something + + private static SortedMap sortedLookupTable; + + private final NativeJobStatus parent; + private final int rawCode; + + WmiJobStatusMap(NativeJobStatus parent, int rawCode) { + this.parent = parent; + this.rawCode = rawCode; + } + + public static NativeJobStatus match(int code) { + // Initialize a sorted map to speed up lookups + if(sortedLookupTable == null) { + sortedLookupTable = new TreeMap<>(); + for(WmiJobStatusMap value : values()) { + sortedLookupTable.put(value.rawCode, value.parent); + } + } + + return sortedLookupTable.get(code); + } + + @Override + public NativeStatus getParent() { + return parent; + } + + @Override + public Object getRawCode() { + return rawCode; + } +} diff --git a/old code/tray/src/qz/printer/status/printer/CupsPrinterStatusMap.java b/old code/tray/src/qz/printer/status/printer/CupsPrinterStatusMap.java new file mode 100755 index 0000000..05c2305 --- /dev/null +++ b/old code/tray/src/qz/printer/status/printer/CupsPrinterStatusMap.java @@ -0,0 +1,959 @@ +package qz.printer.status.printer; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.printer.status.NativeStatus; +import qz.printer.status.Status; + +import java.util.*; + +import static qz.printer.status.printer.CupsPrinterStatusMap.CupsPrinterStatusType.*; + +public enum CupsPrinterStatusMap implements NativeStatus.NativeMap { + // printer-state + IDLE(STATE, NativePrinterStatus.OK), // idle + PROCESSING(STATE, NativePrinterStatus.PROCESSING), // processing + STOPPED(STATE, NativePrinterStatus.PAUSED), // stopped + + // printer-state-reasons. NativePrinterStatus.UNMAPPED will fallback to the printer-state instead + // Mapped printer-state-reasons + OFFLINE_REPORT(REASON, NativePrinterStatus.OFFLINE), // "offline-report" + OTHER(REASON, NativePrinterStatus.UNMAPPED), // "other" + MEDIA_NEEDED(REASON, NativePrinterStatus.PAPER_OUT), // "media-needed" + MEDIA_JAM(REASON, NativePrinterStatus.PAPER_JAM), // "media-jam" + MOVING_TO_PAUSED(REASON, NativePrinterStatus.OK), // "moving-to-paused" + PAUSED(REASON, NativePrinterStatus.UNMAPPED), // "paused" + SHUTDOWN(REASON, NativePrinterStatus.OFFLINE), // "shutdown" + CONNECTING_TO_DEVICE(REASON, NativePrinterStatus.PROCESSING), // "connecting-to-device" + TIMED_OUT(REASON, NativePrinterStatus.NOT_AVAILABLE), // "timed-out" + STOPPING(REASON, NativePrinterStatus.OK), // "stopping" + STOPPED_PARTLY(REASON, NativePrinterStatus.PAUSED), // "stopped-partly" + TONER_LOW(REASON, NativePrinterStatus.TONER_LOW), // "toner-low" + TONER_EMPTY(REASON, NativePrinterStatus.NO_TONER), // "toner-empty" + SPOOL_AREA_FULL(REASON, NativePrinterStatus.OUT_OF_MEMORY), // "spool-area-full" + COVER_OPEN(REASON, NativePrinterStatus.DOOR_OPEN), // "cover-open" + INTERLOCK_OPEN(REASON, NativePrinterStatus.DOOR_OPEN), // "interlock-open" + DOOR_OPEN(REASON, NativePrinterStatus.DOOR_OPEN), // "door-open" + INPUT_TRAY_MISSING(REASON, NativePrinterStatus.PAPER_PROBLEM), // "input-tray-missing" + MEDIA_LOW(REASON, NativePrinterStatus.PAPER_PROBLEM), // "media-low" + MEDIA_EMPTY(REASON, NativePrinterStatus.PAPER_OUT), // "media-empty" + OUTPUT_TRAY_MISSING(REASON, NativePrinterStatus.PAPER_PROBLEM), // "output-tray-missing" + //not a great match + OUTPUT_AREA_ALMOST_FULL(REASON, NativePrinterStatus.TONER_LOW), // "output-area-almost-full" + OUTPUT_AREA_FULL(REASON, NativePrinterStatus.OUTPUT_BIN_FULL), // "output-area-full" + MARKER_SUPPLY_LOW(REASON, NativePrinterStatus.TONER_LOW), // "marker-supply-low" + MARKER_SUPPLY_EMPTY(REASON, NativePrinterStatus.NO_TONER), // "marker-supply-empty" + // not a great match + MARKER_WASTE_ALMOST_FULL(REASON, NativePrinterStatus.TONER_LOW), // "marker-waste-almost-full" + MARKER_WASTE_FULL(REASON, NativePrinterStatus.NO_TONER), // "marker-waste-full" + FUSER_OVER_TEMP(REASON, NativePrinterStatus.WARMING_UP), // "fuser-over-temp" + FUSER_UNDER_TEMP(REASON, NativePrinterStatus.WARMING_UP), // "fuser-under-temp" + // not a great match + OPC_NEAR_EOL(REASON, NativePrinterStatus.TONER_LOW), // "opc-near-eol" + OPC_LIFE_OVER(REASON, NativePrinterStatus.NO_TONER), // "opc-life-over" + DEVELOPER_LOW(REASON, NativePrinterStatus.TONER_LOW), // "developer-low" + DEVELOPER_EMPTY(REASON, NativePrinterStatus.NO_TONER), // "developer-empty" + INTERPRETER_RESOURCE_UNAVAILABLE(REASON, NativePrinterStatus.SERVER_UNKNOWN), // "interpreter-resource-unavailable" + + // CUPS defined states (defined by CUPS, but not part of the IPP specification) + OFFLINE(REASON, NativePrinterStatus.OFFLINE), // "offline" + CUPS_INSECURE_FILTER_WARNING(REASON, NativePrinterStatus.SERVER_UNKNOWN), // "cups-insecure-filter-warning" + CUPS_MISSING_FILTER_WARNING(REASON, NativePrinterStatus.ERROR), // "cups-missing-filter-warning" + CUPS_WAITING_FOR_JOB_COMPLETED(REASON, NativePrinterStatus.PRINTING), // "cups-waiting-for-job-completed"); + + // Deprecated CUPS defined states (outdated or incorrect values known to occur) + CUPS_INSECURE_FILTER_ERROR(REASON, NativePrinterStatus.SERVER_UNKNOWN), // "cups-insecure-filter-error" + CUPS_MISSING_FILTER_ERROR(REASON, NativePrinterStatus.ERROR), // "cups-missing-filter-error" + CUPS_INSECURE_FILTER(REASON, NativePrinterStatus.SERVER_UNKNOWN), // "cups-insecure-filter" + CUPS_MISSING_FILTER(REASON, NativePrinterStatus.ERROR), // "cups-missing-filter" + + // SNMP statuses with no existing CUPS definition + SERVICE_NEEDED(REASON, NativePrinterStatus.UNMAPPED), // "service-needed" + + // Unmapped printer-state-reasons + ALERT_REMOVAL_OF_BINARY_CHANGE_ENTRY(REASON, NativePrinterStatus.UNMAPPED), // alert-removal-of-binary-change-entry + BANDER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // bander-added + BANDER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // bander-almost-empty + BANDER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // bander-almost-full + BANDER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // bander-at-limit + BANDER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // bander-closed + BANDER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // bander-configuration-change + BANDER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // bander-cover-closed + BANDER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // bander-cover-open + BANDER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // bander-empty + BANDER_FULL(REASON, NativePrinterStatus.UNMAPPED), // bander-full + BANDER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // bander-interlock-closed + BANDER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // bander-interlock-open + BANDER_JAM(REASON, NativePrinterStatus.UNMAPPED), // bander-jam + BANDER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // bander-life-almost-over + BANDER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // bander-life-over + BANDER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // bander-memory-exhausted + BANDER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // bander-missing + BANDER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // bander-motor-failure + BANDER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // bander-near-limit + BANDER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // bander-offline + BANDER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // bander-opened + BANDER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // bander-over-temperature + BANDER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // bander-power-saver + BANDER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // bander-recoverable-failure + BANDER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // bander-recoverable-storage + BANDER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // bander-removed + BANDER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // bander-resource-added + BANDER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // bander-resource-removed + BANDER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // bander-thermistor-failure + BANDER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // bander-timing-failure + BANDER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // bander-turned-off + BANDER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // bander-turned-on + BANDER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // bander-under-temperature + BANDER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // bander-unrecoverable-failure + BANDER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // bander-unrecoverable-storage-error + BANDER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // bander-warming-up + BINDER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // binder-added + BINDER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // binder-almost-empty + BINDER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // binder-almost-full + BINDER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // binder-at-limit + BINDER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // binder-closed + BINDER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // binder-configuration-change + BINDER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // binder-cover-closed + BINDER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // binder-cover-open + BINDER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // binder-empty + BINDER_FULL(REASON, NativePrinterStatus.UNMAPPED), // binder-full + BINDER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // binder-interlock-closed + BINDER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // binder-interlock-open + BINDER_JAM(REASON, NativePrinterStatus.UNMAPPED), // binder-jam + BINDER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // binder-life-almost-over + BINDER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // binder-life-over + BINDER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // binder-memory-exhausted + BINDER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // binder-missing + BINDER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // binder-motor-failure + BINDER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // binder-near-limit + BINDER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // binder-offline + BINDER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // binder-opened + BINDER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // binder-over-temperature + BINDER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // binder-power-saver + BINDER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // binder-recoverable-failure + BINDER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // binder-recoverable-storage + BINDER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // binder-removed + BINDER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // binder-resource-added + BINDER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // binder-resource-removed + BINDER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // binder-thermistor-failure + BINDER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // binder-timing-failure + BINDER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // binder-turned-off + BINDER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // binder-turned-on + BINDER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // binder-under-temperature + BINDER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // binder-unrecoverable-failure + BINDER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // binder-unrecoverable-storage-error + BINDER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // binder-warming-up + CAMERA_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // camera-failure + CHAMBER_COOLING(REASON, NativePrinterStatus.UNMAPPED), // chamber-cooling + CHAMBER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // chamber-failure + CHAMBER_HEATING(REASON, NativePrinterStatus.UNMAPPED), // chamber-heating + CHAMBER_TEMPERATURE_HIGH(REASON, NativePrinterStatus.UNMAPPED), // chamber-temperature-high + CHAMBER_TEMPERATURE_LOW(REASON, NativePrinterStatus.UNMAPPED), // chamber-temperature-low + CLEANER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // cleaner-life-almost-over + CLEANER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // cleaner-life-over + CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // configuration-change + DEACTIVATED(REASON, NativePrinterStatus.UNMAPPED), // deactivated + DELETED(REASON, NativePrinterStatus.UNMAPPED), // deleted + DIE_CUTTER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-added + DIE_CUTTER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-almost-empty + DIE_CUTTER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-almost-full + DIE_CUTTER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-at-limit + DIE_CUTTER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-closed + DIE_CUTTER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-configuration-change + DIE_CUTTER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-cover-closed + DIE_CUTTER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-cover-open + DIE_CUTTER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-empty + DIE_CUTTER_FULL(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-full + DIE_CUTTER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-interlock-closed + DIE_CUTTER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-interlock-open + DIE_CUTTER_JAM(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-jam + DIE_CUTTER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-life-almost-over + DIE_CUTTER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-life-over + DIE_CUTTER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-memory-exhausted + DIE_CUTTER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-missing + DIE_CUTTER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-motor-failure + DIE_CUTTER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-near-limit + DIE_CUTTER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-offline + DIE_CUTTER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-opened + DIE_CUTTER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-over-temperature + DIE_CUTTER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-power-saver + DIE_CUTTER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-recoverable-failure + DIE_CUTTER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-recoverable-storage + DIE_CUTTER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-removed + DIE_CUTTER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-resource-added + DIE_CUTTER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-resource-removed + DIE_CUTTER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-thermistor-failure + DIE_CUTTER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-timing-failure + DIE_CUTTER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-turned-off + DIE_CUTTER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-turned-on + DIE_CUTTER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-under-temperature + DIE_CUTTER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-unrecoverable-failure + DIE_CUTTER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-unrecoverable-storage-error + DIE_CUTTER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // die-cutter-warming-up + EXTRUDER_COOLING(REASON, NativePrinterStatus.UNMAPPED), // extruder-cooling + EXTRUDER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // extruder-failure + EXTRUDER_HEATING(REASON, NativePrinterStatus.UNMAPPED), // extruder-heating + EXTRUDER_JAM(REASON, NativePrinterStatus.UNMAPPED), // extruder-jam + EXTRUDER_TEMPERATURE_HIGH(REASON, NativePrinterStatus.UNMAPPED), // extruder-temperature-high + EXTRUDER_TEMPERATURE_LOW(REASON, NativePrinterStatus.UNMAPPED), // extruder-temperature-low + FAN_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // fan-failure + FAX_MODEM_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // fax-modem-life-almost-over + FAX_MODEM_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // fax-modem-life-over + FAX_MODEM_MISSING(REASON, NativePrinterStatus.UNMAPPED), // fax-modem-missing + FAX_MODEM_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // fax-modem-turned-off + FAX_MODEM_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // fax-modem-turned-on + FOLDER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // folder-added + FOLDER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // folder-almost-empty + FOLDER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // folder-almost-full + FOLDER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // folder-at-limit + FOLDER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // folder-closed + FOLDER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // folder-configuration-change + FOLDER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // folder-cover-closed + FOLDER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // folder-cover-open + FOLDER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // folder-empty + FOLDER_FULL(REASON, NativePrinterStatus.UNMAPPED), // folder-full + FOLDER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // folder-interlock-closed + FOLDER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // folder-interlock-open + FOLDER_JAM(REASON, NativePrinterStatus.UNMAPPED), // folder-jam + FOLDER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // folder-life-almost-over + FOLDER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // folder-life-over + FOLDER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // folder-memory-exhausted + FOLDER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // folder-missing + FOLDER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // folder-motor-failure + FOLDER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // folder-near-limit + FOLDER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // folder-offline + FOLDER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // folder-opened + FOLDER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // folder-over-temperature + FOLDER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // folder-power-saver + FOLDER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // folder-recoverable-failure + FOLDER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // folder-recoverable-storage + FOLDER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // folder-removed + FOLDER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // folder-resource-added + FOLDER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // folder-resource-removed + FOLDER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // folder-thermistor-failure + FOLDER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // folder-timing-failure + FOLDER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // folder-turned-off + FOLDER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // folder-turned-on + FOLDER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // folder-under-temperature + FOLDER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // folder-unrecoverable-failure + FOLDER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // folder-unrecoverable-storage-error + FOLDER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // folder-warming-up + HIBERNATE(REASON, NativePrinterStatus.UNMAPPED), // hibernate + HOLD_NEW_JOBS(REASON, NativePrinterStatus.UNMAPPED), // hold-new-jobs + IDENTIFY_PRINTER_REQUESTED(REASON, NativePrinterStatus.UNMAPPED), // identify-printer-requested + IMPRINTER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-added + IMPRINTER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // imprinter-almost-empty + IMPRINTER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // imprinter-almost-full + IMPRINTER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // imprinter-at-limit + IMPRINTER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-closed + IMPRINTER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-configuration-change + IMPRINTER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-cover-closed + IMPRINTER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // imprinter-cover-open + IMPRINTER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // imprinter-empty + IMPRINTER_FULL(REASON, NativePrinterStatus.UNMAPPED), // imprinter-full + IMPRINTER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-interlock-closed + IMPRINTER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // imprinter-interlock-open + IMPRINTER_JAM(REASON, NativePrinterStatus.UNMAPPED), // imprinter-jam + IMPRINTER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // imprinter-life-almost-over + IMPRINTER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // imprinter-life-over + IMPRINTER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-memory-exhausted + IMPRINTER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // imprinter-missing + IMPRINTER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-motor-failure + IMPRINTER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // imprinter-near-limit + IMPRINTER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-offline + IMPRINTER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-opened + IMPRINTER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-over-temperature + IMPRINTER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // imprinter-power-saver + IMPRINTER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-recoverable-failure + IMPRINTER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-recoverable-storage + IMPRINTER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-removed + IMPRINTER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-resource-added + IMPRINTER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // imprinter-resource-removed + IMPRINTER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-thermistor-failure + IMPRINTER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-timing-failure + IMPRINTER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // imprinter-turned-off + IMPRINTER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // imprinter-turned-on + IMPRINTER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-under-temperature + IMPRINTER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // imprinter-unrecoverable-failure + IMPRINTER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // imprinter-unrecoverable-storage-error + IMPRINTER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // imprinter-warming-up + INPUT_CANNOT_FEED_SIZE_SELECTED(REASON, NativePrinterStatus.UNMAPPED), // input-cannot-feed-size-selected + INPUT_MANUAL_INPUT_REQUEST(REASON, NativePrinterStatus.UNMAPPED), // input-manual-input-request + INPUT_MEDIA_COLOR_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // input-media-color-change + INPUT_MEDIA_FORM_PARTS_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // input-media-form-parts-change + INPUT_MEDIA_SIZE_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // input-media-size-change + INPUT_MEDIA_TRAY_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // input-media-tray-failure + INPUT_MEDIA_TRAY_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // input-media-tray-feed-error + INPUT_MEDIA_TRAY_JAM(REASON, NativePrinterStatus.UNMAPPED), // input-media-tray-jam + INPUT_MEDIA_TYPE_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // input-media-type-change + INPUT_MEDIA_WEIGHT_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // input-media-weight-change + INPUT_PICK_ROLLER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // input-pick-roller-failure + INPUT_PICK_ROLLER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // input-pick-roller-life-over + INPUT_PICK_ROLLER_LIFE_WARN(REASON, NativePrinterStatus.UNMAPPED), // input-pick-roller-life-warn + INPUT_PICK_ROLLER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // input-pick-roller-missing + INPUT_TRAY_ELEVATION_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // input-tray-elevation-failure + INPUT_TRAY_POSITION_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // input-tray-position-failure + INSERTER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // inserter-added + INSERTER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // inserter-almost-empty + INSERTER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // inserter-almost-full + INSERTER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // inserter-at-limit + INSERTER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // inserter-closed + INSERTER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // inserter-configuration-change + INSERTER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // inserter-cover-closed + INSERTER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // inserter-cover-open + INSERTER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // inserter-empty + INSERTER_FULL(REASON, NativePrinterStatus.UNMAPPED), // inserter-full + INSERTER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // inserter-interlock-closed + INSERTER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // inserter-interlock-open + INSERTER_JAM(REASON, NativePrinterStatus.UNMAPPED), // inserter-jam + INSERTER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // inserter-life-almost-over + INSERTER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // inserter-life-over + INSERTER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // inserter-memory-exhausted + INSERTER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // inserter-missing + INSERTER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-motor-failure + INSERTER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // inserter-near-limit + INSERTER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // inserter-offline + INSERTER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // inserter-opened + INSERTER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-over-temperature + INSERTER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // inserter-power-saver + INSERTER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-recoverable-failure + INSERTER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // inserter-recoverable-storage + INSERTER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // inserter-removed + INSERTER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // inserter-resource-added + INSERTER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // inserter-resource-removed + INSERTER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-thermistor-failure + INSERTER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-timing-failure + INSERTER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // inserter-turned-off + INSERTER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // inserter-turned-on + INSERTER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-under-temperature + INSERTER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // inserter-unrecoverable-failure + INSERTER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // inserter-unrecoverable-storage-error + INSERTER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // inserter-warming-up + INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // interlock-closed + INTERPRETER_CARTRIDGE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // interpreter-cartridge-added + INTERPRETER_CARTRIDGE_DELETED(REASON, NativePrinterStatus.UNMAPPED), // interpreter-cartridge-deleted + INTERPRETER_COMPLEX_PAGE_ENCOUNTERED(REASON, NativePrinterStatus.UNMAPPED), // interpreter-complex-page-encountered + INTERPRETER_MEMORY_DECREASE(REASON, NativePrinterStatus.UNMAPPED), // interpreter-memory-decrease + INTERPRETER_MEMORY_INCREASE(REASON, NativePrinterStatus.UNMAPPED), // interpreter-memory-increase + INTERPRETER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // interpreter-resource-added + INTERPRETER_RESOURCE_DELETED(REASON, NativePrinterStatus.UNMAPPED), // interpreter-resource-deleted + LAMP_AT_EOL(REASON, NativePrinterStatus.UNMAPPED), // lamp-at-eol + LAMP_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // lamp-failure + LAMP_NEAR_EOL(REASON, NativePrinterStatus.UNMAPPED), // lamp-near-eol + LASER_AT_EOL(REASON, NativePrinterStatus.UNMAPPED), // laser-at-eol + LASER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // laser-failure + LASER_NEAR_EOL(REASON, NativePrinterStatus.UNMAPPED), // laser-near-eol + MAKE_ENVELOPE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-added + MAKE_ENVELOPE_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-almost-empty + MAKE_ENVELOPE_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-almost-full + MAKE_ENVELOPE_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-at-limit + MAKE_ENVELOPE_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-closed + MAKE_ENVELOPE_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-configuration-change + MAKE_ENVELOPE_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-cover-closed + MAKE_ENVELOPE_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-cover-open + MAKE_ENVELOPE_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-empty + MAKE_ENVELOPE_FULL(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-full + MAKE_ENVELOPE_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-interlock-closed + MAKE_ENVELOPE_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-interlock-open + MAKE_ENVELOPE_JAM(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-jam + MAKE_ENVELOPE_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-life-almost-over + MAKE_ENVELOPE_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-life-over + MAKE_ENVELOPE_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-memory-exhausted + MAKE_ENVELOPE_MISSING(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-missing + MAKE_ENVELOPE_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-motor-failure + MAKE_ENVELOPE_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-near-limit + MAKE_ENVELOPE_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-offline + MAKE_ENVELOPE_OPENED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-opened + MAKE_ENVELOPE_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-over-temperature + MAKE_ENVELOPE_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-power-saver + MAKE_ENVELOPE_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-recoverable-failure + MAKE_ENVELOPE_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-recoverable-storage + MAKE_ENVELOPE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-removed + MAKE_ENVELOPE_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-resource-added + MAKE_ENVELOPE_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-resource-removed + MAKE_ENVELOPE_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-thermistor-failure + MAKE_ENVELOPE_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-timing-failure + MAKE_ENVELOPE_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-turned-off + MAKE_ENVELOPE_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-turned-on + MAKE_ENVELOPE_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-under-temperature + MAKE_ENVELOPE_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-unrecoverable-failure + MAKE_ENVELOPE_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-unrecoverable-storage-error + MAKE_ENVELOPE_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // make-envelope-warming-up + MARKER_ADJUSTING_PRINT_QUALITY(REASON, NativePrinterStatus.UNMAPPED), // marker-adjusting-print-quality + MARKER_CLEANER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-cleaner-missing + MARKER_DEVELOPER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-developer-almost-empty + MARKER_DEVELOPER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-developer-empty + MARKER_DEVELOPER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-developer-missing + MARKER_FUSER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-fuser-missing + MARKER_FUSER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // marker-fuser-thermistor-failure + MARKER_FUSER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // marker-fuser-timing-failure + MARKER_INK_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-ink-almost-empty + MARKER_INK_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-ink-empty + MARKER_INK_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-ink-missing + MARKER_OPC_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-opc-missing + MARKER_PRINT_RIBBON_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-print-ribbon-almost-empty + MARKER_PRINT_RIBBON_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-print-ribbon-empty + MARKER_PRINT_RIBBON_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-print-ribbon-missing + MARKER_SUPPLY_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // marker-supply-almost-empty + MARKER_SUPPLY_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-supply-missing + MARKER_TONER_CARTRIDGE_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-toner-cartridge-missing + MARKER_TONER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-toner-missing + MARKER_WASTE_INK_RECEPTACLE_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-ink-receptacle-almost-full + MARKER_WASTE_INK_RECEPTACLE_FULL(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-ink-receptacle-full + MARKER_WASTE_INK_RECEPTACLE_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-ink-receptacle-missing + MARKER_WASTE_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-missing + MARKER_WASTE_TONER_RECEPTACLE_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-toner-receptacle-almost-full + MARKER_WASTE_TONER_RECEPTACLE_FULL(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-toner-receptacle-full + MARKER_WASTE_TONER_RECEPTACLE_MISSING(REASON, NativePrinterStatus.UNMAPPED), // marker-waste-toner-receptacle-missing + MATERIAL_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // material-empty + MATERIAL_LOW(REASON, NativePrinterStatus.UNMAPPED), // material-low + MATERIAL_NEEDED(REASON, NativePrinterStatus.UNMAPPED), // material-needed + MEDIA_DRYING(REASON, NativePrinterStatus.UNMAPPED), // media-drying + MEDIA_PATH_CANNOT_DUPLEX_MEDIA_SELECTED(REASON, NativePrinterStatus.UNMAPPED), // media-path-cannot-duplex-media-selected + MEDIA_PATH_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // media-path-failure + MEDIA_PATH_INPUT_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // media-path-input-empty + MEDIA_PATH_INPUT_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // media-path-input-feed-error + MEDIA_PATH_INPUT_JAM(REASON, NativePrinterStatus.UNMAPPED), // media-path-input-jam + MEDIA_PATH_INPUT_REQUEST(REASON, NativePrinterStatus.UNMAPPED), // media-path-input-request + MEDIA_PATH_JAM(REASON, NativePrinterStatus.UNMAPPED), // media-path-jam + MEDIA_PATH_MEDIA_TRAY_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // media-path-media-tray-almost-full + MEDIA_PATH_MEDIA_TRAY_FULL(REASON, NativePrinterStatus.UNMAPPED), // media-path-media-tray-full + MEDIA_PATH_MEDIA_TRAY_MISSING(REASON, NativePrinterStatus.UNMAPPED), // media-path-media-tray-missing + MEDIA_PATH_OUTPUT_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // media-path-output-feed-error + MEDIA_PATH_OUTPUT_FULL(REASON, NativePrinterStatus.UNMAPPED), // media-path-output-full + MEDIA_PATH_OUTPUT_JAM(REASON, NativePrinterStatus.UNMAPPED), // media-path-output-jam + MEDIA_PATH_PICK_ROLLER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // media-path-pick-roller-failure + MEDIA_PATH_PICK_ROLLER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // media-path-pick-roller-life-over + MEDIA_PATH_PICK_ROLLER_LIFE_WARN(REASON, NativePrinterStatus.UNMAPPED), // media-path-pick-roller-life-warn + MEDIA_PATH_PICK_ROLLER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // media-path-pick-roller-missing + MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // motor-failure + OUTPUT_MAILBOX_SELECT_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // output-mailbox-select-failure + OUTPUT_MEDIA_TRAY_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // output-media-tray-failure + OUTPUT_MEDIA_TRAY_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // output-media-tray-feed-error + OUTPUT_MEDIA_TRAY_JAM(REASON, NativePrinterStatus.UNMAPPED), // output-media-tray-jam + PERFORATER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // perforater-added + PERFORATER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // perforater-almost-empty + PERFORATER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // perforater-almost-full + PERFORATER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // perforater-at-limit + PERFORATER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // perforater-closed + PERFORATER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // perforater-configuration-change + PERFORATER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // perforater-cover-closed + PERFORATER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // perforater-cover-open + PERFORATER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // perforater-empty + PERFORATER_FULL(REASON, NativePrinterStatus.UNMAPPED), // perforater-full + PERFORATER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // perforater-interlock-closed + PERFORATER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // perforater-interlock-open + PERFORATER_JAM(REASON, NativePrinterStatus.UNMAPPED), // perforater-jam + PERFORATER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // perforater-life-almost-over + PERFORATER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // perforater-life-over + PERFORATER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // perforater-memory-exhausted + PERFORATER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // perforater-missing + PERFORATER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-motor-failure + PERFORATER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // perforater-near-limit + PERFORATER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // perforater-offline + PERFORATER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // perforater-opened + PERFORATER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-over-temperature + PERFORATER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // perforater-power-saver + PERFORATER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-recoverable-failure + PERFORATER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // perforater-recoverable-storage + PERFORATER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // perforater-removed + PERFORATER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // perforater-resource-added + PERFORATER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // perforater-resource-removed + PERFORATER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-thermistor-failure + PERFORATER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-timing-failure + PERFORATER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // perforater-turned-off + PERFORATER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // perforater-turned-on + PERFORATER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-under-temperature + PERFORATER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // perforater-unrecoverable-failure + PERFORATER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // perforater-unrecoverable-storage-error + PERFORATER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // perforater-warming-up + PLATFORM_COOLING(REASON, NativePrinterStatus.UNMAPPED), // platform-cooling + PLATFORM_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // platform-failure + PLATFORM_HEATING(REASON, NativePrinterStatus.UNMAPPED), // platform-heating + PLATFORM_TEMPERATURE_HIGH(REASON, NativePrinterStatus.UNMAPPED), // platform-temperature-high + PLATFORM_TEMPERATURE_LOW(REASON, NativePrinterStatus.UNMAPPED), // platform-temperature-low + POWER_DOWN(REASON, NativePrinterStatus.UNMAPPED), // power-down + POWER_UP(REASON, NativePrinterStatus.UNMAPPED), // power-up + PRINTER_MANUAL_RESET(REASON, NativePrinterStatus.UNMAPPED), // printer-manual-reset + PRINTER_NMS_RESET(REASON, NativePrinterStatus.UNMAPPED), // printer-nms-reset + PRINTER_READY_TO_PRINT(REASON, NativePrinterStatus.UNMAPPED), // printer-ready-to-print + PUNCHER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // puncher-added + PUNCHER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // puncher-almost-empty + PUNCHER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // puncher-almost-full + PUNCHER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // puncher-at-limit + PUNCHER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // puncher-closed + PUNCHER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // puncher-configuration-change + PUNCHER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // puncher-cover-closed + PUNCHER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // puncher-cover-open + PUNCHER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // puncher-empty + PUNCHER_FULL(REASON, NativePrinterStatus.UNMAPPED), // puncher-full + PUNCHER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // puncher-interlock-closed + PUNCHER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // puncher-interlock-open + PUNCHER_JAM(REASON, NativePrinterStatus.UNMAPPED), // puncher-jam + PUNCHER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // puncher-life-almost-over + PUNCHER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // puncher-life-over + PUNCHER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // puncher-memory-exhausted + PUNCHER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // puncher-missing + PUNCHER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-motor-failure + PUNCHER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // puncher-near-limit + PUNCHER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // puncher-offline + PUNCHER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // puncher-opened + PUNCHER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-over-temperature + PUNCHER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // puncher-power-saver + PUNCHER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-recoverable-failure + PUNCHER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // puncher-recoverable-storage + PUNCHER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // puncher-removed + PUNCHER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // puncher-resource-added + PUNCHER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // puncher-resource-removed + PUNCHER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-thermistor-failure + PUNCHER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-timing-failure + PUNCHER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // puncher-turned-off + PUNCHER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // puncher-turned-on + PUNCHER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-under-temperature + PUNCHER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // puncher-unrecoverable-failure + PUNCHER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // puncher-unrecoverable-storage-error + PUNCHER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // puncher-warming-up + RESUMING(REASON, NativePrinterStatus.UNMAPPED), // resuming + SCAN_MEDIA_PATH_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-failure + SCAN_MEDIA_PATH_INPUT_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-input-empty + SCAN_MEDIA_PATH_INPUT_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-input-feed-error + SCAN_MEDIA_PATH_INPUT_JAM(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-input-jam + SCAN_MEDIA_PATH_INPUT_REQUEST(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-input-request + SCAN_MEDIA_PATH_JAM(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-jam + SCAN_MEDIA_PATH_OUTPUT_FEED_ERROR(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-output-feed-error + SCAN_MEDIA_PATH_OUTPUT_FULL(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-output-full + SCAN_MEDIA_PATH_OUTPUT_JAM(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-output-jam + SCAN_MEDIA_PATH_PICK_ROLLER_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-pick-roller-failure + SCAN_MEDIA_PATH_PICK_ROLLER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-pick-roller-life-over + SCAN_MEDIA_PATH_PICK_ROLLER_LIFE_WARN(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-pick-roller-life-warn + SCAN_MEDIA_PATH_PICK_ROLLER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-pick-roller-missing + SCAN_MEDIA_PATH_TRAY_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-tray-almost-full + SCAN_MEDIA_PATH_TRAY_FULL(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-tray-full + SCAN_MEDIA_PATH_TRAY_MISSING(REASON, NativePrinterStatus.UNMAPPED), // scan-media-path-tray-missing + SCANNER_LIGHT_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // scanner-light-failure + SCANNER_LIGHT_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // scanner-light-life-almost-over + SCANNER_LIGHT_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // scanner-light-life-over + SCANNER_LIGHT_MISSING(REASON, NativePrinterStatus.UNMAPPED), // scanner-light-missing + SCANNER_SENSOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // scanner-sensor-failure + SCANNER_SENSOR_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // scanner-sensor-life-almost-over + SCANNER_SENSOR_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // scanner-sensor-life-over + SCANNER_SENSOR_MISSING(REASON, NativePrinterStatus.UNMAPPED), // scanner-sensor-missing + SEPARATION_CUTTER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-added + SEPARATION_CUTTER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-almost-empty + SEPARATION_CUTTER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-almost-full + SEPARATION_CUTTER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-at-limit + SEPARATION_CUTTER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-closed + SEPARATION_CUTTER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-configuration-change + SEPARATION_CUTTER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-cover-closed + SEPARATION_CUTTER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-cover-open + SEPARATION_CUTTER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-empty + SEPARATION_CUTTER_FULL(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-full + SEPARATION_CUTTER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-interlock-closed + SEPARATION_CUTTER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-interlock-open + SEPARATION_CUTTER_JAM(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-jam + SEPARATION_CUTTER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-life-almost-over + SEPARATION_CUTTER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-life-over + SEPARATION_CUTTER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-memory-exhausted + SEPARATION_CUTTER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-missing + SEPARATION_CUTTER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-motor-failure + SEPARATION_CUTTER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-near-limit + SEPARATION_CUTTER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-offline + SEPARATION_CUTTER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-opened + SEPARATION_CUTTER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-over-temperature + SEPARATION_CUTTER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-power-saver + SEPARATION_CUTTER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-recoverable-failure + SEPARATION_CUTTER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-recoverable-storage + SEPARATION_CUTTER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-removed + SEPARATION_CUTTER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-resource-added + SEPARATION_CUTTER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-resource-removed + SEPARATION_CUTTER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-thermistor-failure + SEPARATION_CUTTER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-timing-failure + SEPARATION_CUTTER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-turned-off + SEPARATION_CUTTER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-turned-on + SEPARATION_CUTTER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-under-temperature + SEPARATION_CUTTER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-unrecoverable-failure + SEPARATION_CUTTER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-unrecoverable-storage-error + SEPARATION_CUTTER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // separation-cutter-warming-up + SHEET_ROTATOR_ADDED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-added + SHEET_ROTATOR_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-almost-empty + SHEET_ROTATOR_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-almost-full + SHEET_ROTATOR_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-at-limit + SHEET_ROTATOR_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-closed + SHEET_ROTATOR_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-configuration-change + SHEET_ROTATOR_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-cover-closed + SHEET_ROTATOR_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-cover-open + SHEET_ROTATOR_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-empty + SHEET_ROTATOR_FULL(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-full + SHEET_ROTATOR_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-interlock-closed + SHEET_ROTATOR_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-interlock-open + SHEET_ROTATOR_JAM(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-jam + SHEET_ROTATOR_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-life-almost-over + SHEET_ROTATOR_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-life-over + SHEET_ROTATOR_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-memory-exhausted + SHEET_ROTATOR_MISSING(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-missing + SHEET_ROTATOR_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-motor-failure + SHEET_ROTATOR_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-near-limit + SHEET_ROTATOR_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-offline + SHEET_ROTATOR_OPENED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-opened + SHEET_ROTATOR_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-over-temperature + SHEET_ROTATOR_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-power-saver + SHEET_ROTATOR_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-recoverable-failure + SHEET_ROTATOR_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-recoverable-storage + SHEET_ROTATOR_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-removed + SHEET_ROTATOR_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-resource-added + SHEET_ROTATOR_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-resource-removed + SHEET_ROTATOR_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-thermistor-failure + SHEET_ROTATOR_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-timing-failure + SHEET_ROTATOR_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-turned-off + SHEET_ROTATOR_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-turned-on + SHEET_ROTATOR_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-under-temperature + SHEET_ROTATOR_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-unrecoverable-failure + SHEET_ROTATOR_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-unrecoverable-storage-error + SHEET_ROTATOR_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // sheet-rotator-warming-up + SLITTER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // slitter-added + SLITTER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // slitter-almost-empty + SLITTER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // slitter-almost-full + SLITTER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // slitter-at-limit + SLITTER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // slitter-closed + SLITTER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // slitter-configuration-change + SLITTER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // slitter-cover-closed + SLITTER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // slitter-cover-open + SLITTER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // slitter-empty + SLITTER_FULL(REASON, NativePrinterStatus.UNMAPPED), // slitter-full + SLITTER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // slitter-interlock-closed + SLITTER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // slitter-interlock-open + SLITTER_JAM(REASON, NativePrinterStatus.UNMAPPED), // slitter-jam + SLITTER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // slitter-life-almost-over + SLITTER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // slitter-life-over + SLITTER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // slitter-memory-exhausted + SLITTER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // slitter-missing + SLITTER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-motor-failure + SLITTER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // slitter-near-limit + SLITTER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // slitter-offline + SLITTER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // slitter-opened + SLITTER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-over-temperature + SLITTER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // slitter-power-saver + SLITTER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-recoverable-failure + SLITTER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // slitter-recoverable-storage + SLITTER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // slitter-removed + SLITTER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // slitter-resource-added + SLITTER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // slitter-resource-removed + SLITTER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-thermistor-failure + SLITTER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-timing-failure + SLITTER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // slitter-turned-off + SLITTER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // slitter-turned-on + SLITTER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-under-temperature + SLITTER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // slitter-unrecoverable-failure + SLITTER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // slitter-unrecoverable-storage-error + SLITTER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // slitter-warming-up + STACKER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stacker-added + STACKER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stacker-almost-empty + STACKER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // stacker-almost-full + STACKER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stacker-at-limit + STACKER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stacker-closed + STACKER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // stacker-configuration-change + STACKER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stacker-cover-closed + STACKER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stacker-cover-open + STACKER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stacker-empty + STACKER_FULL(REASON, NativePrinterStatus.UNMAPPED), // stacker-full + STACKER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stacker-interlock-closed + STACKER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stacker-interlock-open + STACKER_JAM(REASON, NativePrinterStatus.UNMAPPED), // stacker-jam + STACKER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // stacker-life-almost-over + STACKER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // stacker-life-over + STACKER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // stacker-memory-exhausted + STACKER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // stacker-missing + STACKER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-motor-failure + STACKER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stacker-near-limit + STACKER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // stacker-offline + STACKER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // stacker-opened + STACKER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-over-temperature + STACKER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // stacker-power-saver + STACKER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-recoverable-failure + STACKER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // stacker-recoverable-storage + STACKER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stacker-removed + STACKER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stacker-resource-added + STACKER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stacker-resource-removed + STACKER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-thermistor-failure + STACKER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-timing-failure + STACKER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // stacker-turned-off + STACKER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // stacker-turned-on + STACKER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-under-temperature + STACKER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stacker-unrecoverable-failure + STACKER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // stacker-unrecoverable-storage-error + STACKER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // stacker-warming-up + STANDBY(REASON, NativePrinterStatus.UNMAPPED), // standby + STAPLER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stapler-added + STAPLER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stapler-almost-empty + STAPLER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // stapler-almost-full + STAPLER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stapler-at-limit + STAPLER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stapler-closed + STAPLER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // stapler-configuration-change + STAPLER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stapler-cover-closed + STAPLER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stapler-cover-open + STAPLER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stapler-empty + STAPLER_FULL(REASON, NativePrinterStatus.UNMAPPED), // stapler-full + STAPLER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stapler-interlock-closed + STAPLER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stapler-interlock-open + STAPLER_JAM(REASON, NativePrinterStatus.UNMAPPED), // stapler-jam + STAPLER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // stapler-life-almost-over + STAPLER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // stapler-life-over + STAPLER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // stapler-memory-exhausted + STAPLER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // stapler-missing + STAPLER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-motor-failure + STAPLER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stapler-near-limit + STAPLER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // stapler-offline + STAPLER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // stapler-opened + STAPLER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-over-temperature + STAPLER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // stapler-power-saver + STAPLER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-recoverable-failure + STAPLER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // stapler-recoverable-storage + STAPLER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stapler-removed + STAPLER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stapler-resource-added + STAPLER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stapler-resource-removed + STAPLER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-thermistor-failure + STAPLER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-timing-failure + STAPLER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // stapler-turned-off + STAPLER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // stapler-turned-on + STAPLER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-under-temperature + STAPLER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stapler-unrecoverable-failure + STAPLER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // stapler-unrecoverable-storage-error + STAPLER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // stapler-warming-up + STITCHER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-added + STITCHER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stitcher-almost-empty + STITCHER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // stitcher-almost-full + STITCHER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stitcher-at-limit + STITCHER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-closed + STITCHER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-configuration-change + STITCHER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-cover-closed + STITCHER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stitcher-cover-open + STITCHER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // stitcher-empty + STITCHER_FULL(REASON, NativePrinterStatus.UNMAPPED), // stitcher-full + STITCHER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-interlock-closed + STITCHER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // stitcher-interlock-open + STITCHER_JAM(REASON, NativePrinterStatus.UNMAPPED), // stitcher-jam + STITCHER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // stitcher-life-almost-over + STITCHER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // stitcher-life-over + STITCHER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-memory-exhausted + STITCHER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // stitcher-missing + STITCHER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-motor-failure + STITCHER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // stitcher-near-limit + STITCHER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-offline + STITCHER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-opened + STITCHER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-over-temperature + STITCHER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // stitcher-power-saver + STITCHER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-recoverable-failure + STITCHER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-recoverable-storage + STITCHER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-removed + STITCHER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-resource-added + STITCHER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // stitcher-resource-removed + STITCHER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-thermistor-failure + STITCHER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-timing-failure + STITCHER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // stitcher-turned-off + STITCHER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // stitcher-turned-on + STITCHER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-under-temperature + STITCHER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // stitcher-unrecoverable-failure + STITCHER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // stitcher-unrecoverable-storage-error + STITCHER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // stitcher-warming-up + SUBUNIT_ADDED(REASON, NativePrinterStatus.UNMAPPED), // subunit-added + SUBUNIT_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // subunit-almost-empty + SUBUNIT_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // subunit-almost-full + SUBUNIT_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // subunit-at-limit + SUBUNIT_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // subunit-closed + SUBUNIT_COOLING_DOWN(REASON, NativePrinterStatus.UNMAPPED), // subunit-cooling-down + SUBUNIT_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // subunit-empty + SUBUNIT_FULL(REASON, NativePrinterStatus.UNMAPPED), // subunit-full + SUBUNIT_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // subunit-life-almost-over + SUBUNIT_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // subunit-life-over + SUBUNIT_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // subunit-memory-exhausted + SUBUNIT_MISSING(REASON, NativePrinterStatus.UNMAPPED), // subunit-missing + SUBUNIT_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-motor-failure + SUBUNIT_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // subunit-near-limit + SUBUNIT_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // subunit-offline + SUBUNIT_OPENED(REASON, NativePrinterStatus.UNMAPPED), // subunit-opened + SUBUNIT_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-over-temperature + SUBUNIT_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // subunit-power-saver + SUBUNIT_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-recoverable-failure + SUBUNIT_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // subunit-recoverable-storage + SUBUNIT_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // subunit-removed + SUBUNIT_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // subunit-resource-added + SUBUNIT_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // subunit-resource-removed + SUBUNIT_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-thermistor-failure + SUBUNIT_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-timing-Failure + SUBUNIT_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // subunit-turned-off + SUBUNIT_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // subunit-turned-on + SUBUNIT_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-under-temperature + SUBUNIT_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // subunit-unrecoverable-failure + SUBUNIT_UNRECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // subunit-unrecoverable-storage + SUBUNIT_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // subunit-warming-up + SUSPEND(REASON, NativePrinterStatus.UNMAPPED), // suspend + TESTING(REASON, NativePrinterStatus.UNMAPPED), // testing + TRIMMER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-added + TRIMMER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // trimmer-almost-empty + TRIMMER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // trimmer-almost-full + TRIMMER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // trimmer-at-limit + TRIMMER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-closed + TRIMMER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-configuration-change + TRIMMER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-cover-closed + TRIMMER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // trimmer-cover-open + TRIMMER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // trimmer-empty + TRIMMER_FULL(REASON, NativePrinterStatus.UNMAPPED), // trimmer-full + TRIMMER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-interlock-closed + TRIMMER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // trimmer-interlock-open + TRIMMER_JAM(REASON, NativePrinterStatus.UNMAPPED), // trimmer-jam + TRIMMER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // trimmer-life-almost-over + TRIMMER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // trimmer-life-over + TRIMMER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-memory-exhausted + TRIMMER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // trimmer-missing + TRIMMER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-motor-failure + TRIMMER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // trimmer-near-limit + TRIMMER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-offline + TRIMMER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-opened + TRIMMER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-over-temperature + TRIMMER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // trimmer-power-saver + TRIMMER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-recoverable-failure + TRIMMER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-recoverable-storage + TRIMMER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-removed + TRIMMER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-resource-added + TRIMMER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // trimmer-resource-removed + TRIMMER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-thermistor-failure + TRIMMER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-timing-failure + TRIMMER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // trimmer-turned-off + TRIMMER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // trimmer-turned-on + TRIMMER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-under-temperature + TRIMMER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // trimmer-unrecoverable-failure + TRIMMER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // trimmer-unrecoverable-storage-error + TRIMMER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED), // trimmer-warming-up + UNKNOWN(REASON, NativePrinterStatus.UNMAPPED), // unknown + WRAPPER_ADDED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-added + WRAPPER_ALMOST_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // wrapper-almost-empty + WRAPPER_ALMOST_FULL(REASON, NativePrinterStatus.UNMAPPED), // wrapper-almost-full + WRAPPER_AT_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // wrapper-at-limit + WRAPPER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-closed + WRAPPER_CONFIGURATION_CHANGE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-configuration-change + WRAPPER_COVER_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-cover-closed + WRAPPER_COVER_OPEN(REASON, NativePrinterStatus.UNMAPPED), // wrapper-cover-open + WRAPPER_EMPTY(REASON, NativePrinterStatus.UNMAPPED), // wrapper-empty + WRAPPER_FULL(REASON, NativePrinterStatus.UNMAPPED), // wrapper-full + WRAPPER_INTERLOCK_CLOSED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-interlock-closed + WRAPPER_INTERLOCK_OPEN(REASON, NativePrinterStatus.UNMAPPED), // wrapper-interlock-open + WRAPPER_JAM(REASON, NativePrinterStatus.UNMAPPED), // wrapper-jam + WRAPPER_LIFE_ALMOST_OVER(REASON, NativePrinterStatus.UNMAPPED), // wrapper-life-almost-over + WRAPPER_LIFE_OVER(REASON, NativePrinterStatus.UNMAPPED), // wrapper-life-over + WRAPPER_MEMORY_EXHAUSTED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-memory-exhausted + WRAPPER_MISSING(REASON, NativePrinterStatus.UNMAPPED), // wrapper-missing + WRAPPER_MOTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-motor-failure + WRAPPER_NEAR_LIMIT(REASON, NativePrinterStatus.UNMAPPED), // wrapper-near-limit + WRAPPER_OFFLINE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-offline + WRAPPER_OPENED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-opened + WRAPPER_OVER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-over-temperature + WRAPPER_POWER_SAVER(REASON, NativePrinterStatus.UNMAPPED), // wrapper-power-saver + WRAPPER_RECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-recoverable-failure + WRAPPER_RECOVERABLE_STORAGE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-recoverable-storage + WRAPPER_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-removed + WRAPPER_RESOURCE_ADDED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-resource-added + WRAPPER_RESOURCE_REMOVED(REASON, NativePrinterStatus.UNMAPPED), // wrapper-resource-removed + WRAPPER_THERMISTOR_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-thermistor-failure + WRAPPER_TIMING_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-timing-failure + WRAPPER_TURNED_OFF(REASON, NativePrinterStatus.UNMAPPED), // wrapper-turned-off + WRAPPER_TURNED_ON(REASON, NativePrinterStatus.UNMAPPED), // wrapper-turned-on + WRAPPER_UNDER_TEMPERATURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-under-temperature + WRAPPER_UNRECOVERABLE_FAILURE(REASON, NativePrinterStatus.UNMAPPED), // wrapper-unrecoverable-failure + WRAPPER_UNRECOVERABLE_STORAGE_ERROR(REASON, NativePrinterStatus.UNMAPPED), // wrapper-unrecoverable-storage-error + WRAPPER_WARMING_UP(REASON, NativePrinterStatus.UNMAPPED); // wrapper-warming-up + + private static final Logger log = LogManager.getLogger(CupsPrinterStatusMap.class); + private static final String[] SNMP_REDUNDANT_SUFFIXES = { "-warning", "-report" }; + public static SortedMap sortedReasonLookupTable; + public static SortedMap sortedStateLookupTable; + + private NativePrinterStatus parent; + private CupsPrinterStatusType type; + + enum CupsPrinterStatusType { + STATE, + REASON; + } + + CupsPrinterStatusMap(CupsPrinterStatusType type, NativePrinterStatus parent) { + this.type = type; + this.parent = parent; + } + + public static NativePrinterStatus matchReason(String code) { + // Initialize a sorted map to speed up lookups + if(sortedReasonLookupTable == null) { + sortedReasonLookupTable = new TreeMap<>(); + for(CupsPrinterStatusMap value : values()) { + if(value.type == REASON) { + sortedReasonLookupTable.put(value.name().toLowerCase(Locale.ENGLISH).replace("_", "-"), value.parent); + } + } + } + NativePrinterStatus status = sortedReasonLookupTable.get(code); + return status; + } + + public static NativePrinterStatus matchState(String state) { + // Initialize a sorted map to speed up lookups + if(sortedStateLookupTable == null) { + sortedStateLookupTable = new TreeMap<>(); + for(CupsPrinterStatusMap value : values()) { + if(value.type == STATE) { + sortedStateLookupTable.put(value.name().toLowerCase(Locale.ENGLISH).replace("_", "-"), value.parent); + } + } + } + return sortedStateLookupTable.getOrDefault(state, NativePrinterStatus.UNMAPPED); + } + + public static Status createStatus(String reason, String state, String printer) { + NativePrinterStatus cupsPrinterStatus = matchReason(reason); + + // Edge-case for snmp statuses + if(cupsPrinterStatus == null) { + String sanitizedReason = snmpSanitize(reason); + if (!reason.equals(sanitizedReason)) { + cupsPrinterStatus = sortedReasonLookupTable.get(sanitizedReason); + if (cupsPrinterStatus != null) reason = sanitizedReason; + } + } + + if(cupsPrinterStatus == null && !reason.equalsIgnoreCase("none")) { + // Don't warn for "none" + log.warn("Printer state-reason \"{}\" was not found", reason); + } + + if(cupsPrinterStatus == null) { + // Don't return the raw reason if we couldn't find it mapped, return state instead + return new Status(matchState(state), printer, state); + } else if(cupsPrinterStatus == NativePrinterStatus.UNMAPPED) { + // Still return the state, but let the user know what the unmapped state reason was + return new Status(matchState(state), printer, reason); + } + return new Status(cupsPrinterStatus, printer, reason); + } + + @Override + public NativePrinterStatus getParent() { + return parent; + } + + @Override + public Object getRawCode() { + return name().toLowerCase(Locale.ENGLISH).replace("_", "-"); + } + + /** + * Removes redundant "-warning" or "-report" from SNMP-originated statuses + */ + public static String snmpSanitize(String cupsString) { + for(String suffix : SNMP_REDUNDANT_SUFFIXES) { + if (cupsString.endsWith(suffix)) { + return cupsString.substring(0, cupsString.length() - suffix.length()); + } + } + return cupsString; + } +} diff --git a/old code/tray/src/qz/printer/status/printer/NativePrinterStatus.java b/old code/tray/src/qz/printer/status/printer/NativePrinterStatus.java new file mode 100755 index 0000000..2aa41de --- /dev/null +++ b/old code/tray/src/qz/printer/status/printer/NativePrinterStatus.java @@ -0,0 +1,56 @@ +package qz.printer.status.printer; + +import org.apache.logging.log4j.Level; +import qz.printer.status.NativeStatus; + +import static org.apache.logging.log4j.Level.*; + +/** + * Created by kyle on 7/7/17. + */ +public enum NativePrinterStatus implements NativeStatus { + OK(INFO), + PAUSED(WARN), + ERROR(FATAL), + PENDING_DELETION(WARN), + PAPER_JAM(FATAL), + PAPER_OUT(WARN), + MANUAL_FEED(INFO), + PAPER_PROBLEM(WARN), + OFFLINE(FATAL), + IO_ACTIVE(INFO), + BUSY(INFO), + PRINTING(INFO), + OUTPUT_BIN_FULL(WARN), + NOT_AVAILABLE(FATAL), + WAITING(INFO), + PROCESSING(INFO), + INITIALIZING(INFO), + WARMING_UP(INFO), + TONER_LOW(WARN), + NO_TONER(FATAL), + PAGE_PUNT(FATAL), + USER_INTERVENTION(WARN), + OUT_OF_MEMORY(FATAL), + DOOR_OPEN(WARN), + SERVER_UNKNOWN(WARN), + POWER_SAVE(INFO), + UNKNOWN(INFO), + UNMAPPED(FATAL); // should never make it to the user + + private Level level; + + NativePrinterStatus(Level level) { + this.level = level; + } + + @Override + public NativeStatus getDefault() { + return UNKNOWN; + } + + @Override + public Level getLevel() { + return level; + } +} diff --git a/old code/tray/src/qz/printer/status/printer/WmiPrinterStatusMap.java b/old code/tray/src/qz/printer/status/printer/WmiPrinterStatusMap.java new file mode 100755 index 0000000..b8f7bb5 --- /dev/null +++ b/old code/tray/src/qz/printer/status/printer/WmiPrinterStatusMap.java @@ -0,0 +1,93 @@ +package qz.printer.status.printer; + +import qz.printer.status.NativeStatus; + +import java.util.SortedMap; +import java.util.TreeMap; + +/** + * Created by kyle on 5/18/17. + */ +public enum WmiPrinterStatusMap implements NativeStatus.NativeMap { + OK(NativePrinterStatus.OK, 0x00000000, true), + PAUSED(NativePrinterStatus.PAUSED, 0x00000001, false), + ERROR(NativePrinterStatus.ERROR, 0x00000002, false), + PENDING_DELETION(NativePrinterStatus.PENDING_DELETION, 0x00000004, true), + PAPER_JAM(NativePrinterStatus.PAPER_JAM, 0x00000008, false), + PAPER_OUT(NativePrinterStatus.PAPER_OUT, 0x00000010, false), + MANUAL_FEED(NativePrinterStatus.MANUAL_FEED, 0x00000020, false), + PAPER_PROBLEM(NativePrinterStatus.PAPER_PROBLEM, 0x00000040, false), + OFFLINE(NativePrinterStatus.OFFLINE, 0x00000080, false), + IO_ACTIVE(NativePrinterStatus.IO_ACTIVE, 0x00000100, true), + BUSY(NativePrinterStatus.BUSY, 0x00000200, true), + PRINTING(NativePrinterStatus.PRINTING, 0x00000400, true), + OUTPUT_BIN_FULL(NativePrinterStatus.OUTPUT_BIN_FULL, 0x00000800, false), + NOT_AVAILABLE(NativePrinterStatus.NOT_AVAILABLE, 0x00001000, false), + WAITING(NativePrinterStatus.WAITING, 0x00002000, true), + PROCESSING(NativePrinterStatus.PROCESSING, 0x00004000, true), + INITIALIZING(NativePrinterStatus.INITIALIZING, 0x00008000, true), + WARMING_UP(NativePrinterStatus.WARMING_UP, 0x00010000, true), + TONER_LOW(NativePrinterStatus.TONER_LOW, 0x00020000, true), + NO_TONER(NativePrinterStatus.NO_TONER, 0x00040000, false), + PAGE_PUNT(NativePrinterStatus.PAGE_PUNT, 0x00080000, true), + USER_INTERVENTION(NativePrinterStatus.USER_INTERVENTION, 0x00100000, false), + OUT_OF_MEMORY(NativePrinterStatus.OUT_OF_MEMORY, 0x00200000, false), + DOOR_OPEN(NativePrinterStatus.DOOR_OPEN, 0x00400000, false), + SERVER_UNKNOWN(NativePrinterStatus.SERVER_UNKNOWN, 0x00800000, false), + POWER_SAVE(NativePrinterStatus.POWER_SAVE, 0x01000000, true), + + /** + * For internal use only, not WMI values (change as needed) + */ + // Used for mapping PRINTER_ATTRIBUTE_WORK_OFFLINE from printer attributes to printer status + ATTRIBUTE_WORK_OFFLINE(NativePrinterStatus.OFFLINE, 0x04000000, false), + // "Unknown" placeholder for future/unmapped values + UNKNOWN_STATUS(NativePrinterStatus.UNKNOWN, 0x02000000, false); + + public static int NOT_OK_MASK = getNotOkMask(); + private static SortedMap sortedLookupTable; + + private NativePrinterStatus parent; + private final int rawCode; + + // Printer status isn't very good about reporting recovered errors, we'll try to track them manually + private boolean isOk; + + WmiPrinterStatusMap(NativePrinterStatus parent, int rawCode, boolean isOK) { + this.parent = parent; + this.rawCode = rawCode; + this.isOk = isOK; + } + + public static NativePrinterStatus match(int rawCode) { + // Initialize a sorted map to speed up lookups + if(sortedLookupTable == null) { + sortedLookupTable = new TreeMap<>(); + for(WmiPrinterStatusMap value : values()) { + sortedLookupTable.put(value.rawCode, value.parent); + } + } + + return sortedLookupTable.get(rawCode); + } + + @Override + public NativeStatus getParent() { + return parent; + } + + @Override + public Object getRawCode() { + return rawCode; + } + + private static int getNotOkMask() { + int result = 0; + for(WmiPrinterStatusMap code : values()) { + if(!code.isOk) { + result |= code.rawCode; + } + } + return result; + } +} diff --git a/old code/tray/src/qz/ui/AboutDialog.java b/old code/tray/src/qz/ui/AboutDialog.java new file mode 100755 index 0000000..eb5410f --- /dev/null +++ b/old code/tray/src/qz/ui/AboutDialog.java @@ -0,0 +1,318 @@ +package qz.ui; + +import com.github.zafarkhaja.semver.Version; +import org.eclipse.jetty.server.Server; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.AboutInfo; +import qz.common.Constants; +import qz.ui.component.EmLabel; +import qz.ui.component.IconCache; +import qz.ui.component.LinkLabel; +import qz.utils.FileUtilities; +import qz.utils.SystemUtilities; +import qz.ws.PrintSocketServer; +import qz.ws.substitutions.Substitutions; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.datatransfer.DataFlavor; +import java.awt.dnd.*; +import java.io.File; +import java.net.URI; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Created by Tres on 2/26/2015. + * Displays a basic about dialog + */ +public class AboutDialog extends BasicDialog implements Themeable { + + private static final Logger log = LogManager.getLogger(AboutDialog.class); + private final boolean limitedDisplay; + private Server server; + private JLabel lblUpdate; + private JButton updateButton; + + private JPanel contentPanel; + private JToolBar headerBar; + private Border dropBorder; + + // Use allows word wrapping on a standard JLabel + static class TextWrapLabel extends JLabel { + TextWrapLabel(String text) { + super("" + text + ""); + } + } + + public AboutDialog(JMenuItem menuItem, IconCache iconCache) { + super(menuItem, iconCache); + + //noinspection ConstantConditions - white label support + limitedDisplay = Constants.VERSION_CHECK_URL.isEmpty(); + } + + public void setServer(Server server) { + this.server = server; + + initComponents(); + } + + public void initComponents() { + JLabel lblAbout = new EmLabel(Constants.ABOUT_TITLE, 3); + + JPanel infoPanel = new JPanel(); + infoPanel.setLayout(new BoxLayout(infoPanel, BoxLayout.Y_AXIS)); + + LinkLabel linkLibrary = getLinkLibrary(); + Box versionBox = Box.createHorizontalBox(); + versionBox.setAlignmentX(Component.LEFT_ALIGNMENT); + versionBox.add(new JLabel(String.format("%s (Java)", Constants.VERSION))); + + JPanel aboutPanel = new JPanel(new FlowLayout(FlowLayout.CENTER)); + JLabel logo = new JLabel(getIcon(IconCache.Icon.LOGO_ICON)); + logo.setBorder(new EmptyBorder(0, 0, 0, limitedDisplay ? 0 : 20)); + aboutPanel.add(logo); + + if (!limitedDisplay) { + LinkLabel linkNew = new LinkLabel("What's New?"); + linkNew.setLinkLocation(Constants.VERSION_DOWNLOAD_URL); + + lblUpdate = new JLabel(); + updateButton = new JButton(); + updateButton.setVisible(false); + updateButton.addActionListener(evt -> { + try { Desktop.getDesktop().browse(new URL(Constants.ABOUT_DOWNLOAD_URL).toURI()); } + catch(Exception e) { log.error("", e); } + }); + checkForUpdate(); + versionBox.add(Box.createHorizontalStrut(12)); + versionBox.add(linkNew); + + infoPanel.add(lblAbout); + infoPanel.add(Box.createVerticalGlue()); + infoPanel.add(versionBox); + infoPanel.add(Box.createVerticalGlue()); + infoPanel.add(lblUpdate); + infoPanel.add(updateButton); + infoPanel.add(Box.createVerticalGlue()); + infoPanel.add(new TextWrapLabel(String.format("%s is written and supported by %s.", Constants.ABOUT_TITLE, Constants.ABOUT_COMPANY))); + infoPanel.add(Box.createVerticalGlue()); + infoPanel.add(new TextWrapLabel(String.format("If using %s commercially, please first reach out to the website publisher for support issues.", Constants.ABOUT_TITLE))); + infoPanel.add(Box.createVerticalGlue()); + infoPanel.add(linkLibrary); + infoPanel.setPreferredSize(logo.getPreferredSize()); + } else { + LinkLabel linkLabel = new LinkLabel(Constants.ABOUT_URL); + linkLabel.setLinkLocation(Constants.ABOUT_URL); + + infoPanel.add(Box.createVerticalGlue()); + infoPanel.add(lblAbout); + infoPanel.add(versionBox); + infoPanel.add(Box.createVerticalStrut(16)); + infoPanel.add(linkLabel); + infoPanel.add(Box.createVerticalStrut(8)); + infoPanel.add(linkLibrary); + infoPanel.add(Box.createVerticalGlue()); + infoPanel.add(Box.createHorizontalStrut(16)); + } + + aboutPanel.add(infoPanel); + + contentPanel = new JPanel(); + contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.PAGE_AXIS)); + contentPanel.add(aboutPanel); + contentPanel.add(new JSeparator()); + + if (!limitedDisplay) { + contentPanel.add(getSupportPanel()); + } + + setContent(contentPanel, true); + contentPanel.setDropTarget(createDropTarget()); + setHeader(headerBar = getHeaderBar()); + refreshHeader(); + } + + private static JPanel getSupportPanel() { + LinkLabel lblLicensing = new LinkLabel("Licensing Information", 0.9f, false); + lblLicensing.setLinkLocation(Constants.ABOUT_LICENSING_URL); + + LinkLabel lblSupport = new LinkLabel("Support Information", 0.9f, false); + lblSupport.setLinkLocation(Constants.ABOUT_SUPPORT_URL); + + LinkLabel lblPrivacy = new LinkLabel("Privacy Policy", 0.9f, false); + lblPrivacy.setLinkLocation(Constants.ABOUT_PRIVACY_URL); + + JPanel supportPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 80, 10)); + supportPanel.add(lblLicensing); + supportPanel.add(lblSupport); + supportPanel.add(lblPrivacy); + return supportPanel; + } + + private LinkLabel getLinkLibrary() { + LinkLabel linkLibrary = new LinkLabel("Detailed library information"); + if(server != null && server.isRunning() && !server.isStopping()) { + // Some OSs (e.g. FreeBSD) return null for server.getURI(), fallback to sane values + URI uri = server.getURI(); + String scheme = uri == null ? "http" : uri.getScheme(); + int port = uri == null ? PrintSocketServer.getWebsocketPorts().getInsecurePort(): uri.getPort(); + linkLibrary.setLinkLocation(String.format("%s://%s:%s", scheme, AboutInfo.getPreferredHostname(), port)); + } + return linkLibrary; + } + + private JToolBar getHeaderBar() { + JToolBar headerBar = new JToolBar(); + headerBar.setBorderPainted(false); + headerBar.setLayout(new FlowLayout()); + headerBar.setOpaque(true); + headerBar.setFloatable(false); + + LinkLabel substitutionsLabel = new LinkLabel("Substitutions are in effect for this machine"); + JButton refreshButton = new JButton("", getIcon(IconCache.Icon.RELOAD_ICON)); + refreshButton.setOpaque(false); + refreshButton.addActionListener(e -> { + Substitutions.getInstance(true); + refreshHeader(); + }); + + substitutionsLabel.setLinkLocation(FileUtilities.SHARED_DIR.toFile()); + + headerBar.add(substitutionsLabel); + headerBar.add(refreshButton); + return headerBar; + } + + private DropTarget createDropTarget() { + return new DropTarget() { + public synchronized void drop(DropTargetDropEvent evt) { + processDroppedFile(evt); + } + + @Override + public synchronized void dragEnter(DropTargetDragEvent dtde) { + super.dragEnter(dtde); + setDropBorder(true); + } + + @Override + public synchronized void dragExit(DropTargetEvent dte) { + super.dragExit(dte); + setDropBorder(false); + } + }; + } + + private void processDroppedFile(DropTargetDropEvent evt) { + try { + evt.acceptDrop(DnDConstants.ACTION_COPY); + Object dropped = evt.getTransferable().getTransferData(DataFlavor.javaFileListFlavor); + if(dropped instanceof List) { + List droppedFiles = (List)dropped; + for (File file : droppedFiles) { + if(file.getName().equals(Substitutions.FILE_NAME)) { + blinkDropBorder(true); + log.info("File drop accepted: {}", file); + Path source = file.toPath(); + Path dest = FileUtilities.SHARED_DIR.resolve(file.getName()); + Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING); + FileUtilities.inheritParentPermissions(dest); + Substitutions.getInstance(true); + refreshHeader(); + break; + } else { + blinkDropBorder(false); + break; + } + } + } + evt.dropComplete(true); + } catch (Exception ex) { + log.warn(ex); + } + setDropBorder(false); + } + + private void checkForUpdate() { + Version latestVersion = AboutInfo.findLatestVersion(); + if (latestVersion.greaterThan(Constants.VERSION)) { + lblUpdate.setText("An update is available:"); + + updateButton.setText("Download " + latestVersion); + updateButton.setVisible(true); + } else if (latestVersion.lessThan(Constants.VERSION)) { + lblUpdate.setText("You are on a beta release."); + + updateButton.setText("Revert to stable " + latestVersion); + updateButton.setVisible(true); + } else { + lblUpdate.setText("You have the latest version."); + + updateButton.setVisible(false); + } + } + + private void setDropBorder(boolean isShown) { + if(isShown) { + if(contentPanel.getBorder() == null) { + dropBorder = BorderFactory.createDashedBorder(Constants.TRUSTED_COLOR, 3, 5, 5, true); + contentPanel.setBorder(dropBorder); + } + } else { + contentPanel.setBorder(null); + } + } + + private void blinkDropBorder(boolean success) { + Color borderColor = success ? Color.GREEN : Constants.WARNING_COLOR; + dropBorder = BorderFactory.createDashedBorder(borderColor, 3, 5, 5, true); + AtomicBoolean toggled = new AtomicBoolean(true); + int blinkCount = 3; + int blinkDelay = 100; // ms + for(int i = 0; i < blinkCount * 2; i++) { + Timer timer = new Timer("blink" + i); + timer.schedule(new TimerTask() { + @Override + public void run() { + SwingUtilities.invokeLater(() -> { + contentPanel.setBorder(toggled.getAndSet(!toggled.get())? dropBorder:null); + }); + } + }, i * blinkDelay); + } + } + + private void refreshHeader() { + headerBar.setBackground(SystemUtilities.isDarkDesktop() ? + Constants.TRUSTED_COLOR.darker().darker() : Constants.TRUSTED_COLOR_DARK); + headerBar.setVisible(Substitutions.areActive()); + pack(); + } + + + @Override + public void setVisible(boolean visible) { + if (visible && !limitedDisplay) { + checkForUpdate(); + } + + super.setVisible(visible); + } + + @Override + public void refresh() { + refreshHeader(); + super.refresh(); + } +} diff --git a/old code/tray/src/qz/ui/BasicDialog.java b/old code/tray/src/qz/ui/BasicDialog.java new file mode 100755 index 0000000..40829d0 --- /dev/null +++ b/old code/tray/src/qz/ui/BasicDialog.java @@ -0,0 +1,182 @@ +package qz.ui; + +import qz.common.Constants; +import qz.ui.component.IconCache; +import qz.utils.MacUtilities; +import qz.utils.ShellUtilities; +import qz.utils.SystemUtilities; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.awt.image.BufferedImage; + +/** + * Created by Tres on 2/23/2015. + */ +public class BasicDialog extends JDialog implements Themeable { + private JPanel mainPanel; + private JComponent headerComponent; + private JComponent contentComponent; + + private JPanel buttonPanel; + private JButton closeButton; + + private IconCache iconCache; + + private int stockButtonCount = 0; + + public BasicDialog(JMenuItem caller, IconCache iconCache) { + super((Frame)null, caller.getText().replaceAll("\\.+", ""), true); + this.iconCache = iconCache; + initBasicComponents(); + } + + public BasicDialog(Frame owner, String title, IconCache iconCache) { + super(owner, title, true); + this.iconCache = iconCache; + initBasicComponents(); + } + + public void initBasicComponents() { + setIconImages(iconCache.getImages(IconCache.Icon.TASK_BAR_ICON)); + mainPanel = new JPanel(); + mainPanel.setBorder(new EmptyBorder(Constants.BORDER_PADDING, Constants.BORDER_PADDING, Constants.BORDER_PADDING, Constants.BORDER_PADDING)); + + headerComponent = new JLabel(); + headerComponent.setBorder(new EmptyBorder(0, 0, Constants.BORDER_PADDING, Constants.BORDER_PADDING)); + mainPanel.add(headerComponent, BorderLayout.PAGE_START); + + buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + buttonPanel.add(new JSeparator(JSeparator.HORIZONTAL)); + buttonPanel.setAlignmentX(Component.LEFT_ALIGNMENT); + closeButton = addPanelButton("Close", IconCache.Icon.ALLOW_ICON, KeyEvent.VK_C); + closeButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + setVisible(false); + } + }); + stockButtonCount = buttonPanel.getComponents().length; + + mainPanel.setLayout(new BorderLayout()); + mainPanel.add(headerComponent, BorderLayout.PAGE_START); + mainPanel.add(contentComponent = new JLabel("Hello world!"), BorderLayout.CENTER); + mainPanel.add(buttonPanel, BorderLayout.PAGE_END); + + addKeyListener(KeyEvent.VK_ESCAPE, closeButton); + + getContentPane().add(mainPanel); + setResizable(false); + + pack(); + + setLocationRelativeTo(null); // center on main display + } + + @Override + public void refresh() { + ThemeUtilities.refreshAll(this); + } + + public JLabel setHeader(String header) { + if (headerComponent instanceof JLabel) { + ((JLabel)headerComponent).setText(String.format(header, "").replaceAll("\\s+", " ")); + return (JLabel)headerComponent; + } + return (JLabel)setHeader(new JLabel(header)); + } + + public JComponent setHeader(JComponent headerComponent) { + headerComponent.setAlignmentX(this.headerComponent.getAlignmentX()); + headerComponent.setBorder(this.headerComponent.getBorder()); + mainPanel.add(headerComponent, BorderLayout.PAGE_START, indexOf(this.headerComponent)); + mainPanel.remove(indexOf(this.headerComponent)); + this.headerComponent = headerComponent; + mainPanel.invalidate(); + return headerComponent; + } + + public JComponent setContent(JComponent contentComponent, boolean autoCenter) { + if (contentComponent != null) { + contentComponent.setAlignmentX(LEFT_ALIGNMENT); + mainPanel.add(contentComponent, BorderLayout.CENTER, indexOf(this.contentComponent)); + } + + mainPanel.remove(indexOf(this.contentComponent)); + this.contentComponent = contentComponent; + mainPanel.invalidate(); + pack(); + if (autoCenter) { + setLocationRelativeTo(null); + } + return contentComponent; + } + + public void addPanelComponent(JComponent component) { + for(Component c : buttonPanel.getComponents()) { + if(component.equals(c)) { + return; // don't add twice + } + } + buttonPanel.add(component, buttonPanel.getComponents().length - stockButtonCount); + } + + public JButton addPanelButton(String title, IconCache.Icon icon, int mnemonic) { + return addPanelButton(title, iconCache == null? null:iconCache.getIcon(icon), mnemonic); + } + + public JButton addPanelButton(String title, Icon icon, int mnemonic) { + JButton button = new JButton(title, icon); + button.setMnemonic(mnemonic); + buttonPanel.add(button, buttonPanel.getComponents().length - stockButtonCount); + return button; + } + + public void addKeyListener(int virtualKey, final AbstractButton actionButton) { + getRootPane().getInputMap(JRootPane.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke(virtualKey, 0), actionButton.toString()); + getRootPane().getActionMap().put(actionButton.toString(), new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + actionButton.doClick(); + } + }); + } + + public int indexOf(Component findComponent) { + int i = -1; + for(Component currentComponent : mainPanel.getComponents()) { + i++; + if (findComponent == currentComponent) { + break; + } + } + return i; + } + + public BufferedImage getImage(IconCache.Icon icon) { + if (iconCache != null) { + return iconCache.getImage(icon); + } + return null; + } + + public ImageIcon getIcon(IconCache.Icon icon) { + if (iconCache != null) { + return iconCache.getIcon(icon); + } + return null; + } + + @Override + public void setVisible(boolean b) { + // fix window focus on macOS + if (SystemUtilities.isMac() && !GraphicsEnvironment.isHeadless()) { + MacUtilities.setFocus(); + } + super.setVisible(b); + } +} diff --git a/old code/tray/src/qz/ui/ConfirmDialog.java b/old code/tray/src/qz/ui/ConfirmDialog.java new file mode 100755 index 0000000..750564c --- /dev/null +++ b/old code/tray/src/qz/ui/ConfirmDialog.java @@ -0,0 +1,110 @@ +package qz.ui; + +import qz.ui.component.IconCache; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; + +/** + * Created by Tres on 2/19/2015. + * A basic allow/block dialog with support for displaying Certificate information + */ +public class ConfirmDialog extends JDialog { + private JLabel messageLabel; + private JPanel descriptionPanel; + + private JButton yesButton; + private JButton noButton; + private JPanel optionsPanel; + private JLabel questionLabel; + + private JPanel mainPanel; + + private final IconCache iconCache; + + private boolean approved; + + public ConfirmDialog(Frame owner, String title, IconCache iconCache) { + super(owner, title, true); + this.iconCache = iconCache; + this.approved = false; + this.setIconImages(iconCache.getImages(IconCache.Icon.TASK_BAR_ICON)); + initComponents(); + } + + private void initComponents() { + descriptionPanel = new JPanel(); + messageLabel = new JLabel(); + questionLabel = new JLabel(iconCache.getIcon(IconCache.Icon.QUESTION_ICON)); + + descriptionPanel.add(questionLabel); + descriptionPanel.add(messageLabel); + descriptionPanel.setBorder(new EmptyBorder(3, 3, 3, 3)); + messageLabel.setText("Are you sure?"); + + optionsPanel = new JPanel(); + yesButton = new JButton("OK", iconCache.getIcon(IconCache.Icon.ALLOW_ICON)); + yesButton.setMnemonic(KeyEvent.VK_K); + noButton = new JButton("Cancel", iconCache.getIcon(IconCache.Icon.CANCEL_ICON)); + noButton.setMnemonic(KeyEvent.VK_C); + yesButton.addActionListener(buttonAction); + noButton.addActionListener(buttonAction); + + optionsPanel.add(yesButton); + optionsPanel.add(noButton); + + mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + + mainPanel.add(descriptionPanel); + mainPanel.add(optionsPanel); + + getContentPane().add(mainPanel); + + yesButton.requestFocusInWindow(); + + setDefaultCloseOperation(HIDE_ON_CLOSE); + setResizable(false); + pack(); + + setAlwaysOnTop(true); + setLocationRelativeTo(null); // center on main display + } + + private final transient ActionListener buttonAction = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + approved = e.getSource().equals(yesButton); + setVisible(false); + } + }; + + @Override + public void setVisible(boolean b) { + yesButton.requestFocusInWindow(); + super.setVisible(b); + } + + public boolean isApproved() { + return approved; + } + + public String getMessage() { + return messageLabel.getText(); + } + + public void setMessage(String message) { + messageLabel.setText(message); pack(); + } + + public boolean prompt(String message) { + setMessage(message); + setVisible(true); + return isApproved(); + } +} + diff --git a/old code/tray/src/qz/ui/DetailsDialog.java b/old code/tray/src/qz/ui/DetailsDialog.java new file mode 100755 index 0000000..869f766 --- /dev/null +++ b/old code/tray/src/qz/ui/DetailsDialog.java @@ -0,0 +1,64 @@ +package qz.ui; + +import qz.auth.RequestState; +import qz.ui.component.CertificateTable; +import qz.ui.component.IconCache; +import qz.ui.component.RequestTable; + +import javax.swing.*; + +public class DetailsDialog extends JPanel { + + private JLabel requestLabel; + private JScrollPane reqScrollPane; + private RequestTable requestTable; + + private JLabel certLabel; + private JScrollPane certScrollPane; + private CertificateTable certTable; + + + public DetailsDialog(IconCache iconCache) { + super(); + setLayout(new BoxLayout(this, BoxLayout.Y_AXIS)); + + initComponents(iconCache); + } + + private void initComponents(IconCache iconCache) { + requestLabel = new JLabel("Request"); + requestLabel.setAlignmentX(CENTER_ALIGNMENT); + + requestTable = new RequestTable(iconCache); + reqScrollPane = new JScrollPane(requestTable); + requestTable.getAccessibleContext().setAccessibleName(requestLabel.getText() + " Details"); + requestTable.getAccessibleContext().setAccessibleDescription("Signing details about this request."); + requestLabel.setLabelFor(requestTable); + + certLabel = new JLabel("Certificate"); + certLabel.setAlignmentX(CENTER_ALIGNMENT); + + certTable = new CertificateTable(iconCache); + certScrollPane = new JScrollPane(certTable); + certTable.getAccessibleContext().setAccessibleName(certLabel.getText() + " Details"); + certTable.getAccessibleContext().setAccessibleDescription("Certificate details about this request."); + certLabel.setLabelFor(certTable); + + add(requestLabel); + add(reqScrollPane); + + add(new JToolBar.Separator()); + + add(certLabel); + add(certScrollPane); + } + + public void updateDisplay(RequestState request) { + certTable.setCertificate(request.getCertUsed()); + certTable.autoSize(); + + requestTable.setRequest(request); + requestTable.autoSize(); + } + +} diff --git a/old code/tray/src/qz/ui/GatewayDialog.java b/old code/tray/src/qz/ui/GatewayDialog.java new file mode 100755 index 0000000..93e6e6f --- /dev/null +++ b/old code/tray/src/qz/ui/GatewayDialog.java @@ -0,0 +1,237 @@ +package qz.ui; + +import qz.auth.RequestState; +import qz.common.Constants; +import qz.ui.component.IconCache; +import qz.ui.component.LinkLabel; +import qz.utils.SystemUtilities; + +import javax.swing.*; +import javax.swing.border.EmptyBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; + +/** + * Created by Tres on 2/19/2015. + * A basic allow/block dialog with support for displaying Certificate information + */ +public class GatewayDialog extends JDialog implements Themeable { + + private JLabel verifiedLabel; + private JLabel descriptionLabel; + private LinkLabel certInfoLabel; + private JPanel descriptionPanel; + + private DetailsDialog detailsDialog; + + private JButton allowButton; + private JButton blockButton; + private JPanel optionsPanel; + + private JCheckBox persistentCheckBox; + private JPanel bottomPanel; + + private JPanel mainPanel; + + private final IconCache iconCache; + + private String description; + private RequestState request; + private boolean approved; + private boolean persistent; + + public GatewayDialog(Frame owner, String title, IconCache iconCache) { + super(owner, title, true); + this.iconCache = iconCache; + this.description = ""; + this.approved = false; + this.setIconImages(iconCache.getImages(IconCache.Icon.TASK_BAR_ICON)); + initComponents(); + refreshComponents(); + } + + private void initComponents() { + descriptionPanel = new JPanel(); + verifiedLabel = new JLabel(); + verifiedLabel.setBorder(new EmptyBorder(3, 3, 3, 3)); + descriptionLabel = new JLabel(); + + descriptionPanel.add(verifiedLabel); + descriptionPanel.add(descriptionLabel); + descriptionPanel.setBorder(new EmptyBorder(3, 3, 3, 3)); + + optionsPanel = new JPanel(); + allowButton = new JButton("Allow", iconCache.getIcon(IconCache.Icon.ALLOW_ICON)); + allowButton.setMnemonic(KeyEvent.VK_A); + blockButton = new JButton("Block", iconCache.getIcon(IconCache.Icon.BLOCK_ICON)); + blockButton.setMnemonic(KeyEvent.VK_B); + allowButton.addActionListener(buttonAction); + blockButton.addActionListener(buttonAction); + + detailsDialog = new DetailsDialog(iconCache); + certInfoLabel = new LinkLabel(); + certInfoLabel.setAlignmentX(LEFT_ALIGNMENT); + certInfoLabel.addActionListener(e -> { + detailsDialog.updateDisplay(request); + JOptionPane.showMessageDialog( + GatewayDialog.this, + detailsDialog, + "Details", + JOptionPane.PLAIN_MESSAGE); + }); + + bottomPanel = new JPanel(); + bottomPanel.setLayout(new FlowLayout(FlowLayout.CENTER, 10, 5)); + persistentCheckBox = new JCheckBox(Constants.REMEMBER_THIS_DECISION, false); + persistentCheckBox.setMnemonic(KeyEvent.VK_R); + persistentCheckBox.addActionListener(e -> allowButton.setEnabled(!persistentCheckBox.isSelected() || request.isVerified())); + persistentCheckBox.setAlignmentX(RIGHT_ALIGNMENT); + + bottomPanel.add(certInfoLabel); + bottomPanel.add(persistentCheckBox); + + optionsPanel.add(allowButton); + optionsPanel.add(blockButton); + + mainPanel = new JPanel(); + mainPanel.setLayout(new BoxLayout(mainPanel, BoxLayout.Y_AXIS)); + + mainPanel.add(descriptionPanel); + mainPanel.add(optionsPanel); + mainPanel.add(new JSeparator()); + mainPanel.add(bottomPanel); + + getContentPane().add(mainPanel); + + allowButton.requestFocusInWindow(); + + setDefaultCloseOperation(HIDE_ON_CLOSE); + setResizable(false); + pack(); + + setAlwaysOnTop(true); + setLocationRelativeTo(null); // center on main display + } + + @Override + public void refresh() { + ThemeUtilities.refreshAll(this, detailsDialog); + refreshComponents(); + } + + private final transient ActionListener buttonAction = new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + approved = e.getSource().equals(allowButton); + persistent = persistentCheckBox.isSelected(); + + // Require confirmation for permanent block + if (!approved && persistent) { + ConfirmDialog confirmDialog = new ConfirmDialog(null, "Please Confirm", iconCache); + String message = Constants.BLOCK_SITES_TEXT.replace(" blocked ", " block ") + "?"; + message = String.format(message, request.hasCertificate()? request.getCertName():""); + if (!confirmDialog.prompt(message)) { + persistent = false; + return; + } + } + setVisible(false); + } + }; + + public final void refreshComponents() { + if (request != null) { + // TODO: Add name, publisher + descriptionLabel.setText("" + + String.format(description, "

" + request.getCertName()) + + "

" + request.getValidityInfo() + "" + + ""); + certInfoLabel.setText("View request details"); + + IconCache.Icon trustIcon; + String iconToolTip = null; + Color detailColor = Constants.TRUSTED_COLOR; + if (request.isVerified()) { + //cert and signature are good + if(request.isSponsored()) { + // special case for sponsored certs + trustIcon = IconCache.Icon.TRUST_SPONSORED_ICON; + iconToolTip = Constants.SPONSORED_TOOLTIP; + } else { + trustIcon = IconCache.Icon.TRUST_VERIFIED_ICON; + } + } else if (request.getCertUsed().isValid()) { + //cert is good, but there is an issue with the signature + trustIcon = IconCache.Icon.TRUST_ISSUE_ICON; + detailColor = Constants.WARNING_COLOR; + } else { + //nothing is good + trustIcon = IconCache.Icon.TRUST_MISSING_ICON; + detailColor = Constants.WARNING_COLOR; + } + + verifiedLabel.setIcon(iconCache.getIcon(trustIcon)); + verifiedLabel.setToolTipText(iconToolTip); + certInfoLabel.setForeground(detailColor); + } else { + descriptionLabel.setText(description); + verifiedLabel.setIcon(null); + } + + persistentCheckBox.setSelected(false); + allowButton.setEnabled(true); + allowButton.requestFocusInWindow(); + pack(); + } + + public boolean isApproved() { + return approved; + } + + public boolean isPersistent() { + return persistent; + } + + public void setRequest(RequestState req) { + request = req; + } + + public RequestState getRequest() { + return request; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public boolean prompt(String description, RequestState request, Point position) { + //reset dialog state on new prompt + approved = false; + persistent = false; + persistentCheckBox.setSelected(false); + + if (request == null || request.hasBlockedCert()) { + approved = false; + return false; + } + if (request.hasSavedCert()) { + approved = true; + return true; + } + + setDescription(description); + setRequest(request); + refreshComponents(); + SystemUtilities.centerDialog(this, position); + setVisible(true); + + return isApproved(); + } +} + diff --git a/old code/tray/src/qz/ui/LogDialog.java b/old code/tray/src/qz/ui/LogDialog.java new file mode 100755 index 0000000..d13c359 --- /dev/null +++ b/old code/tray/src/qz/ui/LogDialog.java @@ -0,0 +1,101 @@ +package qz.ui; + +import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.appender.WriterAppender; +import org.apache.logging.log4j.core.filter.ThresholdFilter; +import org.apache.logging.log4j.core.layout.PatternLayout; +import qz.ui.component.IconCache; +import qz.ui.component.LinkLabel; +import qz.utils.FileUtilities; +import qz.utils.LoggerUtilities; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.KeyEvent; +import java.io.File; +import java.io.StringWriter; + +/** + * Created by Tres on 2/26/2015. + */ +public class LogDialog extends BasicDialog { + + private final int ROWS = 20; + private final int COLS = 80; + + private JScrollPane logPane; + private JTextArea logArea; + + private JButton clearButton; + + private WriterAppender logStream; + + + public LogDialog(JMenuItem caller, IconCache iconCache) { + super(caller, iconCache); + initComponents(); + } + + public void initComponents() { + int defaultFontSize = new JLabel().getFont().getSize(); + LinkLabel logDirLabel = new LinkLabel(FileUtilities.USER_DIR + File.separator); + logDirLabel.setLinkLocation(new File(FileUtilities.USER_DIR + File.separator)); + setHeader(logDirLabel); + + StringWriter writeTarget = new StringWriter() { + @Override + public void flush() { + SwingUtilities.invokeLater(() -> { + logArea.append(toString()); + logPane.getVerticalScrollBar().setValue(logPane.getVerticalScrollBar().getMaximum()); + }); + } + }; + + logArea = new JTextArea(ROWS, COLS); + logArea.setEditable(false); + logArea.setLineWrap(true); + logArea.setWrapStyleWord(true); + logArea.setFont(new Font("", Font.PLAIN, defaultFontSize)); //force fallback font for character support + + // TODO: Fix button panel resizing issues + clearButton = addPanelButton("Clear", IconCache.Icon.DELETE_ICON, KeyEvent.VK_L); + clearButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + logArea.setText(null); + + writeTarget.getBuffer().setLength(0); + } + }); + + logPane = new JScrollPane(logArea); + setContent(logPane, true); + setResizable(true); + + // add new appender to Log4J just for text area + logStream = WriterAppender.newBuilder() + .setName("ui-dialog") + .setLayout(PatternLayout.newBuilder().withPattern("[%p] %d{ISO8601} @ %c:%L%n\t%m%n").build()) + .setFilter(ThresholdFilter.createFilter(Level.TRACE, Filter.Result.ACCEPT, Filter.Result.DENY)) + .setTarget(writeTarget) + .build(); + logStream.start(); + } + + @Override + public void setVisible(boolean visible) { + if (visible) { + logArea.setText(null); + LoggerUtilities.getRootLogger().addAppender(logStream); + } else { + LoggerUtilities.getRootLogger().removeAppender(logStream); + } + + super.setVisible(visible); + } + +} diff --git a/old code/tray/src/qz/ui/PairingConfigDialog.java b/old code/tray/src/qz/ui/PairingConfigDialog.java new file mode 100755 index 0000000..523a9dc --- /dev/null +++ b/old code/tray/src/qz/ui/PairingConfigDialog.java @@ -0,0 +1,149 @@ +package qz.ui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.io.*; +import java.nio.file.*; +import org.json.JSONObject; +import qz.utils.FileUtilities; +import qz.auth.PairingAuth; + +public class PairingConfigDialog extends JDialog { + private JTextField siteField; + private JTextField keyField; + private JButton saveButton; + private JButton cancelButton; + private File configFile; + + public PairingConfigDialog(Frame parent) { + super(parent, "QZ Tray Pairing Configuration", true); + + // Use QZ Tray's user directory for config + configFile = new File(FileUtilities.USER_DIR.toFile(), "pairing-config.json"); + + setLayout(new BorderLayout(10, 10)); + + // Create main panel with form + JPanel formPanel = new JPanel(new GridBagLayout()); + GridBagConstraints gbc = new GridBagConstraints(); + gbc.fill = GridBagConstraints.HORIZONTAL; + gbc.insets = new Insets(5, 5, 5, 5); + + // Site label and field + gbc.gridx = 0; + gbc.gridy = 0; + gbc.weightx = 0.3; + formPanel.add(new JLabel("Site Address:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 0.7; + siteField = new JTextField(30); + siteField.setToolTipText("Enter the Flask app address (e.g., http://192.168.1.100:5000)"); + formPanel.add(siteField, gbc); + + // Pairing key label and field + gbc.gridx = 0; + gbc.gridy = 1; + gbc.weightx = 0.3; + formPanel.add(new JLabel("Pairing Key:"), gbc); + + gbc.gridx = 1; + gbc.weightx = 0.7; + keyField = new JTextField(30); + keyField.setToolTipText("Enter the pairing key from your Flask app"); + formPanel.add(keyField, gbc); + + // Info label + JLabel infoLabel = new JLabel("Get your pairing key from the Flask app's Download Extension page"); + infoLabel.setForeground(Color.GRAY); + gbc.gridx = 0; + gbc.gridy = 2; + gbc.gridwidth = 2; + formPanel.add(infoLabel, gbc); + + add(formPanel, BorderLayout.CENTER); + + // Buttons panel + JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT)); + saveButton = new JButton("Save"); + saveButton.addActionListener(e -> saveConfig()); + + cancelButton = new JButton("Cancel"); + cancelButton.addActionListener(e -> dispose()); + + buttonPanel.add(cancelButton); + buttonPanel.add(saveButton); + add(buttonPanel, BorderLayout.SOUTH); + + // Load existing config if available + loadConfig(); + + pack(); + setMinimumSize(new Dimension(500, 200)); + setLocationRelativeTo(parent); + } + + private void loadConfig() { + if (configFile.exists()) { + try { + String content = new String(Files.readAllBytes(configFile.toPath())); + JSONObject config = new JSONObject(content); + siteField.setText(config.optString("site", "")); + keyField.setText(config.optString("pairing_key", "")); + } catch (Exception ex) { + System.err.println("Error loading pairing config: " + ex.getMessage()); + } + } + } + + private void saveConfig() { + String site = siteField.getText().trim(); + String key = keyField.getText().trim(); + + if (site.isEmpty() || key.isEmpty()) { + JOptionPane.showMessageDialog(this, + "Please enter both site address and pairing key.", + "Missing Information", + JOptionPane.WARNING_MESSAGE); + return; + } + + JSONObject config = new JSONObject(); + config.put("site", site); + config.put("pairing_key", key); + + try { + // Ensure parent directory exists + configFile.getParentFile().mkdirs(); + + // Write config + try (FileWriter fw = new FileWriter(configFile)) { + fw.write(config.toString(2)); + } + + // Reload pairing configuration immediately + PairingAuth.reload(); + + JOptionPane.showMessageDialog(this, + "Configuration saved successfully!\n\n" + + "File location: " + configFile.getAbsolutePath() + "\n\n" + + "Restart QZ Tray for changes to take full effect.", + "Success", + JOptionPane.INFORMATION_MESSAGE); + dispose(); + } catch (IOException ex) { + JOptionPane.showMessageDialog(this, + "Error saving configuration: " + ex.getMessage(), + "Error", + JOptionPane.ERROR_MESSAGE); + } + } + + public static void showIfNeeded(Frame parent) { + File configFile = new File(FileUtilities.USER_DIR.toFile(), "pairing-config.json"); + if (!configFile.exists()) { + new PairingConfigDialog(parent).setVisible(true); + } + } +} diff --git a/old code/tray/src/qz/ui/SiteManagerDialog.java b/old code/tray/src/qz/ui/SiteManagerDialog.java new file mode 100755 index 0000000..e59edf3 --- /dev/null +++ b/old code/tray/src/qz/ui/SiteManagerDialog.java @@ -0,0 +1,614 @@ +package qz.ui; + +import org.bouncycastle.util.io.pem.PemObject; +import org.bouncycastle.util.io.pem.PemWriter; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.auth.Certificate; +import qz.common.Constants; +import qz.common.PropertyHelper; +import qz.installer.certificate.CertificateChainBuilder; +import qz.installer.certificate.CertificateManager; +import qz.installer.certificate.KeyPairWrapper; +import qz.ui.component.*; +import qz.utils.*; + +import javax.swing.*; +import javax.swing.border.Border; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; +import java.awt.*; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.UnsupportedFlavorException; +import java.awt.dnd.*; +import java.awt.event.*; +import java.io.*; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Created by Tres on 2/23/2015. + */ +public class SiteManagerDialog extends BasicDialog implements Runnable { + + private static final Logger log = LogManager.getLogger(SiteManagerDialog.class); + + private static final String IMPORT_NEEDED = "The provided certificate \"%s\" is unrecognized and not yet trusted.\n" + + "Would you like to automatically copy it to \"%s\"?"; + private static final String IMPORT_FAILED = "Failed to import certificate. Please import manually."; + private static final String INVALID_CERTIFICATE = "An exception occurred importing the certificate. Please check the logs for details."; + private static final String IMPORT_QUESTION = "Successfully created a new demo keypair. Automatically install?"; + + private static final String DEMO_CERT_QUESTION = "Create a new demo keypair for %s?\n" + + "* This keypair will only work on this computer.\n" + + "* This should only be done by developers.\n" + + "* See also https://qz.io/wiki/signing"; + private static final String DEMO_CERT_NAME = String.format("%s Demo Cert", Constants.ABOUT_TITLE); + + private JSplitPane splitPane; + + private JTabbedPane tabbedPane; + private Border plainBorder; + private Color plainBackground; + private Border dragBorder; + + private ContainerList allowList; + private ContainerList blockList; + + private CertificateTable certTable; + private IconCache iconCache; + private PropertyHelper prefs; + + private JButton addButton; + private JButton deleteButton; + private JCheckBox strictModeCheckBox; + + private Thread readerThread; + private AtomicBoolean threadRunning; + + private long allowTick = -1; + private long blockTick = -1; + + + public SiteManagerDialog(JMenuItem caller, IconCache iconCache, PropertyHelper prefs) { + super(caller, iconCache); + this.iconCache = iconCache; + this.prefs = prefs; + certTable = new CertificateTable(iconCache); + initComponents(); + } + + public void initComponents() { + allowList = new ContainerList<>(); + allowList.setTag(Constants.ALLOW_FILE); + blockList = new ContainerList<>(); + blockList.setTag(Constants.BLOCK_FILE); + + splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT); + splitPane.setResizeWeight(0.5); + + tabbedPane = new JTabbedPane(); + appendListTab(allowList.getList(), Constants.ALLOWED, IconCache.Icon.ALLOW_ICON, KeyEvent.VK_A); + appendListTab(blockList.getList(), Constants.BLOCKED, IconCache.Icon.BLOCK_ICON, KeyEvent.VK_B); + + setHeader(tabbedPane.getSelectedIndex() == 0? Constants.ALLOW_SITES_LABEL:Constants.BLOCK_SITES_LABEL); + + tabbedPane.addChangeListener(e -> { + clearSelection(); + + switch(tabbedPane.getSelectedIndex()) { + case 1: setHeader(Constants.BLOCK_SITES_LABEL); + blockList.getList().setSelectedIndex(0); + break; + default: + setHeader(Constants.ALLOW_SITES_LABEL); + allowList.getList().setSelectedIndex(0); + } + }); + plainBorder = tabbedPane.getBorder(); + plainBackground = tabbedPane.getBackground(); + dragBorder = BorderFactory.createLineBorder(Constants.TRUSTED_COLOR); + + final ListModel allowListModel = allowList.getList().getModel(); + final ListModel blockListModel = blockList.getList().getModel(); + + allowListModel.addListDataListener(new ListDataListener() { + @Override + public void intervalAdded(ListDataEvent e) { refreshTabTitle(); } + + @Override + public void intervalRemoved(ListDataEvent e) { refreshTabTitle(); } + + @Override + public void contentsChanged(ListDataEvent e) { refreshTabTitle(); } + + public void refreshTabTitle() { + String title = Constants.ALLOWED + (String.format(allowListModel.getSize() > 0? " (%s)":"", allowListModel.getSize())); + tabbedPane.setTitleAt(0, title); + } + }); + + blockList.getList().getModel().addListDataListener(new ListDataListener() { + @Override + public void intervalAdded(ListDataEvent e) { refreshTabTitle(); } + + @Override + public void intervalRemoved(ListDataEvent e) { refreshTabTitle(); } + + @Override + public void contentsChanged(ListDataEvent e) { refreshTabTitle(); } + + public void refreshTabTitle() { + String title = Constants.BLOCKED + (String.format(blockListModel.getSize() > 0? " (%s)":"", blockListModel.getSize())); + tabbedPane.setTitleAt(1, title); + } + }); + + addButton = new JButton("+"); + Font addFont = addButton.getFont(); + JPopupMenu addMenu = new JPopupMenu(); + JMenuItem browseItem = new JMenuItem("Browse...", iconCache.getIcon(IconCache.Icon.FOLDER_ICON)); + browseItem.setToolTipText("Browse for a certificate to import."); + browseItem.setMnemonic(KeyEvent.VK_B); + browseItem.addActionListener(e -> { + File chooseFolder = null; + for(String folder : new String[] { "Downloads", "Desktop", ""}) { + Path folderFile = Paths.get(System.getProperty("user.home"), folder); + if(folderFile.toFile().exists()) { + chooseFolder = folderFile.toFile(); + break; + } + } + FileDialog fileDialog = new java.awt.FileDialog(this); + fileDialog.setDirectory(chooseFolder.toString()); + fileDialog.setMultipleMode(false); + fileDialog.setVisible(true); + addCertificates(fileDialog.getFiles(), getSelectedList(), true); + }); + JMenuItem createNewItem = new JMenuItem("Create New...", iconCache.getIcon(IconCache.Icon.SETTINGS_ICON)); + createNewItem.setToolTipText("Developers only: Create and import a new demo keypair for signing."); + createNewItem.setMnemonic(KeyEvent.VK_N); + createNewItem.addActionListener(e -> { + int generateCert = JOptionPane.showConfirmDialog(this, String.format(DEMO_CERT_QUESTION, Constants.ABOUT_TITLE), "Please Confirm", JOptionPane.YES_NO_OPTION); + if(generateCert != JOptionPane.YES_OPTION) { + return; + } + try { + Path created = createDemoCertificate(); + int installKeypair = JOptionPane.showConfirmDialog(this, IMPORT_QUESTION, "Keypair Created", JOptionPane.YES_NO_OPTION); + if(installKeypair == JOptionPane.YES_OPTION) { + addCertificates(new File[] {created.resolve(Constants.SIGNING_CERTIFICATE).toFile()}, allowList, true); + } + ShellUtilities.browseDirectory(created); + } + catch(Throwable t) { + JOptionPane.showMessageDialog(this, "Sorry, an error occurred, please check the logs."); + log.error("An exception occurred creating or installing the demo certificate", t); + } + }); + addMenu.add(browseItem); + addMenu.add(createNewItem); + addButton.setFont(addFont.deriveFont(Font.BOLD, addFont.getSize() * 1.50f)); + addButton.setForeground(Constants.TRUSTED_COLOR); + addButton.setBorderPainted(false); + addButton.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + addMenu.show(addButton, e.getX(), e.getY()); + } + }); + addButton.setEnabled(true); + addKeyListener(KeyEvent.VK_PLUS, addButton); + + deleteButton = new JButton("-"); + Font deleteFont = deleteButton.getFont(); + deleteButton.setFont(deleteFont.deriveFont(Font.BOLD, deleteFont.getSize() * 1.50f)); + deleteButton.setForeground(Constants.WARNING_COLOR); + addButton.setBorderPainted(false); + deleteButton.addActionListener(e -> { + removeCertificate(getSelectedCertificate(), getSelectedList()); + deleteButton.setEnabled(false); + clearSelection(); + }); + deleteButton.setEnabled(false); + addKeyListener(KeyEvent.VK_DELETE, deleteButton); + addKeyListener(KeyEvent.VK_BACK_SPACE, deleteButton); + + // Fixes alignment issues with +/- + JSeparator separator = new JSeparator(); + separator.setOpaque(false); + separator.setForeground(new Color(0, 0, 0, 0)); + + JPanel tabbedPanePanel = new JPanel(); + tabbedPanePanel.setLayout(new BoxLayout(tabbedPanePanel, BoxLayout.Y_AXIS)); + tabbedPanePanel.add(tabbedPane); + JToolBar toolBar = new JToolBar(); + toolBar.setFloatable(false); + toolBar.add(addButton, LEFT_ALIGNMENT); + toolBar.add(deleteButton, LEFT_ALIGNMENT); + toolBar.add(separator, LEFT_ALIGNMENT); + tabbedPanePanel.add(toolBar, LEFT_ALIGNMENT); + splitPane.add(tabbedPanePanel); + splitPane.add(new JScrollPane(certTable)); + splitPane.setAlignmentX(Component.LEFT_ALIGNMENT); + certTable.autoSize(); + + readerThread = new Thread(this); + threadRunning = new AtomicBoolean(false); + + strictModeCheckBox = new JCheckBox(Constants.STRICT_MODE_LABEL, PrefsSearch.getBoolean(ArgValue.TRAY_STRICTMODE, prefs)); + strictModeCheckBox.setToolTipText(Constants.STRICT_MODE_TOOLTIP); + strictModeCheckBox.addActionListener(e -> { + if (strictModeCheckBox.isSelected() && !new ConfirmDialog(null, "Please Confirm", iconCache).prompt(Constants.STRICT_MODE_CONFIRM)) { + strictModeCheckBox.setSelected(false); + return; + } + Certificate.setTrustBuiltIn(!strictModeCheckBox.isSelected()); + prefs.setProperty(ArgValue.TRAY_STRICTMODE, strictModeCheckBox.isSelected()); + certTable.refreshComponents(); + }); + refreshStrictModeCheckbox(); + + setContent(splitPane, true); + + // Register drag/drop events + allowList.getList().setDragEnabled(true); + blockList.getList().setDragEnabled(true); + tabbedPane.setDropTarget(new DropTarget() { + @Override + public synchronized void dragEnter(DropTargetDragEvent e) { + for(DataFlavor flavor : e.getTransferable().getTransferDataFlavors()) { + if(flavor.equals(DataFlavor.javaFileListFlavor)) { + // Dragged from file system + tabbedPane.setBorder(dragBorder); + e.acceptDrag(DnDConstants.ACTION_COPY); + return; + } else if(flavor.equals(DataFlavor.stringFlavor)) { + // Dragged from JList + Component target = e.getDropTargetContext().getComponent(); + if(target instanceof JTabbedPane) { + target.setBackground(Constants.TRUSTED_COLOR); + e.acceptDrag(DnDConstants.ACTION_MOVE); + } + } + } + } + + @Override + public synchronized void dragExit(DropTargetEvent e) { + tabbedPane.setBorder(plainBorder); + tabbedPane.setBackground(plainBackground); + } + + @Override + public synchronized void drop(DropTargetDropEvent e) { + tabbedPane.setBorder(plainBorder); + tabbedPane.setBackground(plainBackground); + try { + e.acceptDrop(DnDConstants.ACTION_COPY); + addCertificates(e.getTransferable().getTransferData(DataFlavor.javaFileListFlavor), getSelectedList(), true); + return; + } + catch(IOException | UnsupportedFlavorException ignore) {} + + e.acceptDrop(DnDConstants.ACTION_MOVE); + Component targetComponent = e.getDropTargetContext().getComponent(); + if(targetComponent instanceof JTabbedPane) { + JTabbedPane tabbedPane = (JTabbedPane)targetComponent; + CertificateDisplay selectedCert = getSelectedCertificate(); + int targetIndex = tabbedPane.indexAtLocation(e.getLocation().x, e.getLocation().y); + ContainerList target = getListByIndex(targetIndex); + ContainerList source = getSelectedList(); + if(source != target) { + addCertificate(selectedCert, target, false); + removeCertificate(selectedCert, source); + clearSelection(); + } + } + } + }); + } + + private void refreshStrictModeCheckbox() { + // Hide strict-mode checkbox for standard configurations + if(Certificate.hasAdditionalCAs() || strictModeCheckBox.isSelected()) { + // Add checkbox near "close" button + addPanelComponent(strictModeCheckBox); + } + } + + @Override + public void setVisible(boolean visible) { + if (visible && !readerThread.isAlive()) { + threadRunning.set(true); + readerThread = new Thread(this); + readerThread.start(); + } else { + threadRunning.set(false); + } + + if (visible && getSelectedList().getList().getSelectedIndex() < 0) { + selectFirst(); + } + + super.setVisible(visible); + } + + public SiteManagerDialog selectFirst() { + SwingUtilities.invokeLater(() -> getSelectedList().getList().setSelectedIndex(0)); + + return this; + } + + private void addCertificateSelectionListener(final JList list) { + list.addListSelectionListener(e -> { + if (list.getSelectedValue() instanceof CertificateDisplay) { + certTable.setCertificate(((CertificateDisplay)list.getSelectedValue()).getCert()); + deleteButton.setEnabled(true); + } else { + deleteButton.setEnabled(false); + } + }); + } + + @SuppressWarnings("unchecked") + private void appendListTab(JList list, String title, IconCache.Icon icon, int mnemonic) { + list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + list.setLayoutOrientation(JList.VERTICAL); + JScrollPane scrollPane = new JScrollPane(list); + tabbedPane.addTab(title, getIcon(icon), scrollPane); + tabbedPane.setMnemonicAt(tabbedPane.indexOfComponent(scrollPane), mnemonic); + addCertificateSelectionListener(list); + list.setCellRenderer(new CertificateListCellRenderer()); + } + + private class CertificateListCellRenderer extends DefaultListCellRenderer { + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel)super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + if (value instanceof CertificateDisplay) { + if (((CertificateDisplay)value).isLocal()) { + label.setIcon(SiteManagerDialog.super.getIcon(IconCache.Icon.SAVED_ICON)); + } else { + label.setIcon(SiteManagerDialog.super.getIcon(IconCache.Icon.DESKTOP_ICON)); + } + } else { + label.setIcon(null); + } + return label; + } + } + + private CertificateDisplay getSelectedCertificate() { + return (CertificateDisplay)getSelectedList().getList().getSelectedValue(); + } + + @SuppressWarnings("unused") + private String getSelectedTabName() { + if (tabbedPane.getSelectedIndex() >= 0) { + return tabbedPane.getTitleAt(tabbedPane.getSelectedIndex()); + } + + return ""; + } + + private void removeCertificate(CertificateDisplay certDisplay, ContainerList list) { + if (list.contains(certDisplay)) { + String saveFile = (list == allowList ? Constants.ALLOW_FILE : Constants.BLOCK_FILE); + if(certDisplay != null && certDisplay.getCert() != null) { + boolean localOk = FileUtilities.deleteFromFile(saveFile, certDisplay.getCert().data(), true); + boolean sharedOk = FileUtilities.deleteFromFile(saveFile, certDisplay.getCert().data(), false); + if(localOk || sharedOk) { + list.remove(certDisplay); + return; + } + } + log.warn("Error removing {} from the list of {} sites", certDisplay, saveFile); + } + } + + private void addCertificate(CertificateDisplay certDisplay, ContainerList list, boolean selectWhenDone) { + if (!list.contains(certDisplay) && !Certificate.UNKNOWN.equals(certDisplay.getCert())) { + FileUtilities.printLineToFile(list == allowList ? Constants.ALLOW_FILE : Constants.BLOCK_FILE, certDisplay.getCert().data()); + list.addAndCallback(certDisplay, selectWhenDone ? () -> { + list.getList().setSelectedValue(certDisplay, true); + return null; + } : null); + } + } + + private void addCertificates(Object dragged, ContainerList list, boolean selectWhenDone) { + if(dragged instanceof java.util.List) { + java.util.List certFiles = (java.util.List)dragged; + if(certFiles.size() > 0) { + if(certFiles.get(0) instanceof File) { + addCertificates((File[])certFiles.toArray(new File[certFiles.size()]), list, selectWhenDone); + } else { + System.out.println("Nope: " + certFiles.get(0).getClass().getName()); + } + + } + + } else { + log.warn("Could not convert certificate to from unknown type: {}", dragged.getClass().getCanonicalName()); + } + } + + private void addCertificates(File[] certFiles, ContainerList list, boolean selectWhenDone) { + for(File file : certFiles) { + try { + Certificate importCert = new Certificate(file.toPath()); + if (importCert.isValid()) { + addCertificate(new CertificateDisplay(importCert, true), list, selectWhenDone); + continue; + } + // Warn of any invalid certs + showInvalidCertWarning(file, importCert); + } + catch(CertificateException | IOException e) { + log.warn("Unable to import cert {}", file, e); + JOptionPane.showMessageDialog(this, String.format(INVALID_CERTIFICATE), "Import failed", JOptionPane.ERROR_MESSAGE); + } + } + } + + private void showInvalidCertWarning(File file, Certificate cert) { + Path override = SystemUtilities.getJarParentPath().resolve(Constants.OVERRIDE_CERT); + String message = String.format(IMPORT_NEEDED, + cert.getCommonName(), + override); + int copyAnswer = JOptionPane.showConfirmDialog(this, message, "Unrecognized Certificate", JOptionPane.YES_NO_OPTION); + if(copyAnswer == JOptionPane.YES_OPTION) { + Cursor backupCursor = getCursor(); + setCursor(new Cursor(Cursor.WAIT_CURSOR)); + boolean copySuccess = ShellUtilities.elevateCopy(file.toPath(), SystemUtilities.getJarParentPath().resolve(Constants.OVERRIDE_CERT)); + setCursor(backupCursor); + if(copySuccess) { + Certificate.scanAdditionalCAs(); + addCertificates(new File[] { file }, allowList, true); + refreshStrictModeCheckbox(); + } else { + JOptionPane.showMessageDialog(this, String.format(IMPORT_FAILED), "Import failed", JOptionPane.WARNING_MESSAGE); + } + } + } + + private ContainerList getSelectedList() { + return getListByIndex(tabbedPane.getSelectedIndex()); + } + + private ContainerList getListByIndex(int index) { + if (index == 0) { + return allowList; + } + + return blockList; + } + + private void clearSelection() { + certTable.setCertificate(null); + allowList.getList().clearSelection(); + blockList.getList().clearSelection(); + } + + public void run() { + threadRunning.set(true); + + File allowFile = FileUtilities.getFile(Constants.ALLOW_FILE, true); + File allowFileShare = FileUtilities.getFile(Constants.ALLOW_FILE, false); + + File blockFile = FileUtilities.getFile(Constants.BLOCK_FILE, true); + File blockFileShare = FileUtilities.getFile(Constants.BLOCK_FILE, false); + + boolean initialSelection = true; + + allowTick = allowTick < 0? 0:allowTick; + blockTick = blockTick < 0? 0:blockTick; + + // Reads the certificate allowed/blocked files and updates the certificate listing + while(threadRunning.get()) { + if (isVisible()) { + if (allowFile.lastModified() > allowTick + || (allowFileShare != null && allowFileShare.lastModified() > allowTick)) { + allowTick = Math.max(allowFile.lastModified(), (allowFileShare == null? 0:allowFileShare.lastModified())); + readCertificates(allowList, allowFileShare, false); + readCertificates(allowList, allowFile, true); + } else if (blockFile.lastModified() > blockTick + || (blockFileShare != null && blockFileShare.lastModified() > blockTick)) { + blockTick = Math.max(blockFile.lastModified(), (blockFileShare == null? 0:blockFileShare.lastModified())); + readCertificates(blockList, blockFileShare, false); + readCertificates(blockList, blockFile, true); + } else { + sleep(2000); + } + + if (initialSelection) { + selectFirst(); + initialSelection = false; + } + } + } + threadRunning.set(false); + } + + public void sleep(int millis) { + try { Thread.sleep(millis); } catch(InterruptedException ignore) {} + } + + /** + * Reads a certificate data file and updates the corresponding {@code ArrayList} + * + * @param certList The {@code ArrayList} requiring updating + * @param file The data file containing allow/block certificate information + */ + public ArrayList readCertificates(ArrayList certList, File file, boolean local) { + if (file == null) { return certList; } + + try(BufferedReader br = new BufferedReader(new FileReader(file))) { + String line; + while((line = br.readLine()) != null) { + if (line.startsWith("#")) { continue; } //treat these lines as comments + String[] data = line.split("\\t"); + + if (data.length == Certificate.saveFields.length) { + HashMap dataMap = new HashMap<>(); + for(int i = 0; i < data.length; i++) { + dataMap.put(Certificate.saveFields[i], data[i]); + } + + CertificateDisplay certificate = new CertificateDisplay(Certificate.loadCertificate(dataMap), local); + + // Don't include the unsigned certificate if we are blocking it, there is a menu option instead + if (!certList.contains(certificate) && !Certificate.UNKNOWN.equals(certificate.getCert())) { + certList.add(certificate); + } + } + } + } + catch(IOException ioe) { + ioe.printStackTrace(); + } + + return certList; + } + + /** + * Creates a demo cert and key, returns the parent folder where they were created + */ + private static Path createDemoCertificate() throws Throwable { + CertificateChainBuilder chainBuilder = new CertificateChainBuilder(DEMO_CERT_NAME); + + KeyPairWrapper keyPair = chainBuilder.createCaCert(); + // Some locations a user might be happy with + Path homeDir = Paths.get(System.getProperty("user.home")); + Path[] paths = { homeDir.resolve("Desktop"), homeDir.resolve("Downloads"), homeDir }; + for(Path path : paths) { + File file = path.toAbsolutePath().normalize().toFile(); + if(file.isDirectory() && file.canWrite()) { + Path certData = file.toPath().resolve(DEMO_CERT_NAME); + if(certData.toFile().mkdir() || (certData.toFile().isDirectory() && certData.toFile().exists())) { + // Write our PKCS#8 PEM file + File privateKey = certData.resolve(Constants.SIGNING_PRIVATE_KEY).toFile(); + PemObject pemObject = new PemObject("PRIVATE KEY", keyPair.getKey().getEncoded()); + log.info("Writing PKCS#8 Private Key: {}", privateKey); + FileWriter writer = new FileWriter(privateKey); + PemWriter pemWriter = new PemWriter(writer); + pemWriter.writeObject(pemObject); + pemWriter.flush(); + + // Write our x509 certificate file + log.info("Writing x509 Certificate: {}", privateKey); + File certificate = certData.resolve(Constants.SIGNING_CERTIFICATE).toFile(); + CertificateManager.writeCert(keyPair.getCert(), certificate); + return certData; + } + } + } + throw new CertificateException("Can't create certificate"); + } + +} diff --git a/old code/tray/src/qz/ui/ThemeUtilities.java b/old code/tray/src/qz/ui/ThemeUtilities.java new file mode 100755 index 0000000..7804bf0 --- /dev/null +++ b/old code/tray/src/qz/ui/ThemeUtilities.java @@ -0,0 +1,41 @@ +package qz.ui; + +import org.apache.commons.lang3.ArrayUtils; + +import javax.swing.*; +import java.awt.*; + +public class ThemeUtilities { + public static void refreshAll(Container container, Component ... orphans) { + // Handle orphaned UI objects (e.g. Component added to a message dialog) + for(Component orphan : orphans) { + recurseOrphanedComponents(orphan); + } + refreshAll(ArrayUtils.addAll(container.getComponents(), orphans)); + } + + private static void refreshAll(Component ... components) { + for(Component c : components) { + if (c instanceof Themeable) { + ((Themeable)c).refresh(); + } + if (c instanceof Container) { + refreshAll((Container)c); + } + } + } + + /** + * Inefficient yet effective way to recurse orphaned component's UI changes + */ + private static Container recurseOrphanedComponents(Component c) { + if (c != null) { + SwingUtilities.updateComponentTreeUI(c); + if (c instanceof JRootPane) { + return (Container)c; + } + return recurseOrphanedComponents(c.getParent()); + } + return null; + } +} diff --git a/old code/tray/src/qz/ui/Themeable.java b/old code/tray/src/qz/ui/Themeable.java new file mode 100755 index 0000000..02f37f8 --- /dev/null +++ b/old code/tray/src/qz/ui/Themeable.java @@ -0,0 +1,5 @@ +package qz.ui; + +public interface Themeable { + void refresh(); +} diff --git a/old code/tray/src/qz/ui/component/CertificateDisplay.java b/old code/tray/src/qz/ui/component/CertificateDisplay.java new file mode 100755 index 0000000..b790e93 --- /dev/null +++ b/old code/tray/src/qz/ui/component/CertificateDisplay.java @@ -0,0 +1,38 @@ +package qz.ui.component; + +import qz.auth.Certificate; + +public class CertificateDisplay { + + private Certificate cert; + private boolean local = true; + + public CertificateDisplay(Certificate cert, boolean local) { + this.cert = cert; + this.local = local; + } + + public Certificate getCert() { + return cert; + } + + public boolean isLocal() { + return local; + } + + @Override + public String toString() { + return cert.toString(); + } + + @Override + public boolean equals(Object obj) { + Object compareTo = obj; + if (obj instanceof CertificateDisplay) { + compareTo = ((CertificateDisplay)obj).getCert(); + } + + //true if the passed object is a matching certificate (either directly or from another CertificateDisplay object) + return cert.equals(compareTo); + } +} diff --git a/old code/tray/src/qz/ui/component/CertificateTable.java b/old code/tray/src/qz/ui/component/CertificateTable.java new file mode 100755 index 0000000..b5b24b8 --- /dev/null +++ b/old code/tray/src/qz/ui/component/CertificateTable.java @@ -0,0 +1,222 @@ +package qz.ui.component; + +import qz.auth.Certificate; +import qz.common.Constants; +import qz.ui.Themeable; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.*; +import java.time.*; +import java.time.temporal.ChronoUnit; +import java.util.TimeZone; +import java.util.function.Function; + +import static qz.auth.Certificate.*; + +/** + * Created by Tres on 2/22/2015. + * Displays Certificate information in a JTable + */ +public class CertificateTable extends DisplayTable implements Themeable { + private Certificate cert; + + private static final TimeZone DEFAULT_TIME_ZONE = TimeZone.getTimeZone("UTC"); + private static final TimeZone ALTERNATE_TIME_ZONE = TimeZone.getDefault(); + private Instant warn; + private Instant now; + + enum CertificateField { + ORGANIZATION("Organization", (Certificate cert) -> cert.getOrganization()), + COMMON_NAME("Common Name", (Certificate cert) -> cert.getCommonName()), + TRUSTED("Trusted", (Certificate cert) -> cert.isTrusted()), + VALID_FROM("Valid From", (Certificate cert) -> cert.getValidFrom()), + VALID_TO("Valid To", (Certificate cert) -> cert.getValidTo()), + FINGERPRINT("Fingerprint", (Certificate cert) -> cert.getFingerprint()); + + String description; + Function getter; + TimeZone timeZone = DEFAULT_TIME_ZONE; // Date fields only + + CertificateField(String description, Function getter) { + this.description = description; + this.getter = getter; + } + + public String getValue(Certificate cert) { + String certFieldValue = getter.apply(cert).toString(); + switch(this) { + case VALID_FROM: + case VALID_TO: + if (!certFieldValue.equals("Not Provided")) { + try { + // Parse the date string as UTC (Z/GMT) + ZonedDateTime utcTime = LocalDateTime.from(DATE_PARSE.parse(certFieldValue)).atZone(ZoneOffset.UTC); + // Shift to the new timezone + ZonedDateTime zonedTime = Instant.from(utcTime).atZone(timeZone.toZoneId()); + // Append a short timezone name e.g. "EST" + return DATE_PARSE.format(zonedTime) + " " + timeZone.getDisplayName(false, TimeZone.SHORT); + } catch (DateTimeException ignore) {} + } + // fallthrough + default: + return certFieldValue; + } + } + + @Override + public String toString() { + return description; + } + + public String getDescription() { + return description; + } + + public static int size() { + return values().length; + } + + public void toggleTimeZone() { + switch(this) { + case VALID_TO: + case VALID_FROM: + this.timeZone = (timeZone == DEFAULT_TIME_ZONE? ALTERNATE_TIME_ZONE:DEFAULT_TIME_ZONE); + break; + default: + throw new UnsupportedOperationException("TimeZone is only supported for date fields"); + } + } + } + + public CertificateTable(IconCache iconCache) { + super(iconCache); + setDefaultRenderer(Object.class, new CertificateTableCellRenderer()); + addMouseListener(new MouseAdapter() { + Point loc = new Point(-1, -1); + + @Override + public void mousePressed(MouseEvent e) { + super.mousePressed(e); + JTable target = (JTable)e.getSource(); + int x = target.getSelectedColumn(); + int y = target.getSelectedRow(); + // Only trigger after the cell is click AND highlighted. + if (loc.distance(x, y) == 0) { + CertificateField rowKey = (CertificateField)target.getValueAt(y, 0); + switch(rowKey) { + case VALID_FROM: + case VALID_TO: + rowKey.toggleTimeZone(); + refreshComponents(); + changeSelection(y, x, false, false); + break; + } + } + loc.setLocation(x, y); + } + }); + + } + + public void setCertificate(Certificate cert) { + this.cert = cert; + refreshComponents(); + } + + @Override + public void refreshComponents() { + if (cert == null) { + removeRows(); + repaint(); + return; + } + + now = Instant.now(); + warn = now.plus(Constants.EXPIRY_WARN, ChronoUnit.DAYS); + + removeRows(); + + // First Column + for(CertificateField field : CertificateField.values()) { + if(field.equals(CertificateField.TRUSTED) && !Certificate.isTrustBuiltIn()) { + continue; // Remove "Verified by" text; uncertain in strict mode + } + model.addRow(new Object[] {field, field.getValue(cert)}); + } + + repaint(); + } + + @Override + public void refresh() { + refreshComponents(); + ((StyledTableCellRenderer)getDefaultRenderer(Object.class)).refresh(); + } + + public void autoSize() { + super.autoSize(CertificateField.size(), 2); + } + + /** Custom cell renderer for JTable to allow colors and styles not directly available in a JTable */ + private class CertificateTableCellRenderer extends StyledTableCellRenderer { + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { + JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col); + + // First Column + if (value instanceof CertificateField) { + switch((CertificateField)value) { + case VALID_FROM: + boolean futureExpiration = cert.getValidFromDate().isAfter(now); + label = stylizeLabel(futureExpiration? STATUS_WARNING:STATUS_NORMAL, label, isSelected, "future inception"); + break; + case VALID_TO: + boolean expiresSoon = cert.getValidToDate().isBefore(warn); + boolean expired = cert.getValidToDate().isBefore(now); + String reason = expired? "expired":(expiresSoon? "expires soon":null); + + label = stylizeLabel(expiresSoon || expired? STATUS_WARNING:STATUS_NORMAL, label, isSelected, reason); + break; + default: + label = stylizeLabel(STATUS_NORMAL, label, isSelected); + break; + } + if (iconCache != null) { + label.setIcon(iconCache.getIcon(IconCache.Icon.FIELD_ICON)); + } + return label; + } + + // Second Column + if (cert == null || col < 1) { return stylizeLabel(STATUS_NORMAL, label, isSelected); } + + CertificateField field = (CertificateField)table.getValueAt(row, col - 1); + if (field == null) { return stylizeLabel(STATUS_NORMAL, label, isSelected); } + switch(field) { + case TRUSTED: + if(cert.isValid()) { + if(cert.isSponsored() && Certificate.isTrustBuiltIn()) { + // isTrustBuiltIn: Assume only QZ sponsors + label.setText(Constants.SPONSORED_CERT); + } else { + label.setText(Constants.TRUSTED_CERT); + } + } else { + label.setText(Constants.UNTRUSTED_CERT); + } + return stylizeLabel(!cert.isValid()? STATUS_WARNING:STATUS_TRUSTED, label, isSelected); + case VALID_FROM: + boolean futureExpiration = cert.getValidFromDate().isAfter(now); + return stylizeLabel(futureExpiration? STATUS_WARNING:STATUS_NORMAL, label, isSelected); + case VALID_TO: + boolean expiresSoon = cert.getValidToDate().isBefore(warn); + boolean expired = cert.getValidToDate().isBefore(now); + return stylizeLabel(expiresSoon || expired? STATUS_WARNING:STATUS_NORMAL, label, isSelected); + default: + return stylizeLabel(STATUS_NORMAL, label, isSelected); + } + } + } +} diff --git a/old code/tray/src/qz/ui/component/ContainerList.java b/old code/tray/src/qz/ui/component/ContainerList.java new file mode 100755 index 0000000..f655ac6 --- /dev/null +++ b/old code/tray/src/qz/ui/component/ContainerList.java @@ -0,0 +1,216 @@ +package qz.ui.component; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.Callable; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Created by Tres on 2/28/2015. + *

+ * ArrayList which is linked to an internal JList and synced with its + * DefaultListModel + *

+ * Updates to the JList are non-blocking and submitted on the Event Dispatch thread. + * Class created to reduce the amount of thread-safe calls cluttering up the SiteManagerDialog. + */ +public class ContainerList extends ArrayList { + + private JList certList; + private SyncedListModel listModel; + private Object tag; + + public ContainerList() { + super(); + this.listModel = new SyncedListModel(); + this.certList = new JList(listModel); + } + + /** + * Sets a miscellaneous Object placeholder + * + * @param tag A generic Object placeholder + */ + public void setTag(Object tag) { + this.tag = tag; + } + + public Object getTag() { + return tag; + } + + public JList getList() { + return certList; + } + + @Override + public boolean remove(final Object o) { + SwingUtilities.invokeLater(() -> { + listModel.skipNextSuperCall(); + listModel.removeElement(o); + }); + return super.remove(o); + } + + @Override + public boolean add(final D element) { + SwingUtilities.invokeLater(() -> { + listModel.skipNextSuperCall(); + listModel.addElement(element); + }); + return super.add(element); + } + + @Override + public void add(final int index, final D element) { + SwingUtilities.invokeLater(() -> { + listModel.skipNextSuperCall(); + listModel.add(index, element); + }); + super.add(index, element); + } + + public boolean addAndCallback(final D element, final Callable callable) { + SwingUtilities.invokeLater(() -> { + listModel.skipNextSuperCall(); + listModel.addElement(element); + if(callable != null) { + try { + callable.call(); + } + catch(Exception ignore) {} + } + }); + return super.add(element); + } + + @Override + public boolean addAll(final Collection collection) { + SwingUtilities.invokeLater(() -> { + for(D element : collection) { + listModel.skipNextSuperCall(); + listModel.addElement(element); + } + }); + return super.addAll(collection); + } + + @Override + public boolean addAll(final int index, final Collection c) { + SwingUtilities.invokeLater(() -> { + int counter = 0; + for(D element : c) { + listModel.skipNextSuperCall(); + listModel.add(index + counter++, element); + } + }); + return super.addAll(index, c); + } + + @Override + public void clear() { + SwingUtilities.invokeLater(() -> { + listModel.skipNextSuperCall(); + listModel.removeAllElements(); + }); + super.clear(); + } + + @Override + protected void removeRange(final int fromIndex, final int toIndex) { + SwingUtilities.invokeLater(() -> { + listModel.skipNextSuperCall(); + listModel.removeRange(fromIndex, toIndex); + }); + super.removeRange(fromIndex, toIndex); + } + + /** + * An internal subclass of DefaultListModel to support the sync-back of elements in case the list + * model is modified directly. + */ + private class SyncedListModel extends DefaultListModel { + public SyncedListModel() { + super(); + } + + AtomicBoolean performSuperCall = new AtomicBoolean(true); + + /** + * Flag to prevent adding items twice or circular references. This intentionally resets any time data is added + * or removed, so it must be called before each operation to be honored. + */ + public void skipNextSuperCall() { + this.performSuperCall.set(false); + } + + @Override + public void add(int index, D element) { + super.add(index, element); + if (performSuperCall.getAndSet(true) && !ContainerList.this.contains(element)) { + ContainerList.super.add(element); + } + } + + @Override + public void addElement(D element) { + super.addElement(element); + if (performSuperCall.getAndSet(true) && !ContainerList.this.contains(element)) { + ContainerList.super.add(element); + } + } + + @Override + public D remove(int index) { + D removed = super.remove(index); + if (performSuperCall.getAndSet(true)) { + ContainerList.super.remove(index); + } + return removed; + } + + @Override + public boolean removeElement(Object o) { + boolean success = super.removeElement(o); + if (performSuperCall.getAndSet(true)) { + ContainerList.super.remove(o); + } + return success; + } + + @Override + public void removeElementAt(int index) { + super.removeElementAt(index); + if (performSuperCall.getAndSet(true)) { + ContainerList.super.remove(index); + } + super.removeElementAt(index); + } + + @Override + public void removeAllElements() { + super.removeAllElements(); + if (performSuperCall.getAndSet(true)) { + ContainerList.super.clear(); + } + } + + @Override + public void removeRange(int fromIndex, int toIndex) { + super.removeRange(fromIndex, toIndex); + if (performSuperCall.getAndSet(true)) { + ContainerList.super.removeRange(fromIndex, toIndex); + } + } + + @Override + public D set(int index, D element) { + D certificate = super.set(index, element); + if (performSuperCall.getAndSet(true)) { + ContainerList.super.set(index, element); + } + return certificate; + } + } +} diff --git a/old code/tray/src/qz/ui/component/DisplayTable.java b/old code/tray/src/qz/ui/component/DisplayTable.java new file mode 100755 index 0000000..50b6fc6 --- /dev/null +++ b/old code/tray/src/qz/ui/component/DisplayTable.java @@ -0,0 +1,74 @@ +package qz.ui.component; + +import qz.utils.SystemUtilities; + +import javax.swing.*; +import javax.swing.table.DefaultTableModel; + +/** + * Displays information in a JTable + */ +public class DisplayTable extends JTable { + + protected DefaultTableModel model; + + protected IconCache iconCache; + + public DisplayTable(IconCache iconCache) { + super(); + initComponents(); + + this.iconCache = iconCache; + } + + private void initComponents() { + model = new DefaultTableModel() { + @Override + public boolean isCellEditable(int x, int y) { return false; } + }; + model.addColumn("Field"); + model.addColumn("Value"); + + // Fix Linux row height + int origHeight = getRowHeight(); + if(SystemUtilities.getWindowScaleFactor() > 1) { + setRowHeight((int)(origHeight * SystemUtilities.getWindowScaleFactor())); + } + + getTableHeader().setReorderingAllowed(false); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + setRowSelectionAllowed(true); + + setModel(model); + } + + public void refreshComponents() { + repaint(); + } + + public void removeRows() { + for(int row = model.getRowCount() - 1; row >= 0; row--) { + model.removeRow(row); + } + } + + /** + * Sets preferred ScrollPane preferred viewable height to match the natural table height + * Leaves the ScrollPane preferred viewable width as default + */ + public void autoSize(int rows, int columns) { + removeRows(); + for(int row = 0; row < rows; row++) { + model.addRow(new Object[columns]); + } + + setPreferredScrollableViewportSize( + SystemUtilities.scaleWindowDimension( + getPreferredScrollableViewportSize().getWidth(), + getPreferredSize().getHeight()) + ); + setFillsViewportHeight(true); + refreshComponents(); + } + +} diff --git a/old code/tray/src/qz/ui/component/EmLabel.java b/old code/tray/src/qz/ui/component/EmLabel.java new file mode 100755 index 0000000..d22929f --- /dev/null +++ b/old code/tray/src/qz/ui/component/EmLabel.java @@ -0,0 +1,31 @@ +package qz.ui.component; + +import javax.swing.*; +import java.awt.*; +import java.awt.font.TextAttribute; +import java.util.HashMap; +import java.util.Map; + +/** + * Create a label at the multiplier of its normal size, similar to CSS's "em" tag + */ +public class EmLabel extends JLabel { + public EmLabel(String text, float multiplier) { + this(text, multiplier, true); + } + public EmLabel(String text, float multiplier, boolean underline) { + super(text); + stylizeComponent(this, multiplier, underline); + } + + public static void stylizeComponent(Component j, float multiplier, boolean underline) { + Font template = j.getFont().deriveFont(multiplier * j.getFont().getSize()); + if (!underline) { + Map attributes = new HashMap<>(template.getAttributes()); + attributes.remove(TextAttribute.UNDERLINE); + j.setFont(template.deriveFont(attributes)); + } else { + j.setFont(template); + } + } +} diff --git a/old code/tray/src/qz/ui/component/IconCache.java b/old code/tray/src/qz/ui/component/IconCache.java new file mode 100755 index 0000000..659f8d1 --- /dev/null +++ b/old code/tray/src/qz/ui/component/IconCache.java @@ -0,0 +1,396 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.ui.component; + +import com.github.zafarkhaja.semver.Version; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.utils.ColorUtilities; +import qz.utils.SystemUtilities; + +import javax.imageio.ImageIO; +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.util.*; +import java.util.List; + +/** + * Created by Tres Finocchiaro on 12/12/2014. + */ +public class IconCache { + + private static final Logger log = LogManager.getLogger(IconCache.class); + + // Internal Jar path containing the images + static String RESOURCES_DIR = "/qz/ui/resources/"; + + /** + * Stores Icon paths + */ + public enum Icon { + // Tray icons + DEFAULT_ICON("qz-default.png", "qz-default-20.png", "qz-default-24.png", "qz-default-32.png", "qz-default-40.png", "qz-default-48.png"), + WARNING_ICON("qz-warning.png", "qz-warning-20.png", "qz-warning-24.png", "qz-warning-32.png", "qz-warning-40.png", "qz-warning-48.png"), + DANGER_ICON("qz-danger.png", "qz-danger-20.png", "qz-danger-24.png", "qz-danger-32.png", "qz-danger-40.png", "qz-danger-48.png"), + MASK_ICON("qz-mask.png", "qz-mask-20.png", "qz-mask-24.png", "qz-mask-32.png", "qz-mask-40.png", "qz-mask-48.png"), + + // Task bar icons - Appending "#" allows hashing under unique id + TASK_BAR_ICON("qz-default.png#", "qz-default-20.png#", "qz-default-24.png#", "qz-default-32.png#", "qz-default-40.png#", "qz-default-48.png#"), + + // Menu Item icons + EXIT_ICON("qz-exit.png"), + RELOAD_ICON("qz-reload.png"), + ABOUT_ICON("qz-about.png"), + DESKTOP_ICON("qz-desktop.png"), + SAVED_ICON("qz-saved.png"), + LOG_ICON("qz-log.png"), + FOLDER_ICON("qz-folder.png"), + SETTINGS_ICON("qz-settings.png"), + + // Dialog icons + ALLOW_ICON("qz-allow.png"), + BLOCK_ICON("qz-block.png"), + CANCEL_ICON("qz-cancel.png"), + TRUST_VERIFIED_ICON("qz-trust-verified.png"), + TRUST_SPONSORED_ICON("qz-trust-sponsored.png"), + TRUST_ISSUE_ICON("qz-trust-issue.png"), + TRUST_MISSING_ICON("qz-trust-missing.png"), + FIELD_ICON("qz-field.png"), + DELETE_ICON("qz-delete.png"), + QUESTION_ICON("qz-question.png"), + + // Banner + LOGO_ICON("qz-logo.png"), + BANNER_ICON("qz-banner.png"); + + private boolean padded = false; + private String[] fileNames; + + /** + * Default constructor + * + * @param fileNames path(s) to image + */ + Icon(String ... fileNames) { this.fileNames = fileNames; } + + /** + * Returns whether or not this icon is used for the SystemTray + * + * @return true if this icon is used for the SystemTray + */ + public boolean isTrayIcon() { + switch(this) { + case DEFAULT_ICON: + case WARNING_ICON: + case DANGER_ICON: + return true; + default: + return false; + } + } + + @Override + public String toString() { return name(); } + + /** + * Returns the full path to the Icon resource + * + * @return full path to Icon resource + */ + public String getPath() { return RESOURCES_DIR + getId(); } + + /** + * Returns the full path to the Icon resource with the specified width suffix. + * Width is determined solely by filename suffix. e.g. foo-32.png + * + * @param size size of desired image + * @return icon file name + */ + public String getId(Dimension size) { + if (size != null) { + for(String fileName : fileNames) { + if (fileName.endsWith("-" + size.width + ".png")) { + return fileName; + } + } + } + return getId(); + } + + public String getId() { + return fileNames[0]; + } + + public String[] getIds() { return fileNames; } + + private void addId(String id) { + fileNames = Arrays.copyOf(fileNames, fileNames.length + 1); + fileNames[fileNames.length - 1] = id; + } + } + + private final HashMap imageIcons; + private final HashMap images; + private static final Color TRANSPARENT = new Color(0,0,0,0); + + /** + * Default constructor. + * Builds a cache of Image and ImageIcon resources by iterating through all IconCache.Icon types + */ + public IconCache() { + imageIcons = new HashMap<>(); + images = new HashMap<>(); + buildIconCache(); + } + + /** + * Populates the internal HashMaps containing the cache + * of ImageIcons and BufferedImages + */ + private void buildIconCache() { + for(Icon i : Icon.values()) { + for (String id : i.getIds()) { + BufferedImage bi = getImageResource(RESOURCES_DIR + id); + imageIcons.put(id, new ImageIcon(bi)); + images.put(id, bi); + } + } + // Stash scaled 2x, 3x versions if missing + int maxScale = 3; + for(Icon i : Icon.values()) { + // Assume single-resource icons are lonely and want scaled instances + if (i.fileNames.length != 1) { + continue; + } + for(int scale = 2; scale <= maxScale; scale++) { + BufferedImage bi = images.get(i.getId()); + // Assume square icon (filename is derived from width only) + String id = i.getId(); + int loc = id.lastIndexOf("."); + if(loc == -1) { + continue; + } + String name = id.substring(0, loc); + String ext = id.substring(loc + 1); + String newSize = String.format("%s-%s.%s", name, bi.getWidth() * scale, ext); + if (!images.containsKey(newSize)) { + i.addId(newSize); + BufferedImage newBi = clone(bi, scale); + imageIcons.put(newSize, new ImageIcon(newBi)); + images.put(newSize, newBi); + } + } + } + } + + /** + * Returns the ImageIcon from cache + * + * @param i an IconCache.Icon + * @return the ImageIcon in the cache + */ + public ImageIcon getIcon(Icon i) { + return SystemUtilities.getWindowScaleFactor() != 1 ? + getIcon(i, true) : imageIcons.get(i.getId()); + } + + private ImageIcon getIcon(Icon i, boolean inferScale) { + if(!inferScale) { + return imageIcons.get(i.getId()); + } + ImageIcon baseIcon = imageIcons.get(i.getId()); + Dimension scaled = SystemUtilities.scaleWindowDimension(baseIcon.getIconWidth(), baseIcon.getIconHeight()); + return imageIcons.get(i.getId(scaled)); + } + + private ImageIcon getIcon(String id) { + return imageIcons.get(id); + } + + public ImageIcon getIcon(Icon i, Dimension size) { + return imageIcons.get(i.getId(size)); + } + + /** + * Returns the Image from cache + * + * @param i an IconCache.Icon + * @return the Image in the cache + */ + public BufferedImage getImage(Icon i) { + return images.get(i.getId()); + } + + public List getImages(Icon i) { + ArrayList icons = new ArrayList<>(); + for(String id : i.getIds()) { + icons.add(images.get(id)); + } + return icons; + } + + public BufferedImage getImage(Icon i, Dimension size) { + return images.get(i.getId(size)); + } + + /** + * Returns all IconCache.Icon's possible values + * + * @return the complete list of IconCache.Icon values + */ + public static Icon[] getTypes() { + return Icon.values(); + } + + /** + * Returns a buffered image from the specified imagePath. The image must + * reside in the RESOURCES_DIR declared above. Images are assumed to be + * bundled into the jar resource. + * + * @param imagePath The file name of the image to load + * @return The BufferedImage representing the data + */ + private static BufferedImage getImageResource(String imagePath) { + try { + InputStream is = IconCache.class.getResourceAsStream(imagePath.replaceAll("#", "")); + if (is != null) { + return ImageIO.read(is); + } else { + log.warn("Cannot find {}", imagePath); + } + } + catch(IOException e) { + log.error("Cannot find {}", imagePath, e); + } + return null; + } + + /** + * Overwrites the specified IconCache.Icon's underlying ImageIcon and BufferedImage with an opaque version + * + * @param i the IconCache.Icon + * @param bgColor the java Color used for the transparent pixels + */ + public void setBgColor(Icon i, Color bgColor) { + for (String id : i.getIds()) { + ImageIcon imageIcon = new ImageIcon(toOpaqueImage(getIcon(id), bgColor)); + images.put(id, toBufferedImage(imageIcon.getImage(), TRANSPARENT)); + imageIcons.put(id, imageIcon); + } + } + + /** + * Replaces the cached tray icons with corrected versions if necessary + * e.g. + * - Ubuntu transparency + * - macOS masked icons + * - macOS 10.14+ dark mode support + */ + public void fixTrayIcons(boolean darkTaskbar) { + // Handle mask-style tray icons + if (SystemUtilities.prefersMaskTrayIcon()) { + // Clone the mask icon + for (String id : Icon.MASK_ICON.getIds()) { + BufferedImage clone = clone(images.get(id)); + // Even on lite mode desktops, white tray icons were the norm until Windows 10 update 1903, (1903 is build 18362.X) + if (SystemUtilities.isWindows() && SystemUtilities.getOsVersion().lessThan(Version.valueOf("10.0.18362"))) { + darkTaskbar = true; + } + if (darkTaskbar) { + clone = ColorUtilities.invert(clone); + } + images.put(id.replaceAll("mask", "default"), clone); + imageIcons.put(id.replaceAll("mask", "default"), new ImageIcon(clone)); + } + } + + // Handle undocumented macOS tray icon padding + for(IconCache.Icon i : IconCache.getTypes()) { + // See also JXTrayIcon.getSize() + if (i.isTrayIcon() && SystemUtilities.isMac()) { + // Prevent padding from happening twice + if (!i.padded) { + padIcon(i, 25); + } + } + } + } + + public static BufferedImage clone(BufferedImage src) { + return clone(src, 1); + } + + public static BufferedImage clone(BufferedImage src, int scaleFactor) { + Image tmp = src.getScaledInstance(src.getWidth() * scaleFactor, src.getHeight() * scaleFactor, src.getType()); + BufferedImage dest = new BufferedImage(tmp.getWidth(null), tmp.getHeight(null), BufferedImage.TYPE_INT_ARGB); + Graphics g = dest.createGraphics(); + g.drawImage(tmp, 0, 0, null); + g.dispose(); + return dest; + } + + public void padIcon(Icon icon, int percent) { + for (String id : icon.getIds()) { + // Calculate padding percentage + int w = images.get(id).getWidth(); + int h = images.get(id).getHeight(); + int wPad = (int)((percent/100.0) * w); + int hPad = (int)((percent/100.0) * h); + + BufferedImage padded = new BufferedImage(w + wPad, h + hPad, BufferedImage.TYPE_INT_ARGB); + Graphics g = padded.getGraphics(); + + // Pad all sides (by half) + g.drawImage(images.get(id), wPad/2, hPad/2, null); + g.dispose(); + + images.put(id, padded); + imageIcons.put(id, new ImageIcon(padded)); + icon.padded = true; + } + } + + /** + * Creates an opaque icon image by setting transparent pixels to the specified bgColor + * + * @param icon The original transparency-enabled image + * @return The image overlaid on the appropriate background color + */ + public static BufferedImage toOpaqueImage(ImageIcon icon, Color bgColor) { + return toBufferedImage(icon.getImage(), bgColor); + } + + /** + * Converts a given Image into a BufferedImage + * + * @param img The Image to be converted + * @return The converted BufferedImage + */ + public static BufferedImage toBufferedImage(Image img, Color bgColor) { + if (img instanceof BufferedImage && bgColor == TRANSPARENT) { + return (BufferedImage)img; + } + + // Create a buffered image with transparency + BufferedImage bi = new BufferedImage(img.getWidth(null), img.getHeight(null), BufferedImage.TYPE_INT_ARGB); + + // Draw the image on to the buffered image + Graphics2D bGr = bi.createGraphics(); + bGr.drawImage(img, 0, 0, bgColor, null); + bGr.dispose(); + + // Return the buffered image + return bi; + } +} diff --git a/old code/tray/src/qz/ui/component/LinkLabel.java b/old code/tray/src/qz/ui/component/LinkLabel.java new file mode 100755 index 0000000..67d3c6c --- /dev/null +++ b/old code/tray/src/qz/ui/component/LinkLabel.java @@ -0,0 +1,99 @@ +package qz.ui.component; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.ui.Themeable; +import qz.utils.ShellUtilities; + +import javax.accessibility.AccessibleContext; +import javax.accessibility.AccessibleRole; +import javax.swing.*; +import java.awt.*; +import java.awt.font.TextAttribute; +import java.io.File; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +/** + * Creates a JButton which visually appears as a clickable link + * + * TODO: Rename this class. Since switching from JLabel to a JButton, this class now has a misleading name. + * + * Created by Tres on 2/19/2015. + */ +public class LinkLabel extends JButton implements Themeable { + + private static final Logger log = LogManager.getLogger(LinkLabel.class); + + public LinkLabel() { + super(); + initialize(); + } + + public LinkLabel(String text) { + super(text); + initialize(); + } + + public LinkLabel(String text, float multiplier, boolean underline) { + super(text); + EmLabel.stylizeComponent(this, multiplier, underline); + initialize(); + } + + public void setLinkLocation(final String url) { + try { + setLinkLocation(new URL(url)); + } + catch(MalformedURLException mue) { + log.error("", mue); + } + } + + public void setLinkLocation(final URL location) { + addActionListener(ae -> { + try { + Desktop.getDesktop().browse(location.toURI()); + } + catch(Exception e) { + log.error("", e); + } + }); + } + + public void setLinkLocation(final File filePath) { + addActionListener(ae -> ShellUtilities.browseDirectory(filePath.isDirectory()? filePath.getPath():filePath.getParent())); + } + + private void initialize() { + Map attributes = new HashMap<>(getFont().getAttributes()); + attributes.put(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON); + setFont(getFont().deriveFont(attributes)); + refresh(); + } + + @Override + public void refresh() { + setForeground(Constants.TRUSTED_COLOR); + setBorderPainted(false); + setBorder(null); + setOpaque(false); + setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)); + } + + public AccessibleContext getAccessibleContext() { + if (accessibleContext == null) { + accessibleContext = new AccessibleLinkLabel(); + } + return accessibleContext; + } + + protected class AccessibleLinkLabel extends AccessibleJButton { + public AccessibleRole getAccessibleRole() { + return AccessibleRole.HYPERLINK; + } + } +} diff --git a/old code/tray/src/qz/ui/component/RequestTable.java b/old code/tray/src/qz/ui/component/RequestTable.java new file mode 100755 index 0000000..f315b80 --- /dev/null +++ b/old code/tray/src/qz/ui/component/RequestTable.java @@ -0,0 +1,152 @@ +package qz.ui.component; + +import org.codehaus.jettison.json.JSONException; +import qz.auth.RequestState; +import qz.ui.Themeable; + +import javax.swing.*; +import java.awt.*; + +public class RequestTable extends DisplayTable implements Themeable { + + enum RequestField { + CALL("Call", "call"), + PARAMS("Parameters", "params"), + SIGNATURE("Signature", "signature"), + TIMESTAMP("Timestamp", "timestamp"), + VALIDITY("Validity", null); + + String description; + String fieldName; + + RequestField(String description, String fieldName) { + this.description = description; + this.fieldName = fieldName; + } + + public String getValue(RequestState request) { + if (request != null && !request.getRequestData().isNull(fieldName)) { + try { return request.getRequestData().getString(fieldName); } + catch(JSONException ignore) {} + } + + return ""; + } + + @Override + public String toString() { + return description; + } + + public static int size() { + return values().length; + } + } + + private RequestState request; + + public RequestTable(IconCache iconCache) { + super(iconCache); + setDefaultRenderer(Object.class, new RequestTableCellRenderer()); + } + + public void setRequest(RequestState request) { + this.request = request; + } + + @Override + public void refreshComponents() { + if (request == null) { + return; + } + + removeRows(); + + for(RequestField field : RequestField.values()) { + if (field == RequestField.VALIDITY) { + model.addRow(new Object[] {field, request.getStatus().getFormatted()}); + } else { + model.addRow(new Object[] {field, field.getValue(request)}); + } + } + + repaint(); + } + + @Override + public void refresh() { + refreshComponents(); + ((StyledTableCellRenderer)getDefaultRenderer(Object.class)).refresh(); + } + + public void autoSize() { + super.autoSize(RequestField.size(), 2); + } + + + private class RequestTableCellRenderer extends StyledTableCellRenderer { + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { + JLabel label = (JLabel)super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col); + + // First Column + if (value instanceof RequestField) { + label = stylizeLabel(STATUS_NORMAL, label, isSelected); + if (iconCache != null) { + label.setIcon(iconCache.getIcon(IconCache.Icon.FIELD_ICON)); + } + return label; + } + + // Second Column + if (request == null || col < 1) { return stylizeLabel(STATUS_NORMAL, label, isSelected); } + + RequestField field = (RequestField)table.getValueAt(row, col - 1); + if (field == null) { return stylizeLabel(STATUS_NORMAL, label, isSelected); } + + int style = STATUS_NORMAL; + switch(field) { + case CALL: + if (label.getText().isEmpty() && request.isInitialConnect()) { + //only time call can be empty is when setting up the connection + label.setText("connect"); + } + break; + case PARAMS: + if (label.getText().isEmpty()) { + label.setText("{}"); + } + break; + case SIGNATURE: + if (request.isVerified()) { + style = STATUS_TRUSTED; + } else if (request.getStatus() != RequestState.Validity.EXPIRED) { + style = STATUS_WARNING; + } + + if (label.getText().isEmpty()) { + if (request.isInitialConnect()) { + label.setText("Not Required"); + style = STATUS_NORMAL; + } else { + label.setText("Missing"); + } + } + break; + case TIMESTAMP: + if (request.getStatus() == RequestState.Validity.EXPIRED) { + style = STATUS_WARNING; + } + break; + case VALIDITY: + style = request.isVerified()? STATUS_TRUSTED:STATUS_WARNING; + break; + } + + return stylizeLabel(style, label, isSelected); + } + + } + +} diff --git a/old code/tray/src/qz/ui/component/StyledTableCellRenderer.java b/old code/tray/src/qz/ui/component/StyledTableCellRenderer.java new file mode 100755 index 0000000..72890a0 --- /dev/null +++ b/old code/tray/src/qz/ui/component/StyledTableCellRenderer.java @@ -0,0 +1,66 @@ +package qz.ui.component; + +import qz.common.Constants; + +import javax.swing.*; +import javax.swing.table.DefaultTableCellRenderer; +import java.awt.*; + +public class StyledTableCellRenderer extends DefaultTableCellRenderer { + + protected Color defaultForeground; + protected Color defaultSelectedForeground; + + final int STATUS_NORMAL = 0; + final int STATUS_WARNING = 1; + final int STATUS_TRUSTED = 2; + + public StyledTableCellRenderer() { + refresh(); + } + + public void refresh() { + defaultForeground = UIManager.getDefaults().getColor("Table.foreground"); + defaultSelectedForeground = UIManager.getDefaults().getColor("Table.selectionForeground"); + } + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { + return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col); + } + + protected JLabel stylizeLabel(int statusCode, JLabel label, boolean isSelected) { + return stylizeLabel(statusCode, label, isSelected, null); + } + + protected JLabel stylizeLabel(int statusCode, JLabel label, boolean isSelected, String reason) { + label.setIcon(null); + + int fontWeight; + Color foreground; + + switch(statusCode) { + case STATUS_WARNING: + foreground = Constants.WARNING_COLOR; + fontWeight = Font.BOLD; + break; + case STATUS_TRUSTED: + foreground = Constants.TRUSTED_COLOR; + fontWeight = Font.PLAIN; + break; + case STATUS_NORMAL: + default: + foreground = defaultForeground; + fontWeight = Font.PLAIN; + } + + label.setFont(label.getFont().deriveFont(fontWeight)); + label.setForeground(isSelected? defaultSelectedForeground:foreground); + if (statusCode == STATUS_WARNING && reason != null) { + label.setText(label.getText() + " (" + reason + ")"); + } + + return label; + } + +} diff --git a/old code/tray/src/qz/ui/resources/qz-about.png b/old code/tray/src/qz/ui/resources/qz-about.png new file mode 100755 index 0000000..b7ef824 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-about.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-allow.png b/old code/tray/src/qz/ui/resources/qz-allow.png new file mode 100755 index 0000000..88103e5 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-allow.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-banner.png b/old code/tray/src/qz/ui/resources/qz-banner.png new file mode 100755 index 0000000..f50e79d Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-banner.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-block.png b/old code/tray/src/qz/ui/resources/qz-block.png new file mode 100755 index 0000000..59230a7 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-block.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-cancel.png b/old code/tray/src/qz/ui/resources/qz-cancel.png new file mode 100755 index 0000000..ffb1267 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-cancel.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-danger-20.png b/old code/tray/src/qz/ui/resources/qz-danger-20.png new file mode 100755 index 0000000..0f3fc82 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-danger-20.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-danger-24.png b/old code/tray/src/qz/ui/resources/qz-danger-24.png new file mode 100755 index 0000000..cb9efd4 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-danger-24.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-danger-32.png b/old code/tray/src/qz/ui/resources/qz-danger-32.png new file mode 100755 index 0000000..609576b Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-danger-32.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-danger-40.png b/old code/tray/src/qz/ui/resources/qz-danger-40.png new file mode 100755 index 0000000..b35f5a8 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-danger-40.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-danger-48.png b/old code/tray/src/qz/ui/resources/qz-danger-48.png new file mode 100755 index 0000000..b82b932 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-danger-48.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-danger.png b/old code/tray/src/qz/ui/resources/qz-danger.png new file mode 100755 index 0000000..4dd01ad Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-danger.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-default-20.png b/old code/tray/src/qz/ui/resources/qz-default-20.png new file mode 100755 index 0000000..c96b9af Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-default-20.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-default-24.png b/old code/tray/src/qz/ui/resources/qz-default-24.png new file mode 100755 index 0000000..4ee9c4b Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-default-24.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-default-32.png b/old code/tray/src/qz/ui/resources/qz-default-32.png new file mode 100755 index 0000000..9760d19 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-default-32.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-default-40.png b/old code/tray/src/qz/ui/resources/qz-default-40.png new file mode 100755 index 0000000..5aa6fb5 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-default-40.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-default-48.png b/old code/tray/src/qz/ui/resources/qz-default-48.png new file mode 100755 index 0000000..d8a97e6 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-default-48.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-default.png b/old code/tray/src/qz/ui/resources/qz-default.png new file mode 100755 index 0000000..2d455e9 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-default.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-delete.png b/old code/tray/src/qz/ui/resources/qz-delete.png new file mode 100755 index 0000000..54d8a8a Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-delete.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-desktop.png b/old code/tray/src/qz/ui/resources/qz-desktop.png new file mode 100755 index 0000000..d78e4fa Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-desktop.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-exit.png b/old code/tray/src/qz/ui/resources/qz-exit.png new file mode 100755 index 0000000..1c82d36 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-exit.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-field.png b/old code/tray/src/qz/ui/resources/qz-field.png new file mode 100755 index 0000000..35ca771 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-field.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-folder.png b/old code/tray/src/qz/ui/resources/qz-folder.png new file mode 100755 index 0000000..e4e8b00 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-folder.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-log.png b/old code/tray/src/qz/ui/resources/qz-log.png new file mode 100755 index 0000000..25b6594 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-log.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-logo.png b/old code/tray/src/qz/ui/resources/qz-logo.png new file mode 100755 index 0000000..be5c013 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-logo.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-mask-20.png b/old code/tray/src/qz/ui/resources/qz-mask-20.png new file mode 100755 index 0000000..565c4d1 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-mask-20.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-mask-24.png b/old code/tray/src/qz/ui/resources/qz-mask-24.png new file mode 100755 index 0000000..89579d7 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-mask-24.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-mask-32.png b/old code/tray/src/qz/ui/resources/qz-mask-32.png new file mode 100755 index 0000000..ca89e0b Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-mask-32.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-mask-40.png b/old code/tray/src/qz/ui/resources/qz-mask-40.png new file mode 100755 index 0000000..c514cb0 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-mask-40.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-mask-48.png b/old code/tray/src/qz/ui/resources/qz-mask-48.png new file mode 100755 index 0000000..3739e2a Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-mask-48.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-mask.png b/old code/tray/src/qz/ui/resources/qz-mask.png new file mode 100755 index 0000000..6793249 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-mask.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-question.png b/old code/tray/src/qz/ui/resources/qz-question.png new file mode 100755 index 0000000..09b9a2e Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-question.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-reload.png b/old code/tray/src/qz/ui/resources/qz-reload.png new file mode 100755 index 0000000..b5ac7da Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-reload.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-saved.png b/old code/tray/src/qz/ui/resources/qz-saved.png new file mode 100755 index 0000000..cb6b7a9 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-saved.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-settings.png b/old code/tray/src/qz/ui/resources/qz-settings.png new file mode 100755 index 0000000..6f984dc Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-settings.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-trust-issue.png b/old code/tray/src/qz/ui/resources/qz-trust-issue.png new file mode 100755 index 0000000..b9e33e5 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-trust-issue.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-trust-missing.png b/old code/tray/src/qz/ui/resources/qz-trust-missing.png new file mode 100755 index 0000000..6fd6c52 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-trust-missing.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-trust-sponsored.png b/old code/tray/src/qz/ui/resources/qz-trust-sponsored.png new file mode 100755 index 0000000..7ed9fb9 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-trust-sponsored.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-trust-verified.png b/old code/tray/src/qz/ui/resources/qz-trust-verified.png new file mode 100755 index 0000000..79f14c4 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-trust-verified.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-warning-20.png b/old code/tray/src/qz/ui/resources/qz-warning-20.png new file mode 100755 index 0000000..f784cc1 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-warning-20.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-warning-24.png b/old code/tray/src/qz/ui/resources/qz-warning-24.png new file mode 100755 index 0000000..bef1f77 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-warning-24.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-warning-32.png b/old code/tray/src/qz/ui/resources/qz-warning-32.png new file mode 100755 index 0000000..04790a1 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-warning-32.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-warning-40.png b/old code/tray/src/qz/ui/resources/qz-warning-40.png new file mode 100755 index 0000000..36f29df Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-warning-40.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-warning-48.png b/old code/tray/src/qz/ui/resources/qz-warning-48.png new file mode 100755 index 0000000..17d1ff1 Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-warning-48.png differ diff --git a/old code/tray/src/qz/ui/resources/qz-warning.png b/old code/tray/src/qz/ui/resources/qz-warning.png new file mode 100755 index 0000000..828dcce Binary files /dev/null and b/old code/tray/src/qz/ui/resources/qz-warning.png differ diff --git a/old code/tray/src/qz/ui/tray/AWTMenuWrapper.java b/old code/tray/src/qz/ui/tray/AWTMenuWrapper.java new file mode 100755 index 0000000..9ad32c7 --- /dev/null +++ b/old code/tray/src/qz/ui/tray/AWTMenuWrapper.java @@ -0,0 +1,107 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.ui.tray; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +/** + * Wraps a Swing JMenuItem or JSeparator into an AWT item including text, shortcuts and action listeners. + * + * @author Tres Finocchiaro + */ +public class AWTMenuWrapper { + private MenuItem item; + + private AWTMenuWrapper(JCheckBoxMenuItem item) { + this.item = new CheckboxMenuItem(item.getText()); + wrapState(item); + wrapEnabled(item); + wrapShortcut(item); + wrapItemListeners(item); + } + + private AWTMenuWrapper(JMenuItem item) { + this.item = new MenuItem(item.getText()); + wrapEnabled(item); + wrapShortcut(item); + wrapActionListeners(item); + } + + private AWTMenuWrapper(JMenu menu) { + this.item = new Menu(menu.getText()); + wrapEnabled(menu); + wrapShortcut(menu); + } + + @SuppressWarnings("unused") + private AWTMenuWrapper(JSeparator ignore) { + this.item = new MenuItem("-"); + } + + private MenuItem getMenuItem() { + return item; + } + + private void wrapShortcut(JMenuItem item) { + this.item.setShortcut(new MenuShortcut(item.getMnemonic(), false)); + } + + private void wrapActionListeners(JMenuItem item) { + for (ActionListener l : item.getActionListeners()) { + this.item.addActionListener(l); + } + } + + // Special case for CheckboxMenuItem + private void wrapItemListeners(final JMenuItem item) { + for (final ActionListener l : item.getActionListeners()) { + ((CheckboxMenuItem)this.item).addItemListener(e -> { + ((JCheckBoxMenuItem)item).setState(((CheckboxMenuItem)AWTMenuWrapper.this.item).getState()); + l.actionPerformed(new ActionEvent(item, e.getID(), item.getActionCommand())); + }); + } + } + + private void wrapState(JMenuItem item) { + if (this.item instanceof CheckboxMenuItem && item instanceof JCheckBoxMenuItem) { + ((CheckboxMenuItem)this.item).setState(((JCheckBoxMenuItem)item).getState()); + item.addChangeListener(e -> { + ((CheckboxMenuItem)this.item).setState(((JCheckBoxMenuItem)item).getState()); + }); + } + } + + private void wrapEnabled(JMenuItem item) { + // Match initial state + this.item.setEnabled(item.isEnabled()); + // Monitor future state + item.addPropertyChangeListener("enabled", evt -> { + this.item.setEnabled((Boolean)evt.getNewValue()); + }); + } + + public static MenuItem wrap(Component c) { + if (c instanceof JCheckBoxMenuItem) { + return new AWTMenuWrapper((JCheckBoxMenuItem)c).getMenuItem(); + } else if (c instanceof JMenu) { + return new AWTMenuWrapper((JMenu)c).getMenuItem(); + } else if (c instanceof JSeparator) { + return new AWTMenuWrapper((JSeparator)c).getMenuItem(); + } else if (c instanceof JMenuItem) { + return new AWTMenuWrapper((JMenuItem)c).getMenuItem(); + } else { + return new MenuItem("Error"); + } + } +} \ No newline at end of file diff --git a/old code/tray/src/qz/ui/tray/ClassicTrayIcon.java b/old code/tray/src/qz/ui/tray/ClassicTrayIcon.java new file mode 100755 index 0000000..7690f46 --- /dev/null +++ b/old code/tray/src/qz/ui/tray/ClassicTrayIcon.java @@ -0,0 +1,50 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.ui.tray; + +import org.jdesktop.swinghelper.tray.JXTrayIcon; + +import javax.swing.*; +import java.awt.*; + +/** + * Wraps a Swing JPopupMenu into an AWT PopupMenu + * + * @author Tres Finocchiaro + */ +public class ClassicTrayIcon extends JXTrayIcon { + public ClassicTrayIcon(Image image) { + super(image); + } + + @Override + public void setJPopupMenu(JPopupMenu source) { + final PopupMenu popup = new PopupMenu(); + setPopupMenu(popup); + wrapAll(popup, source.getComponents()); + popup.addNotify(); + } + + /** + * Convert an array of Swing menu components to its AWT equivalent + * @param menu PopupMenu to receive new components + * @param components Array of components to recurse over + */ + private static void wrapAll(Menu menu, Component[] components) { + for (Component c : components) { + MenuItem item = AWTMenuWrapper.wrap(c); + menu.add(item); + if (item instanceof Menu) { + wrapAll((Menu)item, ((JMenu)c).getMenuComponents()); + } + } + } +} \ No newline at end of file diff --git a/old code/tray/src/qz/ui/tray/ModernTrayIcon.java b/old code/tray/src/qz/ui/tray/ModernTrayIcon.java new file mode 100755 index 0000000..7023fa5 --- /dev/null +++ b/old code/tray/src/qz/ui/tray/ModernTrayIcon.java @@ -0,0 +1,122 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + * + */ + +package qz.ui.tray; + +import org.jdesktop.swinghelper.tray.JXTrayIcon; + +import javax.swing.*; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; +import java.awt.*; +import java.awt.event.AWTEventListener; +import java.awt.event.MouseEvent; +import java.awt.event.WindowEvent; +import java.awt.event.WindowListener; + +/** + * @author A. Tres Finocchiaro + */ +public class ModernTrayIcon extends JXTrayIcon { + private static Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize(); + private JFrame invisibleFrame; + private JPopupMenu popup; + private int x = 0, y = 0; + + public ModernTrayIcon(Image image) { + super(image); + } + + @Override + public void setJPopupMenu(final JPopupMenu popup) { + this.popup = popup; + + invisibleFrame = new JFrame(); + invisibleFrame.setAlwaysOnTop(true); + invisibleFrame.setUndecorated(true); + invisibleFrame.setBackground(Color.BLACK); + invisibleFrame.setSize(0, 0); + invisibleFrame.pack(); + + popup.addPopupMenuListener(new PopupMenuListener() { + @Override public void popupMenuWillBecomeVisible(PopupMenuEvent e) {} + @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { invisibleFrame.setVisible(false); } + @Override public void popupMenuCanceled(PopupMenuEvent e) { invisibleFrame.setVisible(false); } + }); + + invisibleFrame.addWindowListener(new WindowListener() { + @Override + public void windowActivated(WindowEvent we) { + popup.setInvoker(invisibleFrame); + popup.setVisible(true); + popup.setLocation(x, y > screenSize.getHeight() / 2 ? y - popup.getHeight() : y); + popup.requestFocus(); + } + + @Override public void windowOpened(WindowEvent we) {} + @Override public void windowClosing(WindowEvent we) {} + @Override public void windowClosed(WindowEvent we) {} + @Override public void windowIconified(WindowEvent we) {} + @Override public void windowDeiconified(WindowEvent we) {} + @Override public void windowDeactivated(WindowEvent we) {} + }); + + addTrayListener(); + } + + @Override + public void setImage(Image image) { + super.setImage(image); + if (invisibleFrame != null) { + invisibleFrame.setIconImage(image); + } + } + + public JPopupMenu getJPopupMenu() { + return popup; + } + + /** + * Functional equivalent of a MouseAdapter, but accommodates an edge-case in Gnome3 where the tray + * icon cannot listen on mouse events. + */ + private void addTrayListener() { + Toolkit.getDefaultToolkit().addAWTEventListener(new AWTEventListener() { + @Override + public void eventDispatched(AWTEvent e) { + Point p = isTrayEvent(e); + if (p != null) { + x = p.x; y = p.y; + invisibleFrame.setVisible(true); + invisibleFrame.requestFocus(); + } + } + }, MouseEvent.MOUSE_EVENT_MASK); + } + + /** + * Determines if TrayIcon event is detected + * @param e An AWTEvent + * @return A Point on the screen which the tray event occurred, or null if none is found + */ + private static Point isTrayEvent(AWTEvent e) { + if (e instanceof MouseEvent) { + MouseEvent me = (MouseEvent)e; + + if (me.getID() == MouseEvent.MOUSE_RELEASED && me.getSource() != null) { + if (me.getSource().getClass().getName().contains("TrayIcon")) { + return me.getLocationOnScreen(); + } + } + } + return null; + } +} \ No newline at end of file diff --git a/old code/tray/src/qz/ui/tray/TaskbarTrayIcon.java b/old code/tray/src/qz/ui/tray/TaskbarTrayIcon.java new file mode 100755 index 0000000..8dec35e --- /dev/null +++ b/old code/tray/src/qz/ui/tray/TaskbarTrayIcon.java @@ -0,0 +1,149 @@ +package qz.ui.tray; + +import qz.common.Constants; + +import javax.swing.*; +import javax.swing.event.PopupMenuEvent; +import javax.swing.event.PopupMenuListener; +import java.awt.*; +import java.awt.event.*; +import java.lang.reflect.Field; + +public class TaskbarTrayIcon extends JFrame implements WindowListener { + + private Dimension iconSize; + private JPopupMenu popup; + + public TaskbarTrayIcon(Image trayImage, final ActionListener exitListener) { + super(Constants.ABOUT_TITLE); + initializeComponents(trayImage, exitListener); + } + + private void initializeComponents(Image trayImage, final ActionListener exitListener) { + // must come first + setUndecorated(true); + setTaskBarTitle(getTitle()); + setSize(0, 0); + getContentPane().setBackground(Color.BLACK); + iconSize = new Dimension(40, 40); + + setIconImage(trayImage); + setResizable(false); + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + exitListener.actionPerformed(new ActionEvent(e.getComponent(), e.getID(), "Exit")); + } + }); + addWindowListener(this); + } + + // fixes Linux taskbar title per http://hg.netbeans.org/core-main/rev/5832261b8434, JDK-6528430 + public static void setTaskBarTitle(String title) { + try { + Class toolkit = Toolkit.getDefaultToolkit().getClass(); + if ("sun.awt.X11.XToolkit".equals(toolkit.getName())) { + final Field awtAppClassName = toolkit.getDeclaredField("awtAppClassName"); + awtAppClassName.setAccessible(true); + awtAppClassName.set(null, title); + } + } + catch(Exception ignore) {} + } + + /** + * Returns the "tray" icon size (not the dialog size) + */ + @Override + public Dimension getSize() { + return iconSize; + } + + public void setJPopupMenu(final JPopupMenu popup) { + this.popup = popup; + this.popup.addPopupMenuListener(new PopupMenuListener() { + @Override + public void popupMenuWillBecomeVisible(PopupMenuEvent popupMenuEvent) {} + + @Override + public void popupMenuWillBecomeInvisible(PopupMenuEvent popupMenuEvent) { + setState(JFrame.ICONIFIED); + } + + @Override + public void popupMenuCanceled(PopupMenuEvent popupMenuEvent) { + setState(JFrame.ICONIFIED); + } + }); + this.popup.addKeyListener(new KeyListener() { + @Override + public void keyTyped(KeyEvent keyEvent) {} + + @Override + public void keyPressed(KeyEvent keyEvent) { + if (keyEvent.getKeyCode() == KeyEvent.VK_ESCAPE) { + TaskbarTrayIcon.this.popup.setVisible(false); + } + } + + @Override + public void keyReleased(KeyEvent keyEvent) {} + }); + } + + public void displayMessage(String caption, String text, TrayIcon.MessageType level) { + int messageType; + switch(level) { + case WARNING: + messageType = JOptionPane.WARNING_MESSAGE; + break; + case ERROR: + messageType = JOptionPane.ERROR_MESSAGE; + break; + case INFO: + messageType = JOptionPane.INFORMATION_MESSAGE; + break; + case NONE: + default: + messageType = JOptionPane.PLAIN_MESSAGE; + } + JOptionPane.showMessageDialog(null, text, caption, messageType); + } + + @Override + public void windowDeiconified(WindowEvent e) { + Point p = MouseInfo.getPointerInfo().getLocation(); + setLocation(p); + // call show against parent to prevent un-clickable state + popup.show(this, 0, 0); + + // move to mouse cursor; adjusting for screen boundaries + Point before = popup.getLocationOnScreen(); + Point after = new Point(); + after.setLocation(before.x < p.x? p.x - popup.getWidth():p.x, before.y < p.y? p.y - popup.getHeight():p.y); + popup.setLocation(after); + } + + @Override + public void windowOpened(WindowEvent windowEvent) {} + + @Override + public void windowClosing(WindowEvent windowEvent) {} + + @Override + public void windowClosed(WindowEvent windowEvent) {} + + @Override + public void windowIconified(WindowEvent windowEvent) {} + + @Override + public void windowActivated(WindowEvent windowEvent) {} + + @Override + public void windowDeactivated(WindowEvent windowEvent) { + if (popup != null) { + popup.setVisible(false); + setState(JFrame.ICONIFIED); + } + } + +} diff --git a/old code/tray/src/qz/ui/tray/TrayType.java b/old code/tray/src/qz/ui/tray/TrayType.java new file mode 100755 index 0000000..3167e9c --- /dev/null +++ b/old code/tray/src/qz/ui/tray/TrayType.java @@ -0,0 +1,107 @@ +package qz.ui.tray; + +import org.jdesktop.swinghelper.tray.JXTrayIcon; +import qz.ui.component.IconCache; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionListener; + +/** + * Wrapper class to allow popup menu on a tray-less OS + * @author Tres Finocchiaro + */ +public enum TrayType { + JX, + CLASSIC, + MODERN, + TASKBAR; + + private JXTrayIcon tray = null; + private TaskbarTrayIcon taskbar = null; + private IconCache iconCache; + + public JXTrayIcon tray() { return tray; } + + public TrayType init(IconCache iconCache) { + return init(null, iconCache); + } + + public TrayType init(ActionListener exitListener, IconCache iconCache) { + switch (this) { + case JX: + tray = new JXTrayIcon(blankImage()); break; + case CLASSIC: + tray = new ClassicTrayIcon(blankImage()); break; + case MODERN: + tray = new ModernTrayIcon(blankImage()); break; + default: + taskbar = new TaskbarTrayIcon(blankImage(), exitListener); + } + this.iconCache = iconCache; + return this; + } + + private static Image blankImage() { + return new ImageIcon(new byte[1]).getImage(); + } + + public boolean isTray() { return tray != null; } + + public boolean getTaskbar() { return taskbar != null; } + + public void setIcon(IconCache.Icon icon) { + if (isTray()) { + tray.setImage(iconCache.getImage(icon, tray.getSize())); + } else { + taskbar.setIconImages(iconCache.getImages(icon)); + } + } + + public void setImage(Image image) { + if (isTray()) { + tray.setImage(image); + } else { + taskbar.setIconImage(image); + } + } + + public void setImageAutoSize(boolean autoSize) { + if (isTray()) { + tray.setImageAutoSize(autoSize); + } + } + + public Dimension getSize() { + return isTray() ? tray.getSize() : taskbar.getSize(); + } + + public void setToolTip(String tooltip) { + if (isTray()) { + tray.setToolTip(tooltip); + } + } + + public void setJPopupMenu(JPopupMenu popup) { + if (isTray()) { + tray.setJPopupMenu(popup); + } else { + taskbar.setJPopupMenu(popup); + } + } + + public void displayMessage(String caption, String text, TrayIcon.MessageType level) { + if (isTray()) { + tray.displayMessage(caption, text, level); + } else { + taskbar.displayMessage(caption, text, level); + } + } + + public void showTaskbar() { + if (getTaskbar()) { + taskbar.setVisible(true); + taskbar.setState(Frame.ICONIFIED); + } + } +} diff --git a/old code/tray/src/qz/utils/ArabicConversionUtilities.java b/old code/tray/src/qz/utils/ArabicConversionUtilities.java new file mode 100755 index 0000000..2b170ef --- /dev/null +++ b/old code/tray/src/qz/utils/ArabicConversionUtilities.java @@ -0,0 +1,89 @@ +package qz.utils; + +import com.ibm.icu.charset.CharsetEncoderICU; +import com.ibm.icu.charset.CharsetProviderICU; +import com.ibm.icu.text.ArabicShaping; +import com.ibm.icu.text.ArabicShapingException; +import com.ibm.icu.text.Bidi; + +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.Charset; +import java.nio.charset.CoderResult; +import java.nio.charset.StandardCharsets; + +/** + * Created by Yohanes Nugroho on 7/10/2018. + */ +public class ArabicConversionUtilities { + + /** + * This is the simplest and most reliable method: + * If all characters on input string does not contain any Arabic letters then return it as it is, + * otherwise do special Arabic text conversion + *

+ * To send data to printer, we need to split the commands from the text, eg:
+ * {@code var data = ['\x1b\x41\x42', "Arabic text to print", '\x1b\x42x53', "Other texts"]} + * + * @param escp_or_text a String that contains only ESC/P code or only text + * @return encoded bytes + */ + public static byte[] convertToIBM864(String escp_or_text) throws CharacterCodingException, ArabicShapingException { + boolean allAscii = true; + for(int i = 0; i < escp_or_text.length(); i++) { + //https://wiki.sei.cmu.edu/confluence/display/java/STR01-J.+Do+not+assume+that+a+Java+char+fully+represents+a+Unicode+code+point + int ch = escp_or_text.codePointAt(i); + if (ch > 255) { + allAscii = false; + } + } + + if (allAscii) { + //we use 'ISO-8859-1' that will map bytes as it is + return escp_or_text.getBytes(StandardCharsets.ISO_8859_1); + } else { + //Layout the characters from logical order to visual ordering + Bidi para = new Bidi(); + para.setPara(escp_or_text, Bidi.LEVEL_DEFAULT_LTR, null); + String data = para.writeReordered(Bidi.DO_MIRRORING); + return convertVisualOrderedToIBM864(data); + } + } + + /** + * Shape a visual ordered Arabic string and then encode it in IBM864 encoding + * + * @param str input string + * @return encoded bytes + */ + private static byte[] convertVisualOrderedToIBM864(String str) throws ArabicShapingException, CharacterCodingException { + //We shape the characters to map it to Unicode in FExx range + //Note that the output of Bidi is VISUAL_LTR, so we need the flag: ArabicShaping.TEXT_DIRECTION_VISUAL_LTR) + ArabicShaping as = new ArabicShaping(ArabicShaping.LETTERS_SHAPE | ArabicShaping.TEXT_DIRECTION_VISUAL_LTR | ArabicShaping.LENGTH_GROW_SHRINK); + String shaped = as.shape(str); + + //then we need to convert it to IBM864 using ICU Encoder + CharsetProviderICU icu = new CharsetProviderICU(); + Charset cs = icu.charsetForName("IBM864"); + CharsetEncoderICU icuc = (CharsetEncoderICU)cs.newEncoder(); + + //We need to use fallback for some character forms that can not be found + icuc.setFallbackUsed(true); + ByteBuffer output = ByteBuffer.allocate(shaped.length() * 2); + CharBuffer inp = CharBuffer.wrap(shaped); + CoderResult res = icuc.encode(inp, output, true); + if (res.isError()) { + res.throwException(); + } + + int length = output.position(); + byte all[] = output.array(); + + byte out[] = new byte[length]; + System.arraycopy(all, 0, out, 0, length); + + return out; + } + +} diff --git a/old code/tray/src/qz/utils/ArgParser.java b/old code/tray/src/qz/utils/ArgParser.java new file mode 100755 index 0000000..dae8854 --- /dev/null +++ b/old code/tray/src/qz/utils/ArgParser.java @@ -0,0 +1,495 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2019 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.utils; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.build.JLink; +import qz.common.Constants; +import qz.common.SecurityInfo; +import qz.exception.MissingArgException; +import qz.installer.Installer; +import qz.installer.TaskKiller; +import qz.installer.certificate.CertificateManager; +import qz.build.provision.ProvisionBuilder; + +import java.io.File; +import java.lang.reflect.Field; +import java.util.*; +import java.util.List; + +import static qz.common.Constants.*; +import static qz.utils.ArgParser.ExitStatus.*; +import static qz.utils.ArgValue.*; +import static qz.utils.ArgValue.ArgValueOption.*; + +public class ArgParser { + public enum ExitStatus { + SUCCESS(0), + GENERAL_ERROR(1), + USAGE_ERROR(2), + NO_AUTOSTART(0); + private int code; + ExitStatus(int code) { + this.code = code; + } + public int getCode() { + return code; + } + } + + protected static final Logger log = LogManager.getLogger(ArgParser.class); + + private static final String USAGE_COMMAND = String.format("java -jar %s.jar", PROPS_FILE); + private static final String USAGE_COMMAND_PARAMETER = String.format("java -Dfoo.bar= -jar %s.jar", PROPS_FILE); + private static final int DESCRIPTION_COLUMN = 35; + private static final int INDENT_SIZE = 2; + + private List args; + private boolean headless; + private ExitStatus exitStatus; + + public ArgParser(String[] args) { + this.exitStatus = SUCCESS; + this.args = new ArrayList<>(Arrays.asList(args)); + + // Apple grossly allows adding weird flags + // This can be removed when it's removed from unix-launcher.sh.in + if(this.args.size() > 2 && this.args.get(0).startsWith("-NS")) { + this.args.remove(0); + this.args.remove(0); + } + } + public List getArgs() { + return args; + } + + public int getExitCode() { + return exitStatus.getCode(); + } + + public boolean isHeadless() { return headless; }; + + /** + * Gets the requested flag status + */ + private boolean hasFlag(String ... matches) { + for(String match : matches) { + for(String arg : args) { + if(match.equals(arg)) { + return true; + } + } + } + return false; + } + + public boolean hasFlag(ArgValue argValue) { + return hasFlag(argValue.getMatches()); + } + + public ArgValue hasFlags(boolean skipHelp, ArgValue ... argValues) { + for(ArgValue argValue : argValues) { + if(skipHelp && argValue == HELP) { + continue; + } + if(hasFlag(argValue)) { + return argValue; + } + } + return null; + } + + public boolean hasFlag(ArgValueOption argValueOption) { + return hasFlag(argValueOption.getMatches()); + } + + /** + * Allows a pattern such as "--arg%d" to look for "--arg1", "--arg2" in succession + */ + private String[] valuesOpt(String pattern) throws MissingArgException { + List all = new LinkedList<>(); + int argCounter = 0; + while(true) { + String found = valueOpt(String.format(pattern, ++argCounter)); + if(found == null) { + break; + } + all.add(found); + } + return all.toArray(new String[all.size()]); + } + + private String valueOf(String ... matches) throws MissingArgException { + return valueOf(false, matches); + } + + /** + * Convenience for valueOf(false, ...); + */ + private String valueOpt(String ... matches) throws MissingArgException { + return valueOf(true, matches); + } + + /** + * Gets the argument value immediately following a command + * @throws MissingArgException + */ + private String valueOf(boolean optional, String ... matches) throws MissingArgException { + for(String match : matches) { + if (args.contains(match)) { + int index = args.indexOf(match) + 1; + if (args.size() >= index + 1) { + String val = args.get(index); + if(!val.trim().isEmpty()) { + return val; + } + } + // FIXME: This doesn't fire when one might expect it to, but fixing it may cause regressions + if(!optional) { + throw new MissingArgException(); + } + } + } + return null; + } + + public String valueOf(ArgValue argValue) throws MissingArgException { + return valueOf(argValue.getMatches()); + } + + public String valueOf(ArgValueOption argValueOption) throws MissingArgException { + return valueOf(argValueOption.getMatches()); + } + + public ExitStatus processInstallerArgs(ArgValue argValue, List args) { + try { + switch(argValue) { + case PREINSTALL: + return Installer.preinstall() ? SUCCESS : SUCCESS; // don't abort on preinstall + case INSTALL: + // Handle destination + String dest = valueOf(DEST); + // Handle silent installs + boolean silent = hasFlag(SILENT); + Installer.install(dest, silent); // exception will set error + return SUCCESS; + case CERTGEN: + TaskKiller.killAll(); + + // Handle trusted SSL certificate + String trustedKey = valueOf(KEY); + String trustedCert = valueOf(CERT); + String trustedPfx = valueOf(PFX); + String trustedPass = valueOf(PASS); + if (trustedKey != null && trustedCert != null) { + File key = new File(trustedKey); + File cert = new File(trustedCert); + if(key.exists() && cert.exists()) { + new CertificateManager(key, cert); // exception will set error + return SUCCESS; + } + log.error("One or more trusted files was not found."); + throw new MissingArgException(); + } else if((trustedKey != null || trustedCert != null || trustedPfx != null) && trustedPass != null) { + String pfxPath = trustedPfx == null ? (trustedKey == null ? trustedCert : trustedKey) : trustedPfx; + File pfx = new File(pfxPath); + + if(pfx.exists()) { + new CertificateManager(pfx, trustedPass.toCharArray()); // exception will set error + return SUCCESS; + } + log.error("The provided pfx/pkcs12 file was not found: {}", pfxPath); + throw new MissingArgException(); + } else { + // Handle localhost override + String hosts = valueOf(HOST); + if (hosts != null) { + Installer.getInstance().certGen(true, hosts.split(";")); + return SUCCESS; + } + Installer.getInstance().certGen(true); + // Failure in this step is extremely rare, but + return SUCCESS; // exception will set error + } + case UNINSTALL: + Installer.uninstall(); + return SUCCESS; + case SPAWN: + args.remove(0); // first argument is "spawn", remove it + Installer.getInstance().spawn(args); + return SUCCESS; + default: + throw new UnsupportedOperationException("Installation type " + argValue + " is not yet supported"); + } + } catch(MissingArgException e) { + log.error("Valid usage:{} {} {}", System.lineSeparator(), USAGE_COMMAND, argValue.getUsage()); + return USAGE_ERROR; + } catch(Exception e) { + log.error("Installation step {} failed", argValue, e); + return GENERAL_ERROR; + } + } + + public ExitStatus processBuildArgs(ArgValue argValue) { + try { + switch(argValue) { + case JLINK: + new JLink( + valueOf("--platform", "-p"), + valueOf("--arch", "-a"), + valueOf("--vendor", "-e"), + valueOf("--version", "-v"), + valueOf("--gc", "-g"), + valueOf("--gcversion", "-c"), + valueOpt("--targetjdk", "-j") + ); + return SUCCESS; + case PROVISION: + ProvisionBuilder provisionBuilder; + + String jsonParam = valueOpt("--json"); + if(jsonParam != null) { + // Process JSON provision file (overwrites existing provisions) + provisionBuilder = new ProvisionBuilder(new File(jsonParam), valueOpt("--target-os"), valueOpt("--target-arch")); + provisionBuilder.saveJson(true); + } else { + // Process single provision step (preserves existing provisions) + provisionBuilder = new ProvisionBuilder( + valueOf("--type"), + valueOpt("--phase"), + valueOpt("--os"), + valueOpt("--arch"), + valueOf("--data"), + valueOpt("--args"), + valueOpt("--description"), + valuesOpt("--arg%d") + ); + provisionBuilder.saveJson(false); + } + log.info("Successfully added provisioning step(s) {} to file '{}'", provisionBuilder.getJson(), ProvisionBuilder.BUILD_PROVISION_FILE); + return SUCCESS; + default: + throw new UnsupportedOperationException("Build type " + argValue + " is not yet supported"); + } + } catch(MissingArgException e) { + log.error("Valid usage:{} {} {}", System.lineSeparator(), USAGE_COMMAND, argValue.getUsage()); + return USAGE_ERROR; + } catch(Exception e) { + log.error("Build step {} failed", argValue, e); + return GENERAL_ERROR; + } + } + + /** + * Attempts to intercept utility command line args. + * If intercepted, returns true and sets the exitStatus to a usable integer + */ + public boolean intercept() { + // First handle help request + if(hasFlag(HELP)) { + System.out.println(String.format("Usage: %s (command)", USAGE_COMMAND)); + + ArgValue command; + if((command = hasFlags(true, ArgValue.values())) != null) { + // Intercept command-specific help requests + printHelp(command); + + // Loop over command-specific documentation + ArgValueOption[] argValueOptions = ArgValueOption.filter(command); + if(argValueOptions.length > 0) { + System.out.println("OPTIONS"); + for(ArgValueOption argValueOption : argValueOptions) { + printHelp(argValueOption); + } + } else { + System.out.println(System.lineSeparator() + "No options available for this command."); + } + } else { + // Show generic help + for(ArgValue.ArgType argType : ArgValue.ArgType.values()) { + System.out.println(String.format("%s%s", System.lineSeparator(), argType)); + switch(argType) { + case PREFERENCES: + System.out.println(String.format(" Preferences can be set via \"%s %s=%s\", command line via \"%s\" or via file using %s.properties" + System.lineSeparator(), + SystemUtilities.isWindows() ? "set" : "export", + "QZ_OPTS", + "-Dfoo.bar=", + USAGE_COMMAND_PARAMETER, + PROPS_FILE)); + } + for(ArgValue argValue : ArgValue.filter(argType)) { + printHelp(argValue); + } + } + + System.out.println(String.format("%sFor help on a specific command:", System.lineSeparator())); + System.out.println(String.format("%sUsage: %s --help (command)", StringUtils.rightPad("", INDENT_SIZE), USAGE_COMMAND)); + commandLoop: + for(ArgValue argValue : ArgValue.values()) { + for(ArgValueOption ignore : ArgValueOption.filter(argValue)) { + System.out.println(String.format("%s--help %s", StringUtils.rightPad("", INDENT_SIZE * 2), argValue.getMatches()[0])); + continue commandLoop; + } + } + } + + exitStatus = USAGE_ERROR; + return true; + } + + // Second, handle build or install commands + ArgValue found = hasFlags(true, ArgValue.filter(ArgType.INSTALLER, ArgType.BUILD)); + if(found != null) { + switch(found.getType()) { + case BUILD: + // Handle build commands (e.g. jlink) + exitStatus = processBuildArgs(found); + return true; + case INSTALLER: + // Handle install commands (e.g. install, uninstall, certgen, etc) + exitStatus = processInstallerArgs(found, args); + return true; + } + } + + // Last, handle all other commands including normal startup + ArgValue argValue = null; + try { + // Handle graceful autostart disabling + if (hasFlag(AUTOSTART)) { + exitStatus = SUCCESS; + if(!FileUtilities.isAutostart()) { + exitStatus = NO_AUTOSTART; + return true; + } + // Don't intercept + exitStatus = SUCCESS; + return false; + } + + // Handle headless flag + if(headless = hasFlag("-h", "--headless")) { + // Don't intercept + exitStatus = SUCCESS; + return false; + } + + // Handle version request + if (hasFlag(ArgValue.VERSION)) { + System.out.println(Constants.VERSION); + exitStatus = SUCCESS; + return true; + } + // Handle macOS CFBundleIdentifier request + if (hasFlag(BUNDLEID)) { + System.out.println(MacUtilities.getBundleId()); + exitStatus = SUCCESS; + return true; + } + // Handle cert installation + String certFile; + if ((certFile = valueOf(argValue = ALLOW)) != null) { + exitStatus = FileUtilities.addToCertList(ALLOW_FILE, new File(certFile)); + return true; + } + if ((certFile = valueOf(argValue = BLOCK)) != null) { + exitStatus = FileUtilities.addToCertList(BLOCK_FILE, new File(certFile)); + return true; + } + + // Handle file.allow + String allowPath; + if ((allowPath = valueOf(argValue = FILE_ALLOW)) != null) { + exitStatus = FileUtilities.addFileAllowProperty(allowPath, valueOf(SANDBOX)); + return true; + } + if ((allowPath = valueOf(argValue = FILE_REMOVE)) != null) { + exitStatus = FileUtilities.removeFileAllowProperty(allowPath); + return true; + } + + // Print library list + if (hasFlag(LIBINFO)) { + SecurityInfo.printLibInfo(); + exitStatus = SUCCESS; + return true; + } + } catch(MissingArgException e) { + System.out.println("Usage:"); + if(argValue != null) { + printHelp(argValue); + } + log.error("Invalid usage was provided"); + exitStatus = USAGE_ERROR; + return true; + } catch(Exception e) { + log.error("Internal error occurred", e); + exitStatus = GENERAL_ERROR; + return true; + } + return false; + } + + private static ArrayList collectPrefs() { + ArrayList opts = new ArrayList<>(); + for(Field f : Constants.class.getDeclaredFields()) { + if(f.getName().startsWith("PREFS_")) { + try { + Object val = f.get(null); + if (val instanceof String) { + opts.add((String)val); + } + } catch(Exception ignore) {} + } + } + return opts; + } + + private static void printHelp(String[] commands, String description, String usage, Object defaultVal, int indent) { + String text = String.format("%s%s", StringUtils.leftPad("", indent), StringUtils.join(commands, ", ")); + + // Try to handle overflow + String[] overflow = null; + if((text.length() > 27 + indent) && text.contains(",")) { + String[] split = text.split(","); + text = split[0] + ","; + overflow = Arrays.copyOfRange(split, 1, split.length); + } + + if (description != null) { + text = StringUtils.rightPad(text, DESCRIPTION_COLUMN) + description; + if(defaultVal != null) { + text += String.format(" [%s]", defaultVal); + } + } + + if(overflow != null) { + for(int i = 0; i < overflow.length; i++) { + String ending = (i == overflow.length - 1) ? "" : ","; + text += System.lineSeparator() + StringUtils.leftPad("", indent + INDENT_SIZE) + overflow[i].trim() + ending; + } + } + System.out.println(text); + if (usage != null) { + System.out.println(StringUtils.rightPad("", DESCRIPTION_COLUMN) + String.format(" %s %s", USAGE_COMMAND, usage)); + } + } + + private static void printHelp(ArgValue argValue) { + printHelp(argValue.getMatches(), argValue.getDescription(), argValue.getUsage(), argValue.getDefaultVal(), INDENT_SIZE); + } + + private static void printHelp(ArgValueOption argValueOption) { + printHelp(argValueOption.getMatches(), argValueOption.getDescription(), null, null, INDENT_SIZE); + } +} diff --git a/old code/tray/src/qz/utils/ArgValue.java b/old code/tray/src/qz/utils/ArgValue.java new file mode 100755 index 0000000..9fdafef --- /dev/null +++ b/old code/tray/src/qz/utils/ArgValue.java @@ -0,0 +1,224 @@ +package qz.utils; + +import org.apache.commons.lang3.StringUtils; +import qz.common.Constants; +import qz.ws.substitutions.Substitutions; + +import java.util.ArrayList; +import java.util.Arrays; + +import static qz.utils.ArgValue.ArgType.*; + +public enum ArgValue { + // Informational + HELP(INFORMATION, "Display help information and exit.", null, null, + "--help", "-h", "/?"), + VERSION(INFORMATION, "Display version information and exit.", null, null, + "--version", "-v"), + BUNDLEID(INFORMATION, "Display Apple bundle identifier and exit.", null, null, + "--bundleid", "-i"), + LIBINFO(INFORMATION, "Display detailed library version information and exit.", null, null, + "--libinfo", "-l"), + + // Actions + ALLOW(ACTION,String.format("Add the specified certificate to %s.dat.", Constants.ALLOW_FILE), "--allow cert.pem", null, + "--allow", "--whitelist", "-a"), + BLOCK(ACTION, String.format("Add the specified certificate to %s.dat.", Constants.BLOCK_FILE), "--block cert.pem", null, + "--block", "--blacklist", "-b"), + FILE_ALLOW(ACTION, String.format("Add the specified file.allow entry to %s.properties for FileIO operations, sandboxed to a specified certificate if provided", Constants.PROPS_FILE), "--file-allow /my/file/path [--sandbox \"Company Name\"]", null, + "--file-allow"), + FILE_REMOVE(ACTION, String.format("Removes the specified file.allow entry from %s.properties for FileIO operations", Constants.PROPS_FILE), "--file-remove /my/file/path", null, + "--file-remove"), + + // Options + AUTOSTART(OPTION,"Read and honor any autostart preferences before launching.", null, true, + "--honorautostart", "-A"), + STEAL(OPTION, "Ask other running instance to stop so that this instance can take precedence.", null, false, + "--steal", Constants.DATA_DIR + ":steal"), + HEADLESS(OPTION, "Force startup \"headless\" without graphical interface or interactive components.", null, false, + "--headless"), + + // Installer stubs + PREINSTALL(INSTALLER, "Perform critical pre-installation steps: Stop instances, all other special considerations.", null, null, + "preinstall"), + INSTALL(INSTALLER, "Copy to the specified destination and preforms platform-specific registration.", "install --dest /my/install/location [--silent]", null, + "install"), + CERTGEN(INSTALLER, "Performs certificate generation and registration for proper HTTPS support.", "certgen [--key key.pem --cert cert.pem] [--pfx cert.pfx --pass 12345] [--host \"list;of;hosts\"]", null, + "certgen"), + UNINSTALL(INSTALLER, "Perform all uninstall tasks: Stop instances, delete files, unregister settings.", null, null, + "uninstall"), + SPAWN(INSTALLER, "Spawn an instance of the specified program as the logged-in user, avoiding starting as the root user if possible.", "spawn [program params ...]", null, + "spawn"), + + // Build stubs + JLINK(BUILD, "Download, compress and bundle a Java Runtime", "jlink [--platform mac|windows|linux] [--arch x64|aarch64] [--vendor bellsoft|eclipse|...] [--version ...] [--gc hotspot|openj9] [--gcversion ...]", null, + "jlink"), + PROVISION(BUILD, "Provision/bundle addition settings or resources into this installer", "provision --json file.json [--target-os windows --target-arch x86_64]", null, + "provision"), + + // Parameter stubs + TRAY_NOTIFICATIONS(PREFERENCES, "Show verbose connect/disconnect notifications in the tray area", null, false, + "tray.notifications"), + TRAY_HEADLESS(PREFERENCES, "Start QZ Tray in headless (no user interface) mode", null, false, + "tray.headless"), + TRAY_MONOCLE(PREFERENCES, "Enable/disable the use of the Monocle for JavaFX/HTML rendering", null, true, + "tray.monocle"), + TRAY_STRICTMODE(PREFERENCES, "Enable/disable solely trusting certificates matching authcert.override", null, false, + "tray.strictmode"), + TRAY_IDLE_PRINTERS(PREFERENCES, "Enable/disable idle crawling of printers and their media information for faster initial results", null, true, + "tray.idle.printers"), + TRAY_IDLE_JAVAFX(PREFERENCES, "Enable/disable idle starting of JavaFX for better initial performance", null, true, + "tray.idle.javafx"), + SECURITY_FILE_ENABLED(PREFERENCES, "Enable/disable all File Communications features", null, true, + "security.file.enabled"), + SECURITY_FILE_STRICT(PREFERENCES, "Enable/disable signing requirements for File Communications features", null, true, + "security.file.strict"), + + SECURITY_SUBSTITUTIONS_ENABLE(PREFERENCES, "Enable/disable client-side JSON data substitutions via \"" + Substitutions.FILE_NAME + "\" file", null, true, + "security.substitutions.enable"), + SECURITY_SUBSTITUTIONS_STRICT(PREFERENCES, "Enable/disable restrictions for materially changing JSON substitutions such as \"copies\":, \"data\": { \"data\": ... } blobs", null, true, + "security.substitutions.strict"), + + SECURITY_DATA_PROTOCOLS(PREFERENCES, "URL protocols allowed for print, serial, hid, etc", null, "http,https", + "security.data.protocols"), + SECURITY_PRINT_TOFILE(PREFERENCES, "Enable/disable printing directly to file paths", null, false, + "security.print.tofile"), + SECURITY_WSS_SNISTRICT(PREFERENCES, "Enables strict http/websocket SNI checks", null, false, + "security.wss.snistrict"), + SECURITY_WSS_HTTPSONLY(PREFERENCES, "Disables insecure http/websocket ports (e.g. '8182')", null, false, + "security.wss.httpsonly"), + SECURITY_WSS_HOST(PREFERENCES, "Influences which physical adapter to bind to by setting the host parameter for http/websocket listening", null, "0.0.0.0", + "security.wss.host"), + SECURITY_WSS_ALLOWORIGIN(PREFERENCES, "Override 'Access-Control-Allow-Origin: *' HTTP response header for fine-grained control of incoming HTTP connections", null, "*", + "security.wss.alloworigin"), + WEBSOCKET_SECURE_PORTS(PREFERENCES, "Comma separated list of secure websocket (wss://) ports to use", null, StringUtils.join(Constants.DEFAULT_WSS_PORTS, ","), + "websocket.secure.ports"), + WEBSOCKET_INSECURE_PORTS(PREFERENCES, "Comma separated list of insecure websocket (ws://) ports to use", null, StringUtils.join(Constants.DEFAULT_WS_PORTS, ","), + "websocket.insecure.ports"), + LOG_DISABLE(PREFERENCES, "Disable/enable logging features", null, false, + "log.disable"), + LOG_ROTATE(PREFERENCES, "Number of log files to retain when the size fills up", null, 5, + "log.rotate"), + LOG_SIZE(PREFERENCES, "Maximum file size (in bytes) of a single log file", null, 524288, + "log.size"), + AUTHCERT_OVERRIDE(PREFERENCES, "Override the trusted root certificate in the software.", null, null, + "authcert.override", "trustedRootCert"), + PRINTER_STATUS_JOB_DATA(PREFERENCES, "Return all raw (binary) job data with job statuses (use with caution)", null, false, + "printer.status.jobdata"); + + private ArgType argType; + private String description; + private String usage; + private Object defaultVal; + private String[] matches; + + ArgValue(ArgType argType, String description, String usage, Object defaultVal, String ... matches) { + this.argType = argType; + this.description = description; + this.usage = usage; + this.defaultVal = defaultVal; + this.matches = matches; + } + + public String getMatch() { return matches[0]; } + + public String[] getMatches() { + return matches; + } + + public String getDescription() { + return description; + } + + public String getUsage() { + return usage; + } + + public ArgType getType() { + return argType; + } + + public Object getDefaultVal() { + return defaultVal; + } + + public enum ArgType { + INFORMATION, + ACTION, + OPTION, + INSTALLER, + BUILD, + PREFERENCES, + } + + public static ArgValue[] filter(ArgType ... argTypes) { + ArrayList match = new ArrayList<>(Arrays.asList(argTypes)); + ArrayList found = new ArrayList<>(); + for(ArgValue argValue : values()) { + if(match.contains(argValue.getType())) { + found.add(argValue); + } + } + return found.toArray(new ArgValue[found.size()]); + } + + /** + * Child/parent for options + */ + public enum ArgValueOption { + // action + SANDBOX(ArgValue.FILE_ALLOW, "Treats the allow entry as a sandboxed location. Only certificates with an exact Common Name can access this location", + "--sandbox"), + + // install + DEST(ArgValue.INSTALL, "Installs to the specified destination. If omitted, a sane default will be used.", + "--dest", "-d"), + SILENT(ArgValue.INSTALL, "Suppress all prompts to the user, taking sane defaults.", + "--silent", "-s"), + + // certgen + HOST(ArgValue.CERTGEN, "Semicolon-delimited hostnames and/or IP addresses to generate the HTTPS certificate for.", + "--host", "--hosts"), + CERT(ArgValue.CERTGEN, "Path to a stand-alone HTTPS certificate", + "--cert", "-c"), + KEY(ArgValue.CERTGEN, "Path to a stand-alone HTTPS private key", + "--key", "-k"), + PFX(ArgValue.CERTGEN, "Path to a paired HTTPS private key and certificate in PKCS#12 format.", + "--pfx", "--pkcs12"), + PASS(ArgValue.CERTGEN, "Password for decoding private key.", + "--pass", "-p"); + + ArgValue parent; + String description; + String[] matches; + + ArgValueOption(ArgValue parent, String description, String ... matches) { + this.parent = parent; + this.description = description; + this.matches = matches; + } + + public static ArgValueOption[] filter(ArgValue ... parents) { + ArrayList match = new ArrayList<>(Arrays.asList(parents)); + ArrayList found = new ArrayList<>(); + for(ArgValueOption argValueOption : values()) { + if(match.contains(argValueOption.getParent())) { + found.add(argValueOption); + } + } + return found.toArray(new ArgValueOption[found.size()]); + } + + public ArgValue getParent() { + return parent; + } + + public String[] getMatches() { + return matches; + } + + public String getDescription() { + return description; + } + } +} diff --git a/old code/tray/src/qz/utils/ByteUtilities.java b/old code/tray/src/qz/utils/ByteUtilities.java new file mode 100755 index 0000000..acba8ba --- /dev/null +++ b/old code/tray/src/qz/utils/ByteUtilities.java @@ -0,0 +1,294 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.utils; + +import org.apache.commons.ssl.Base64; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.ByteArrayBuilder; +import qz.common.Constants; + +import java.util.*; + +/** + * Place for all raw static byte conversion functions. + * Especially useful for converting hexadecimal strings to byte arrays, + * byte arrays to hexadecimal strings, + * byte arrays to integer array conversions, etc. + * + * @author Tres Finocchiaro + */ +public class ByteUtilities { + private static final Logger log = LogManager.getLogger(ByteUtilities.class); + + public enum Endian { + BIG, LITTLE + } + + /** + * Converts a hexadecimal string to a byte array. + *

+ * This is especially useful for special characters that are appended via + * JavaScript, specifically "\0" or the {@code NUL} character, which + * will terminate a JavaScript string early. + * + * @param hex Base 16 String to covert to byte array. + */ + public static byte[] hexStringToByteArray(String hex) throws NumberFormatException { + byte[] data = new byte[0]; + if (hex != null && !hex.isEmpty()) { + String[] split; + if (hex.length() > 2) { + if (hex.length() >= 3 && hex.contains("x")) { + hex = hex.startsWith("x")? hex.substring(1):hex; + hex = hex.endsWith("x")? hex.substring(0, hex.length() - 1):hex; + split = hex.split("x"); + } else { + split = hex.split("(?<=\\G..)"); + } + + data = new byte[split.length]; + for(int i = 0; i < split.length; i++) { + Integer signedByte = Integer.parseInt(split[i], 16); + data[i] = (byte)(signedByte & 0xFF); + } + } else if (hex.length() == 2) { + data = new byte[] {Byte.parseByte(hex)}; + } + } + + return data; + } + + public static String toString(PrintingUtilities.Flavor flavor, byte[] bytes) { + switch(flavor) { + case BASE64: + return Base64.encodeBase64String(bytes); + case HEX: + return ByteUtilities.bytesToHex(bytes); + case PLAIN: + break; + default: + log.warn("ByteUtilities.toString(...) does not support {}, defaulting to {}", flavor, PrintingUtilities.Flavor.PLAIN); + } + return new String(bytes); + } + + public static String bytesToHex(byte[] bytes) { + return bytesToHex(bytes, true); + } + + /** + * Converts an array of bytes to its hexadecimal form. + * + * @param bytes Bytes to be converted. + * @param upperCase Whether the hex string should be UPPER or lower case. + */ + public static String bytesToHex(byte[] bytes, boolean upperCase) { + char[] hexChars = new char[bytes.length * 2]; + int v; + for(int j = 0; j < bytes.length; j++) { + v = bytes[j] & 0xFF; + hexChars[j * 2] = Constants.HEXES_ARRAY[v >>> 4]; + hexChars[j * 2 + 1] = Constants.HEXES_ARRAY[v & 0x0F]; + } + + if (upperCase) { + return new String(hexChars); + } + return new String(hexChars).toLowerCase(Locale.ENGLISH); + } + + /** + * Iterates through byte array finding matches of {@code match} inside {@code target}. + *

+ * TODO: Make this natively Iterable. + * + * @param target Byte array to search. + * @param match Sub-array to match inside {@code target}. + * @return Array of starting indices for matched values. + */ + public static Integer[] indicesOfMatches(byte[] target, byte[] match) { + if (target == null || match == null || target.length == 0 + || match.length == 0 || match.length > target.length) { + return new Integer[0]; + } + + LinkedList indexes = new LinkedList<>(); + + // Find instances of byte list + outer: + for(int i = 0; i < target.length - match.length + 1; i++) { + for(int j = 0; j < match.length; j++) { + if (target[i + j] != match[j]) { + continue outer; + } + } + + indexes.add(i); + } + + return indexes.toArray(new Integer[indexes.size()]); + } + + /** + * Gets the first index in {@code target} of matching bytes from {@code match} + * + * @param target Byte array to search for matches + * @param match Byte match searched + * @return First matching index from {@code target} array or {@code null} if no matches + */ + public static Integer firstMatchingIndex(byte[] target, byte[] match) { + return firstMatchingIndex(target, match, 0); + } + + /** + * Gets the first index in {@code target} of matching bytes from {@code match} where the index is equal or greater than {@code fromIndex} + * + * @param target Byte array to search for matches + * @param match Byte match searched + * @param fromIndex Offset index in {@code target} array (inclusive) + * @return First matching index after {@code fromIndex} from {@code target} array or {@code null} if no matches + */ + public static Integer firstMatchingIndex(byte[] target, byte[] match, int fromIndex) { + Integer[] indices = indicesOfMatches(target, match); + for(Integer idx : indices) { + if (idx >= fromIndex) { + return idx; + } + } + + return null; + } + + /** + * Splits the {@code src} byte array after every {@code count}-th instance of the supplied {@code pattern} byte array. + *

+ * This is useful for large print batches that need to be split up, + * (for example) after the P1 or ^XO command has been issued. + *

+ * TODO: + * A rewrite of this would be a proper {@code Iteratable} interface + * paired with an {@code Iterator} that does this automatically + * and would eliminate the need for a {@code indicesOfMatches()} function. + * + * @param src Array to split. + * @param pattern Pattern to determine where split should occur. + * @param count Number of matches between splits. + */ + public static List splitByteArray(byte[] src, byte[] pattern, int count) throws NullPointerException, IndexOutOfBoundsException, ArrayStoreException { + if (count < 1) { throw new IllegalArgumentException("Count cannot be less than 1"); } + + List byteArrayList = new ArrayList<>(); + ByteArrayBuilder builder = new ByteArrayBuilder(); + + Integer[] split = indicesOfMatches(src, pattern); + + int counted = 1; + int prev = 0; + + for(int i : split) { + //copy everything from the last pattern (or the start) to the end of this pattern + byte[] temp = new byte[i - prev + pattern.length]; + System.arraycopy(src, prev, temp, 0, temp.length); + builder.append(temp); + + //if we have 'count' matches, add it to list and start a new builder + if (counted < count) { + counted++; + } else { + byteArrayList.add(builder); + builder = new ByteArrayBuilder(); + counted = 1; + } + + prev = i + pattern.length; + } + + //include any builder matches below 'count' + if (!byteArrayList.contains(builder) && builder.getLength() > 0) { + byteArrayList.add(builder); + } + + return byteArrayList; + } + + /** + * Converts an integer array to a String representation of a hexadecimal number. + * + * @param raw Numbers to be converted to hex. + * @return Hex string representation. + */ + public static String getHexString(int[] raw) { + if (raw == null) { return null; } + + final StringBuilder hex = new StringBuilder(2 * raw.length); + for(final int i : raw) { + hex.append(Constants.HEXES.charAt((i & 0xF0) >> 4)).append(Constants.HEXES.charAt((i & 0x0F))); + } + + return hex.toString(); + } + + public static int parseBytes(byte[] bytes, int startIndex, int length, Endian endian) { + int parsed = 0; + + byte[] lenBytes = new byte[length]; + System.arraycopy(bytes, startIndex, lenBytes, 0, length); + + if (endian == Endian.BIG) { + for(int b = 0; b < length; b++) { + parsed <<= 8; + parsed += (int)lenBytes[b]; + } + } else { //LITTLE endian + for(int b = length - 1; b >= 0; b--) { + parsed <<= 8; + parsed += (int)lenBytes[b]; + } + } + + return parsed; + } + + public static int[] unwind(int bitwiseCode) { + int bitPopulation = Integer.bitCount(bitwiseCode); + int[] matches = new int[bitPopulation]; + int mask = 1; + + while(bitPopulation > 0) { + if ((mask & bitwiseCode) > 0) { + matches[--bitPopulation] = mask; + } + mask <<= 1; + } + return matches; + } + + public static boolean numberEquals(Object val1, Object val2) { + try { + if(val1 == null || val2 == null) { + return val1 == val2; + } else if(val1.getClass() == val2.getClass()) { + return val1.equals(val2); + } else if(val1 instanceof Long) { + return val1.equals(Long.parseLong(val2.toString())); + } else if(val2 instanceof Long) { + return val2.equals(Long.parseLong(val1.toString())); + } else { + return Double.parseDouble(val1.toString()) == Double.parseDouble(val2.toString()); + } + } catch(NumberFormatException nfe) { + log.warn("Cannot not compare [{} = '{}']. Reason: {} {}", val1, val2, nfe.getClass().getName(), nfe.getMessage()); + } + return false; + } + +} diff --git a/old code/tray/src/qz/utils/ColorUtilities.java b/old code/tray/src/qz/utils/ColorUtilities.java new file mode 100755 index 0000000..f604bac --- /dev/null +++ b/old code/tray/src/qz/utils/ColorUtilities.java @@ -0,0 +1,104 @@ +/* + * Copyright 2005 Joe Walker + * + * 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. + */ + +package qz.utils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.awt.image.BufferedImage; + +/** + * Utilities for working with colors. + * + * @author Joe Walker [joe at getahead dot ltd dot uk] + */ +public class ColorUtilities { + + private static final Logger log = LogManager.getLogger(ColorUtilities.class); + + public static final Color DEFAULT_COLOR = Color.WHITE; + + /** + * Decode an HTML color string like '#F567BA;' into a {@link Color} + * + * @param colorString The string to decode + * @return The decoded color, or White if it cannot be parsed + */ + public static Color decodeHtmlColorString(String colorString) { + colorString = colorString.replaceAll("[#;\\s]", ""); + + int red, green, blue; + switch(colorString.length()) { + case 6: + red = Integer.parseInt(colorString.substring(0, 2), 16); + green = Integer.parseInt(colorString.substring(2, 4), 16); + blue = Integer.parseInt(colorString.substring(4, 6), 16); + break; + case 3: + red = Integer.parseInt(colorString.substring(0, 1), 16); + green = Integer.parseInt(colorString.substring(1, 2), 16); + blue = Integer.parseInt(colorString.substring(2, 3), 16); + break; + case 1: + red = green = blue = Integer.parseInt(colorString.substring(0, 1), 16); + break; + default: + log.warn("Unable to read {} as a proper color string, returning default", colorString); + return DEFAULT_COLOR; + } + + return new Color(red, green, blue); + } + + /** + * Determines if the provided buffered image is black + * @param bi BufferedImage to check for all black pixels + * @return true if all black, false otherwise + */ + public static boolean isBlack(BufferedImage bi) { + for (int y = 0; y < bi.getHeight(); y++) { + for (int x = 0; x < bi.getWidth(); x++) { + int pixel = bi.getRGB(x, y); + if (pixel>>24 != 0x00 && (pixel & 0x00FFFFFF) != 0) { + return false; + } + } + } + return true; + } + + /** + * Inverts the color of all pixels in an image + * @param bi Source BufferedImage + * @return New inverted BufferedImage + */ + public static BufferedImage invert(BufferedImage bi) { + BufferedImage inverted = new BufferedImage(bi.getWidth(), bi.getHeight(), BufferedImage.TYPE_INT_ARGB); + for (int y = 0; y < bi.getHeight(); y++) { + for (int x = 0; x < bi.getWidth(); x++) { + int pixel = bi.getRGB(x, y); + int a = (pixel>>24)&0xFF; + int r = 0xFF ^ ((pixel>>16)&0xFF); + int g = 0xFF ^ ((pixel>>8)&0xFF); + int b = 0xFF ^ ((pixel>>0)&0xFF); + inverted.setRGB(x, y, a << 24 | r << 16 | g << 8 | b << 0); + } + } + return inverted; + } +} diff --git a/old code/tray/src/qz/utils/ConnectionUtilities.java b/old code/tray/src/qz/utils/ConnectionUtilities.java new file mode 100755 index 0000000..466a028 --- /dev/null +++ b/old code/tray/src/qz/utils/ConnectionUtilities.java @@ -0,0 +1,305 @@ +/** + * @author Ewan McDougall + * + * Copyright (C) 2017 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.utils; + +import java.awt.*; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.file.Paths; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import org.apache.commons.lang3.StringUtils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; + +import javax.net.ssl.*; + +public final class ConnectionUtilities { + + private static final Logger log = LogManager.getLogger(ConnectionUtilities.class); + private static Map requestProps; + + /** + * Returns an input stream that reads from the URL. + * While setting the underlying URLConnections User-Agent. + * + * @param urlString an absolute URL giving location of resource to read. + */ + public static InputStream getInputStream(String urlString, boolean protocolRestricted) throws IOException { + try { + URL url = new URL(urlString); + if(protocolRestricted) { + String allowed = PrefsSearch.getString(ArgValue.SECURITY_DATA_PROTOCOLS); + if(!isAllowed(allowed, url)) { + log.error("URL '{}' is not a valid http or https location. Configure property '{}' to modify this behavior.", url, ArgValue.SECURITY_DATA_PROTOCOLS.getMatch()); + throw new IOException(String.format("URL '%s' is not a valid [%s] location", url, allowed)); + } + } + URLConnection urlConn = url.openConnection(); + for( String key : getRequestProperties().keySet()) { + urlConn.setRequestProperty(key, requestProps.get(key)); + } + return urlConn.getInputStream(); + } catch(IOException e) { + if(e instanceof SSLHandshakeException) { + logSslInformation(urlString); + } + throw e; + } + } + + private static boolean isAllowed(String allowed, URL url) { + if(url == null) return false; + String urlProtocol = url.getProtocol(); + if(urlProtocol == null || urlProtocol.trim().isEmpty()) return false; + allowed = ArgValue.SECURITY_DATA_PROTOCOLS.getDefaultVal() + + (allowed == null || allowed.trim().isEmpty() ? "" : "," + allowed); + String[] protocols = allowed.split(","); + // Loop over http, https, etc + for(String protocol : protocols) { + if(urlProtocol.trim().equalsIgnoreCase(protocol.trim())) { + return true; + } + } + // Allow exception for file: demo/assets + if(urlProtocol.trim().toLowerCase(Locale.ENGLISH).equals("file")) { + try { + // Sanitize manipulative URLs + url = Paths.get(url.toURI()).normalize().toUri().toURL(); + if (url.getPath().matches(".*/demo/assets/.*|.*/tray/assets/.*")) { + log.warn("Allowing printing from restricted protocol '{}:' for demo asset '{}'", urlProtocol, url); + return true; + } + } + catch(URISyntaxException | MalformedURLException ignore) {} + } + return false; + } + + /** + * A blind SSL trust manager, for debugging SSL issues + */ + private static X509TrustManager BLIND_TRUST_MANAGER = new X509TrustManager() { + private X509Certificate[] accepted; + + @Override + public void checkClientTrusted(X509Certificate[] xcs, String string) { + // do nothing + } + + @Override + public void checkServerTrusted(X509Certificate[] accepted, String string) { + this.accepted = accepted; + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return accepted; + } + }; + + /** + * Log certificate information for a given URL, useful for troubleshooting "PKIX path building failed" + */ + private static void logSslInformation(String urlString) { + StringBuilder certInfo = new StringBuilder("\nCertificate details are unavailable"); + try { + URL url = new URL(urlString); + SSLContext context = SSLContext.getInstance("TLS"); + context.init(null, new TrustManager[] {BLIND_TRUST_MANAGER}, null); + SSLSocketFactory factory = context.getSocketFactory(); + SSLSocket socket = (SSLSocket)factory.createSocket(url.getHost(), url.getPort()); + socket.startHandshake(); + socket.close(); + + Certificate[] chain = socket.getSession().getPeerCertificates(); + + if (chain != null) { + certInfo = new StringBuilder(); + for(java.security.cert.Certificate cert : chain) { + if (cert instanceof X509Certificate) { + X509Certificate x = (X509Certificate)cert; + certInfo.append(String.format("\n\n\t%s: %s", "Subject: ", x.getIssuerX500Principal())); + certInfo.append(String.format("\n\t%s: %s", "From: ", x.getNotBefore())); + certInfo.append(String.format("\n\t%s: %s", "Expires: ", x.getNotAfter())); + } + } + } + + } catch(Exception ignore) {} + log.error("A trust exception has occurred with the provided certificate(s). This\n" + + "\tmay be SSL misconfiguration, interception by proxy, firewall, antivirus\n" + + "\tor in some cases a dated or corrupted Java installation. Please attempt\n" + + "\tto resolve this problem manually before reaching out to support." + + "{}\n", certInfo); + } + + private static Map getRequestProperties() { + if (requestProps == null) { + requestProps = new HashMap() { + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (String key : keySet()) + sb.append(key + ": " + get(key) + "\n"); + return sb.toString(); + } + }; + + // Legacy User-Agent String + requestProps.put("User-Agent", String.format("Mozilla/5.0 (%s; %s) %s/%s %s/%s", + getUserAgentOS(), + getUserAgentArch(), + Constants.ABOUT_TITLE.replaceAll("[^a-zA-Z]", ""), + Constants.VERSION.getNormalVersion(), + getFrameworkName(), + getFrameworkMajorVersion() + + )); + + // Client Hints + requestProps.put("Sec-CH-UA-Platform", getPlatform(false)); + requestProps.put("Sec-CH-UA-Platform-Version", getPlatformVersion()); + requestProps.put("Sec-CH-UA-Arch", getArch()); + requestProps.put("Sec-CH-UA-Bitness", getBitness()); + requestProps.put("Sec-CH-UA-Full-Version", Constants.VERSION.toString()); + requestProps.put("Sec-CH-UA", String.format("\"%s\"; v=\"%s\", \"%s\"; v=\"%s\"", + Constants.ABOUT_TITLE, + Constants.VERSION, + getFrameworkName(), + getFrameworkVersion())); + log.trace("User agent string for URL requests:\n\n{}", requestProps.toString()); + } + return requestProps; + } + + private static String getArch() { + switch(SystemUtilities.getArch()) { + case X86: + case X86_64: + return "x86"; + case AARCH64: + return "arm"; + case RISCV32: + case RISCV64: + return "riscv"; + case PPC64: + return "ppc"; + default: + return "unknown"; + } + } + + private static String getBitness() { + // If available, will return "64" or "32" + String bitness = System.getProperty("sun.arch.data.model"); + if(bitness != null ) { + return bitness; + } + // fallback on some sane, hard-coded values + switch(SystemUtilities.getArch()) { + case ARM32: + case RISCV32: + case X86: + return "32"; + case X86_64: + case RISCV64: + case PPC64: + default: + return "64"; + } + } + + private static String getPlatform(boolean legacy) { + if(SystemUtilities.isWindows()) { + return legacy ? "Windows NT" : "Windows"; + } else if(SystemUtilities.isMac()) { + return legacy ? "Macintosh" : "macOS"; + } else if(SystemUtilities.isLinux()) { + //detect display manager + String linuxOS = ""; + String[] parts = StringUtils.split(System.getProperty("awt.toolkit"), "."); + //assume sun.awt.X11.XToolKit namespace + if (!GraphicsEnvironment.isHeadless() && parts != null && parts.length > 2) { + linuxOS = parts[2]; + } + if (UnixUtilities.isUbuntu()) { + linuxOS += (linuxOS.isEmpty() ? "" : "; ") + "Ubuntu"; + } else if(UnixUtilities.isFedora()) { + linuxOS += (linuxOS.isEmpty()? "" : "; ") + "Fedora"; + } else if(UnixUtilities.isDebian()) { + linuxOS += (linuxOS.isEmpty() ? "" : "; ") + "Debian"; + } + return linuxOS; + } + return System.getProperty("os.name"); + } + + private static String getPlatformVersion() { + return System.getProperty("os.version"); + } + + private static String getFrameworkName() { + return "Java"; + } + + private static String getFrameworkMajorVersion() { + return System.getProperty("java.vm.specification.version"); + } + + private static String getFrameworkVersion() { + return Constants.JAVA_VERSION.toString(); + } + + private static String getUserAgentOS() { + if (SystemUtilities.isWindows()) { + //assume NT + return String.format("%s %s", getPlatform(true), getPlatformVersion()); + } else if(SystemUtilities.isMac()) { + return String.format("%s; %s %s", getPlatform(true), System.getProperty("os.name"), getPlatformVersion().replace('.', '_')); + } + return getPlatform(true); + } + + private static String getUserAgentArch() { + String arch; + switch (SystemUtilities.getArch()) { + case X86: + arch = "x86"; + break; + case X86_64: + arch = "x86_64"; + break; + default: + arch = SystemUtilities.OS_ARCH; + } + + switch(SystemUtilities.getOs()) { + case LINUX: + return "Linux " + arch; + case WINDOWS: + if(WindowsUtilities.isWow64()) { + return "WOW64"; + } + default: + return arch; + } + } +} diff --git a/old code/tray/src/qz/utils/DeviceUtilities.java b/old code/tray/src/qz/utils/DeviceUtilities.java new file mode 100755 index 0000000..380f81d --- /dev/null +++ b/old code/tray/src/qz/utils/DeviceUtilities.java @@ -0,0 +1,79 @@ +package qz.utils; + +import org.apache.commons.codec.binary.StringUtils; +import org.apache.commons.lang3.StringEscapeUtils; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +/** + * Utilities used by both Serial and USB/HID + */ +public class DeviceUtilities { + + private static final Logger log = LogManager.getLogger(DeviceUtilities.class); + + /** + * Pull data from a json object and convert it to bytes based on type + */ + public static byte[] getDataBytes(JSONObject params, Charset charset) throws JSONException, IOException { + if (charset == null) { charset = StandardCharsets.UTF_8; } + + byte[] bytesToSend = null; + + JSONObject metadata = params.optJSONObject("data"); + if (metadata == null) { + metadata = new JSONObject(); + metadata.put("data", params.get("data")); + metadata.put("type", "PLAIN"); + } + + // Flavor is called "type" in this API + PrintingUtilities.Flavor flavor = PrintingUtilities.Flavor.parse(metadata.optString("type"), PrintingUtilities.Flavor.PLAIN); + + switch(flavor) { + case PLAIN: + // Special handling for raw bytes + if (metadata.optJSONArray("data") == null) { + bytesToSend = characterBytes(metadata.getString("data"), charset); + } else { + JSONArray fromSource = metadata.getJSONArray("data"); + bytesToSend = new byte[fromSource.length()]; + for(int i = 0; i < fromSource.length(); i++) { + bytesToSend[i] = (byte)fromSource.getInt(i); + } + } + break; + default: + bytesToSend = flavor.read(metadata.getString("data")); + } + + return bytesToSend; + } + + + /** + * Turn a string into a character byte array. + * First attempting to take the entire string as a character literal (for non-printable unicode). + */ + public static byte[] characterBytes(String convert, Charset charset) { + if (convert.length() > 2) { + try { + //try to interpret entire string as single char representation (such as "\u0000" or "0xFFFF") + char literal = (char)Integer.parseInt(convert.substring(2), 16); + return StringUtils.getBytesUtf8(String.valueOf(literal)); + } + catch(NumberFormatException ignore) {} + } + + //try escaping string using Apache (to get strings like "\r" as characters) + return StringEscapeUtils.unescapeJava(convert).getBytes(charset); + } + +} diff --git a/old code/tray/src/qz/utils/FileUtilities.java b/old code/tray/src/qz/utils/FileUtilities.java new file mode 100755 index 0000000..b844a03 --- /dev/null +++ b/old code/tray/src/qz/utils/FileUtilities.java @@ -0,0 +1,925 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.utils; + +import org.apache.commons.io.Charsets; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.text.translate.CharSequenceTranslator; +import org.apache.commons.lang3.text.translate.LookupTranslator; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.w3c.dom.DOMException; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; +import org.xml.sax.SAXException; +import qz.App; +import qz.auth.Certificate; +import qz.auth.RequestState; +import qz.common.ByteArrayBuilder; +import qz.common.Constants; +import qz.common.PropertyHelper; +import qz.communication.FileIO; +import qz.communication.FileParams; +import qz.exception.NullCommandException; +import qz.installer.WindowsSpecialFolders; +import qz.installer.certificate.CertificateManager; +import qz.installer.provision.ProvisionInstaller; + +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; +import java.io.*; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.nio.file.*; +import java.nio.file.attribute.*; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.stream.Stream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import static qz.common.Constants.*; + +/** + * Common static file i/o utilities + * + * @author Tres Finocchiaro + */ +public class FileUtilities { + + private static final Logger log = LogManager.getLogger(FileUtilities.class); + public static final Path USER_DIR = getUserDirectory(); + public static final Path SHARED_DIR = getSharedDirectory(); + public static final Path TEMP_DIR = getTempDirectory(); + public static final char FILE_SEPARATOR = ';'; + public static final char FIELD_SEPARATOR = '|'; + public static final char ESCAPE_CHAR = '^'; + + /** + * Zips up the USER_DIR, places on desktop with timestamp + */ + public static boolean zipLogs() { + String date = new SimpleDateFormat("yyyy-MM-dd_HHmm").format(new Date()); + String filename = Constants.DATA_DIR + "-" + date + ".zip"; + Path destination = getUserDesktop().resolve(filename); + + try { + zipDirectory(USER_DIR, destination); + log.info("Zipped the contents of {} and placed the resulting files in {}", USER_DIR, destination); + return true; + } catch(IOException e) { + log.warn("Could not create zip file: {}", destination, e); + } + return false; + } + + protected static void zipDirectory(Path sourceDir, Path outputFile) throws IOException { + final ZipOutputStream outputStream = new ZipOutputStream(new FileOutputStream(outputFile.toFile())); + Files.walkFileTree(sourceDir, new SimpleFileVisitor() { + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attributes) { + try { + Path targetFile = sourceDir.relativize(file); + outputStream.putNextEntry(new ZipEntry(targetFile.toString())); + byte[] bytes = Files.readAllBytes(file); + outputStream.write(bytes, 0, bytes.length); + outputStream.closeEntry(); + } catch (IOException e) { + e.printStackTrace(); + } + return FileVisitResult.CONTINUE; + } + }); + outputStream.close(); + } + + /** + * Location where user Desktop is located + */ + private static Path getUserDesktop() { + // OneDrive will override the default location on Windows + if(SystemUtilities.isWindows()) { + try { + return Paths.get(WindowsSpecialFolders.DESKTOP.getPath()); + } + catch(Throwable ignore) {} + } + return Paths.get(System.getProperty("user.home"), "Desktop"); + } + + /** + * Location where preferences and logs are kept + */ + private static Path getUserDirectory() { + if(SystemUtilities.isWindows()) { + try { + return Paths.get(WindowsSpecialFolders.ROAMING_APPDATA.getPath(), Constants.DATA_DIR); + } catch(Throwable ignore) { + return Paths.get(System.getenv("APPDATA"), Constants.DATA_DIR); + } + } else if(SystemUtilities.isMac()) { + return Paths.get(System.getProperty("user.home"), "/Library/Application Support/", Constants.DATA_DIR); + } else { + return Paths.get(System.getProperty("user.home"), "." + Constants.DATA_DIR); + } + } + + /** + * Location where shared preferences are kept, such as .autostart + */ + private static Path getSharedDirectory() { + if(SystemUtilities.isWindows()) { + try { + return Paths.get(WindowsSpecialFolders.PROGRAM_DATA.getPath(), Constants.DATA_DIR); + } catch(Throwable ignore) { + return Paths.get(System.getenv("PROGRAMDATA"), Constants.DATA_DIR); + } + } else if(SystemUtilities.isMac()) { + return Paths.get("/Library/Application Support/", Constants.DATA_DIR); + } else { + return Paths.get("/srv/", Constants.DATA_DIR); + } + } + + public static boolean childOf(File childFile, Path parentPath) { + Path child = childFile.toPath().normalize().toAbsolutePath(); + Path parent = parentPath.normalize().toAbsolutePath(); + return child.startsWith(parent); + } + + public static Path inheritParentPermissions(Path filePath) { + if(SystemUtilities.isWindows()) { + // assume permissions are inherited + } else { + // assume permissions are not inherited + try { + FileAttribute> attributes = PosixFilePermissions.asFileAttribute(Files.getPosixFilePermissions(filePath.getParent())); + Files.setPosixFilePermissions(filePath, attributes.value()); + // Remove execute flag + filePath.toFile().setExecutable(false, false); + } catch(IOException e) { + log.warn("Unable to inherit file permissions {}", filePath, e); + } + } + return filePath; + + } + + private static final String[] badExtensions = new String[] { + "exe", "pif", "paf", "application", "msi", "com", "cmd", "bat", "lnk", // Windows Executable program or script + "gadget", // Windows desktop gadget + "msp", "mst", // Microsoft installer patch/transform file + "cpl", "scr", "ins", // Control Panel/Screen Saver/Internet Settings + "hta", // HTML application, run as trusted application without sandboxing + "msc", // Microsoft Management Console file + "dll", // Microsoft shared library + "jar", "jnlp", // Java Executable + "vb", "vbs", "vbe", "js", "jse", "ws", "wsf", "wsc", "wsh",// Windows Script + "ps1", "ps1xml", "ps2", "ps2xml", "ps1", "ps1xml", "ps2", "ps2xml", "psc1", "psc2", // Windows PowerShell script + "msh", "msh1", "msh2", "mshxml", "msh1xml", "msh2xml", // Monad/PowerShell script + "scf", "inf", // Windows Explorer/AutoRun command file + "reg", // Windows Registry file + "doc", "docx", "dot", "dotx", "dotm", // Microsoft Word + "xls", "xlt", "xlm", "xlsx", "xlsm", "xltx", "xltm", "xlsb", "xla", "xlam", "xll", "xlw", // Microsoft Excel + "ppt", "pps", "pptx", "pptm", "potx", "potm", "ppam", "ppsx", "ppsm", "sldx", "sldm", // Microsoft PowerPoint + "ade", "adp", "adn", "accdb", "accdr", "accdt", "mdb", "mda", "mdn", "mdt", // Microsoft Access + "mdw", "mdf", "mde", "accde", "mam", "maq", "mar", "mat", "maf", "ldb", "laccdb", // Microsoft Access + "app", "action", "bin", "command", "workflow", // Mac OS Application/Executable + "sh", "ksh", "csh", "pl", "py", "bash", "run", // Unix Script + "ipa, apk", // iOS/Android App + "widget", // Yahoo Widget + "url" // Internet Shortcut + }; + + private static final CharSequenceTranslator translator = new LookupTranslator(new String[][] { + {"" + ESCAPE_CHAR, "" + ESCAPE_CHAR + ESCAPE_CHAR}, + {"\\", ESCAPE_CHAR + "b"}, + {"/", ESCAPE_CHAR + "f"}, + {":", ESCAPE_CHAR + "c"}, + {"*", ESCAPE_CHAR + "a"}, + {"?", ESCAPE_CHAR + "m"}, + {"\"", ESCAPE_CHAR + "q"}, + {"<", ESCAPE_CHAR + "g"}, + {">", ESCAPE_CHAR + "l"}, + {"|", ESCAPE_CHAR + "p"}, + {"" + (char)0x7f, ESCAPE_CHAR + "d"} + }); + + /* resource files */ + private static HashMap localFileMap = new HashMap<>(); + private static HashMap sharedFileMap = new HashMap<>(); + private static ArrayList> whiteList; + private static boolean FILE_IO_ENABLED = true; + private static boolean FILE_IO_STRICT = false; + + public static void setFileIoEnabled(boolean enabled) { + FILE_IO_ENABLED = enabled; + } + + public static void setFileIoStrict(boolean strict) { + FILE_IO_STRICT = strict; + } + + /** + * Performs security checks before allowing File IO operations: + * 1. Is the request verified (was the signature OK?)? + * 2. Is the certificate valid? + * 3. Is the location whitelisted? + * 4. Is the file extension permitted + */ + private static void checkFileRequest(Path path, FileParams fp, RequestState request, boolean allowRootDir) throws AccessDeniedException { + if(!FILE_IO_ENABLED) { + throw new AccessDeniedException("File operations are disabled"); + } else if(!request.isVerified() && FILE_IO_STRICT) { + throw new AccessDeniedException("File request is not verified"); + } else if(request.getCertUsed() == null || !request.getCertUsed().isTrusted()) { + throw new AccessDeniedException("Certificate provided is not trusted"); + } else if(!isWhiteListed(path, allowRootDir, fp.isSandbox(), request)) { + throw new AccessDeniedException("File operation is not in a permitted location"); + } else if(!allowRootDir && !Files.isDirectory(path)) { + if (!isGoodExtension(path)) { + throw new AccessDeniedException(path.toString()); + } + } + } + + public static Path getAbsolutePath(JSONObject params, RequestState request, boolean allowRootDir) throws JSONException, IOException { + return getAbsolutePath(params, request, allowRootDir, false); + } + + public static Path getAbsolutePath(JSONObject params, RequestState request, boolean allowRootDir, boolean createMissing) throws JSONException, IOException { + FileParams fp = new FileParams(params); + String commonName = request.isVerified()? escapeFileName(request.getCertName()):"UNTRUSTED"; + + Path path = createAbsolutePath(fp, commonName); + checkFileRequest(path, fp, request, allowRootDir); + initializeRootFolder(fp, commonName); + + if (createMissing) { + if (!SystemUtilities.isWindows()) { + Path resolve; + // Find existing parental directory + for(resolve = path.getParent(); !Files.exists(resolve); resolve = resolve.getParent()) { + // do nothing + } + Set permissions = Files.getPosixFilePermissions(resolve); + FileAttribute> fileAttributes = PosixFilePermissions.asFileAttribute(permissions); + Files.createDirectories(path.getParent(), fileAttributes); + } else { + Files.createDirectories(path.getParent()); + } + } + + return path; + } + + private static void initializeRootFolder(FileParams fileParams, String commonName) throws IOException { + Path parent = fileParams.isShared()? SHARED_DIR:USER_DIR; + + Path rootPath; + if (fileParams.isSandbox()) { + rootPath = Paths.get(parent.toString(), FileIO.SANDBOX_DATA_SUFFIX, commonName); + } else { + rootPath = Paths.get(parent.toString(), FileIO.GLOBAL_DATA_SUFFIX); + } + + if (!Files.exists(rootPath)) { + Files.createDirectories(rootPath); + if(fileParams.isShared()) { + rootPath.toFile().setWritable(true, false); + } + } + } + + /** + * Returns a normalised and absolute path. If the input path was relative, + * the root may reside in one of four locations, based on the sandbox and + * shared flags. If the input path was absolute, the path will be + * normalised and returned without any further changes. + * + * @param fileParams File or Directory to sandbox + * @param commonName Common name of the associated certificate for use with sandbox location + * @return absolute path of input, with relative location's root being determined by the {@code sandbox} and {@code shared} flags. + */ + public static Path createAbsolutePath(FileParams fileParams, String commonName) { + Path sanitizedPath; + if (fileParams.getPath().isAbsolute()) { + sanitizedPath = fileParams.getPath(); + } else { + Path parent = fileParams.isShared()? SHARED_DIR:USER_DIR; + if (fileParams.isSandbox()) { + sanitizedPath = Paths.get(parent.toString(), FileIO.SANDBOX_DATA_SUFFIX, commonName).resolve(fileParams.getPath()); + } else { + sanitizedPath = Paths.get(parent.toString(), FileIO.GLOBAL_DATA_SUFFIX).resolve(fileParams.getPath()); + } + } + + return sanitizedPath.normalize(); + } + + /** + * Checks a path's extension against a list of forbidden extensions. If a match is found, false is returned. + */ + public static boolean isGoodExtension(Path path) { + String fileName = path.getFileName().toString(); + + //"foo.exe." is valid on windows, but is immediately changed to "foo.exe" by the os + //this is undocumented behavior, therefore, rather than trying to support it, we fail it. + if (SystemUtilities.isWindows() && fileName.endsWith(".")) { return false; } + + String[] tokens = fileName.split("\\.(?=[^.]+$)"); + if (tokens.length == 2) { + String extension = tokens[1]; + for(String bad : badExtensions) { + if (bad.equalsIgnoreCase(extension)) { + return false; + } + } + } + return true; + } + + /** + * Returns whether or not the given file or folder is white-listed for File IO + * Currently hard-coded to the QZ data directory or anything provided by qz-tray.properties + * e.g. %APPDATA%/qz/data or $HOME/.qz/data, etc + */ + public static boolean isWhiteListed(Path path, boolean allowRootDir, boolean sandbox, RequestState request) { + String commonName = request.isVerified()? escapeFileName(request.getCertName()):"UNTRUSTED"; + if (whiteList == null) { + whiteList = new ArrayList<>(); + //default sandbox locations. More can be added through the properties file + whiteList.add(new AbstractMap.SimpleEntry<>(USER_DIR, FIELD_SEPARATOR + "sandbox" + FIELD_SEPARATOR)); + whiteList.add(new AbstractMap.SimpleEntry<>(SHARED_DIR, FIELD_SEPARATOR + "sandbox" + FIELD_SEPARATOR)); + whiteList.addAll(parseDelimitedPaths(getFileAllowProperty(App.getTrayProperties()).toString())); + } + + Path cleanPath = path.normalize().toAbsolutePath(); + for(Map.Entry allowed : whiteList) { + if (cleanPath.startsWith(allowed.getKey())) { + if ("".equals(allowed.getValue()) || allowed.getValue().contains(FIELD_SEPARATOR + commonName + FIELD_SEPARATOR) && (allowRootDir || !cleanPath.equals(allowed.getKey()))) { + return true; + } else if (allowed.getValue().contains(FIELD_SEPARATOR + "sandbox" + FIELD_SEPARATOR)) { + Path p; + if (sandbox) { + p = Paths.get(allowed.getKey().toString(), FileIO.SANDBOX_DATA_SUFFIX, commonName); + } else { + p = Paths.get(allowed.getKey().toString(), FileIO.GLOBAL_DATA_SUFFIX); + } + if (cleanPath.startsWith(p) && (allowRootDir || !cleanPath.equals(p))) { + return true; + } + } + } + } + return false; + } + + public static ArgParser.ExitStatus addFileAllowProperty(String path, String commonName) throws IOException { + PropertyHelper props = new PropertyHelper(new File(CertificateManager.getWritableLocation(), Constants.PROPS_FILE + ".properties")); + ArrayList> paths = parseDelimitedPaths(getFileAllowProperty(props).toString(), false); + Iterator> iterator = paths.iterator(); + String commonNameEscaped = escapePathProperty(commonName); + // First, iterate to see if the path already exists + boolean found = false; + boolean updated = false; + while(iterator.hasNext()) { + Map.Entry value = iterator.next(); + if(value.getKey().toString().equals(path)) { + found = true; + if(!commonNameEscaped.isEmpty() && !value.getValue().contains(commonNameEscaped)) { + value.setValue((value.getValue().isEmpty() ? FIELD_SEPARATOR : value.getValue()) + commonNameEscaped + FIELD_SEPARATOR); + updated = true; + } + } + } + if(!found) { + paths.add(new AbstractMap.SimpleEntry<>(Paths.get(path).normalize().toAbsolutePath(), commonNameEscaped.isEmpty() ? "" : FIELD_SEPARATOR + commonNameEscaped + FIELD_SEPARATOR)); + updated = true; + } + if(updated) { + if(saveFileAllowProperty(props, paths)) { + log.info("Added \"file.allow\" entry to {}.properties.", Constants.PROPS_FILE); + return ArgParser.ExitStatus.SUCCESS; + } + return ArgParser.ExitStatus.GENERAL_ERROR; + } else { + log.warn("Skipping \"file.allow\" entry in {}.properties, it already exist.", Constants.PROPS_FILE); + return ArgParser.ExitStatus.SUCCESS; + } + } + + public static ArgParser.ExitStatus removeFileAllowProperty(String path) throws IOException { + PropertyHelper props = new PropertyHelper(new File(CertificateManager.getWritableLocation(), Constants.PROPS_FILE + ".properties")); + ArrayList> paths = parseDelimitedPaths(getFileAllowProperty(props).toString(), false); + + int before = paths.size(); + Iterator> iterator = paths.iterator(); + while(iterator.hasNext()) { + Map.Entry value = iterator.next(); + if(value.getKey().toString().equals(path)) { + iterator.remove(); + } + } + if(paths.size() != before) { + if(saveFileAllowProperty(props, paths)) { + log.info("Removed \"file.allow\" entry from {}.properties.", Constants.PROPS_FILE); + return ArgParser.ExitStatus.SUCCESS; + } + return ArgParser.ExitStatus.GENERAL_ERROR; + } else { + log.warn("Skipping \"file.allow\" entry in {}.properties, it doesn't exist.", Constants.PROPS_FILE); + return ArgParser.ExitStatus.SUCCESS; + } + } + + private static boolean saveFileAllowProperty(PropertyHelper props, ArrayList> paths) { + StringBuilder fileAllow = new StringBuilder(); + for(Map.Entry path : paths) { + fileAllow + .append(escapePathProperty(path.getKey().toString())) + .append(path.getValue()) + .append(FILE_SEPARATOR); + } + props.remove("file.whitelist"); + props.remove("file.allow"); + if(fileAllow.length() > 0) { + props.setProperty("file.allow", fileAllow.toString()); + } + return props.save(); + } + + /** + * Escapes ESCAPE_CHARACTER, FILE_SEPARATOR and FIELD_SEPARATOR + * so it a multi-value file path can be safely parsed later + */ + private static String escapePathProperty(String path) { + return path == null ? "" : path + .replace("" + ESCAPE_CHAR, ("" + ESCAPE_CHAR) + ESCAPE_CHAR) + .replace("" + FILE_SEPARATOR, ("" + ESCAPE_CHAR) + FILE_SEPARATOR) + .replace("" + FIELD_SEPARATOR, ("" + ESCAPE_CHAR) + FIELD_SEPARATOR); + } + + private static StringBuilder getFileAllowProperty(Properties props) { + StringBuilder propString = new StringBuilder(); + if(props != null) { + propString.append(props.getProperty("file.allow", "")); + if (propString.length() == 0) { + // Deprecated + propString.append(props.getProperty("file.whitelist", "")); + if (propString.length() > 0) { + log.warn("Property \"file.whitelist\" is deprecated and will be removed in a future version. Please use \"file.allow\" instead."); + } + } + } + return propString; + } + + /** + * Parses semi-colon delimited paths with optional pipe-delimited descriptions + * e.g. C:\file1.txt;C:\file2.txt + * C:\file1.txt|ABC Inc.;C:\file2.txt|XYZ Inc. + */ + public static ArrayList> parseDelimitedPaths(String delimited, boolean escapeCommonNames) { + ArrayList> foundPaths = new ArrayList<>(); + if (delimited != null) { + StringBuilder propString = new StringBuilder(delimited); + boolean escaped = false; + boolean resetPending = false, tokenPending = false; + ArrayList tokens = new ArrayList<>(); + //unescape and tokenize + for(int i = 0; i < propString.length(); i++) { + char iteratingChar = propString.charAt(i); + //if the char before this was an escape char, we are no longer escaped and we skip delimiter detection + if (escaped) { + escaped = false; + } else { + if (iteratingChar == ESCAPE_CHAR) { + escaped = true; + propString.deleteCharAt(i); + i--; + } else { + tokenPending = iteratingChar == FIELD_SEPARATOR || iteratingChar == FILE_SEPARATOR; + resetPending = iteratingChar == FILE_SEPARATOR; + } + } + boolean lastChar = (i == propString.length() - 1); + //if a delimiter is found, save string to token and delete it from propString + if (tokenPending || lastChar) { + String token = propString.substring(0, lastChar && !tokenPending ? i + 1 : i); + if (!token.isEmpty()) tokens.add(token); + propString.delete(0, i + 1); + i = -1; + tokenPending = false; + } + //if a semicolon was found or we are on the last char of the string, dump the tokens into a pair and add it to whiteList + if (resetPending || lastChar) { + resetPending = false; + String commonNames = tokens.size() > 1? "" + FIELD_SEPARATOR:""; + for(int n = 1; n < tokens.size(); n++) { + if(escapeCommonNames) { + commonNames += escapeFileName(tokens.get(n)) + FIELD_SEPARATOR; + } else { + // We still need to maintain some level of escaping for reserved characters + // this is just going to cause headaches later, but we don't have much choice + commonNames += escapePathProperty(tokens.get(n)); + if(!commonNames.endsWith("" + FIELD_SEPARATOR)) { + commonNames += FIELD_SEPARATOR; + } + } + } + foundPaths.add(new AbstractMap.SimpleEntry<>(Paths.get(tokens.get(0)).normalize().toAbsolutePath(), commonNames)); + tokens.clear(); + } + } + } + return foundPaths; + } + + public static ArrayList> parseDelimitedPaths(Properties props, String key) { + return parseDelimitedPaths(props == null ? null : props.getProperty(key)); + } + + public static ArrayList> parseDelimitedPaths(String delimited) { + return parseDelimitedPaths(delimited, false); + } + + /** + * Returns whether or not the supplied path is restricted, such as the qz-tray data directory + * Warning: This does not follow symlinks + * + * @param path File or Directory path to test + * @return {@code true} if restricted, {@code false} otherwise + */ + public static boolean isBadPath(String path) { + return childOf(new File(path), USER_DIR); + } + + /** + * Escapes invalid chars from filenames. This does not cause collisions. Escape char is ESCAPE_CHAR + * Characters escaped, ^ \ / : * ? " < > | + * Warning: Restricted filenames such as lpt1, com1, aux... are not escaped by this function + * + * @param fileName file name to escape + * @return escaped string + */ + public static String escapeFileName(String fileName) { + StringBuilder returnStringBuilder = new StringBuilder(translator.translate(fileName)); + for(int n = returnStringBuilder.length() - 1; n >= 0; n--) { + char c = returnStringBuilder.charAt(n); + if (c < 0x20) { + returnStringBuilder.replace(n, n + 1, ESCAPE_CHAR + String.format("%02d", (int)c)); + } + } + return returnStringBuilder.toString(); + } + + public static String readLocalFile(String file) throws IOException { + return new String(readFile(new DataInputStream(new FileInputStream(file))), Charsets.UTF_8); + } + + public static String readLocalFile(Path path) throws IOException { + return new String(readFile(new DataInputStream(new FileInputStream(path.toFile()))), Charsets.UTF_8); + } + + public static byte[] readRawFile(String url) throws IOException { + return readFile(new DataInputStream(ConnectionUtilities.getInputStream(url, true))); + } + + private static byte[] readFile(DataInputStream in) throws IOException { + ByteArrayBuilder cmds = new ByteArrayBuilder(); + byte[] buffer = new byte[Constants.BYTE_BUFFER_SIZE]; + + int len; + while((len = in.read(buffer)) > -1) { + byte[] temp = new byte[len]; + System.arraycopy(buffer, 0, temp, 0, len); + cmds.append(temp); + } + in.close(); + + return cmds.getByteArray(); + } + + + public static void setupListener(FileIO fileIO) throws IOException { + FileWatcher.startWatchThread(); + FileWatcher.registerWatch(fileIO); + } + + + /** + * Reads an XML file from URL, searches for the tag specified by + * {@code dataTag} tag name and returns the {@code String} value + * of that tag. + * + * @param url location of the xml file to be read + * @param dataTag tag in the file to be searched + * @return value of the tag if found + */ + public static String readXMLFile(String url, String dataTag) throws DOMException, IOException, NullCommandException, + ParserConfigurationException, SAXException { + Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(url); + doc.getDocumentElement().normalize(); + log.info("Root element " + doc.getDocumentElement().getNodeName()); + + NodeList nodeList = doc.getElementsByTagName(dataTag); + if (nodeList.getLength() > 0) { + return nodeList.item(0).getTextContent(); + } + + throw new NullCommandException(String.format("Node \"%s\" could not be found in XML file specified", dataTag)); + } + + + public static synchronized boolean printLineToFile(String fileName, String message, boolean local) { + File file = getFile(fileName, local); + if (file == null) { return false; } + + try(FileWriter fw = new FileWriter(file, true)) { + message += "\r\n"; + fw.write(message); + fw.flush(); + return true; + } + catch(IOException e) { + log.error("Cannot write to file {}", fileName, e); + } + + return false; + } + + public static boolean printLineToFile(String fileName, String message) { + return printLineToFile(fileName, message, true); + } + + public static File getFile(String name, boolean local) { + HashMap fileMap; + if (local) { + fileMap = localFileMap; + } else { + fileMap = sharedFileMap; + } + + if (!fileMap.containsKey(name) || fileMap.get(name) == null) { + File path = local ? USER_DIR.toFile() : SHARED_DIR.toFile(); + File dat = Paths.get(path.toString(), name + ".dat").toFile(); + + try { + path.mkdirs(); + dat.createNewFile(); + if(!local) { + dat.setReadable(true, false); + dat.setWritable(true, false); + } + } + catch(IOException e) { + //failure is possible due to user permissions on shared files + if (local || (!name.equals(Constants.ALLOW_FILE) && !name.equals(Constants.BLOCK_FILE))) { + log.warn("Cannot setup file {} ({})", dat, local? "Local":"Shared", e); + } + } + + if (dat.exists()) { + fileMap.put(name, dat); + } + } + + return fileMap.get(name); + } + + public static void deleteFile(String name) { + File file = localFileMap.get(name); + + if (file != null && !file.delete()) { + log.warn("Unable to delete file {}", name); + file.deleteOnExit(); + } + + localFileMap.put(name, null); + } + + public static ArgParser.ExitStatus addToCertList(String list, File certFile) throws Exception { + FileReader fr = new FileReader(certFile); + Certificate cert = new Certificate(IOUtils.toString(fr)); + if(FileUtilities.printLineToFile(list, cert.data(), !SystemUtilities.isAdmin())) { + log.info("Successfully added {} to {} list", cert.getOrganization(), ALLOW_FILE); + return ArgParser.ExitStatus.SUCCESS; + } + log.error("Failed to add {} to {} list", cert.getOrganization(), ALLOW_FILE); + return ArgParser.ExitStatus.GENERAL_ERROR; + } + + public static synchronized boolean deleteFromFile(String fileName, String deleteLine, boolean local) { + File file = getFile(fileName, local); + File temp = getFile(Constants.TEMP_FILE, local); + + try(BufferedReader br = new BufferedReader(new FileReader(file)); BufferedWriter bw = new BufferedWriter(new FileWriter(temp))) { + String line; + while((line = br.readLine()) != null) { + if (!line.equals(deleteLine)) { + bw.write(line + "\r\n"); + } + } + + bw.flush(); + bw.close(); + br.close(); + + deleteFile(fileName); + return temp.renameTo(file); + } + catch(IOException e) { + log.error("Unable to delete line from file", e); + return false; + } + } + + /** + * + * @return First line of ".autostart" file in user or shared space or "0" if blank. If neither are found, returns "1". + * @throws IOException + */ + private static String readAutoStartFile() throws IOException { + log.debug("Checking for {} preference in user directory {}...", Constants.AUTOSTART_FILE, USER_DIR); + Path userAutoStart = Paths.get(USER_DIR.toString(), Constants.AUTOSTART_FILE); + List lines = null; + if (Files.exists(userAutoStart)) { + lines = Files.readAllLines(userAutoStart); + } else { + log.debug("Checking for {} preference in shared directory {}...", Constants.AUTOSTART_FILE, SHARED_DIR); + Path sharedAutoStart = Paths.get(SHARED_DIR.toString(), Constants.AUTOSTART_FILE); + if (Files.exists(sharedAutoStart)) { + lines = Files.readAllLines(sharedAutoStart); + } + } + if (lines == null) { + return "1"; + } else if (lines.isEmpty()) { + log.warn("File {} is empty, this shouldn't happen.", Constants.AUTOSTART_FILE); + return "0"; + } else { + String val = lines.get(0).trim(); + log.debug("Autostart preference {} contains {}", Constants.AUTOSTART_FILE, val); + return val; + } + } + + private static synchronized boolean writeAutoStartFile(String mode) throws IOException { + Path autostartFile = Paths.get(USER_DIR.toString(), Constants.AUTOSTART_FILE); + Files.write(autostartFile, mode.getBytes(), StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE, StandardOpenOption.WRITE); + return readAutoStartFile().equals(mode); + } + + public static boolean setAutostart(boolean autostart) { + try { + return writeAutoStartFile(autostart ? "1": "0"); + } + catch(IOException e) { + return false; + } + } + + public static boolean isAutostart() { + try { + return "1".equals(readAutoStartFile()); + } + catch(IOException e) { + return false; + } + } + + /** + * Configures the given embedded resource file using qz.common.Constants combined with the provided + * HashMap and writes to the specified location + * + * Will look for resource relative to relativeClass package location. + */ + public static synchronized void configureAssetFile(String relativeAsset, File dest, HashMap additionalMappings, Class relativeClass) throws IOException { + // Static fields, parsed from qz.common.Constants + List fields = new ArrayList<>(); + HashMap allMappings = (HashMap)additionalMappings.clone(); + fields.addAll(Arrays.asList(Constants.class.getFields())); // public only + for(Field field : fields) { + if (Modifier.isStatic(field.getModifiers())) { // static only + try { + String key = "%" + field.getName() + "%"; + Object value = field.get(null); + if (value != null) { + if (value instanceof String) { + allMappings.putIfAbsent(key, (String)value); + } else if(value instanceof Boolean) { + allMappings.putIfAbsent(key, "" + field.getBoolean(null)); + } + } + } + catch(IllegalAccessException e) { + // This should never happen; we are only using public fields + log.warn("{} occurred fetching a value for {}", e.getClass().getName(), field.getName(), e); + } + } + } + + BufferedReader reader = new BufferedReader(new InputStreamReader(relativeClass.getResourceAsStream(relativeAsset))); + BufferedWriter writer = new BufferedWriter(new FileWriter(dest)); + + String line; + while((line = reader.readLine()) != null) { + for(Map.Entry mapping : allMappings.entrySet()) { + if (line.contains(mapping.getKey())) { + line = line.replaceAll(mapping.getKey(), mapping.getValue()); + } + } + writer.write(line + "\n"); + } + reader.close(); + writer.close(); + } + + public static void configureAssetFile(String relativeAsset, Path dest, HashMap additionalMappings, Class relativeClass) throws IOException { + configureAssetFile(relativeAsset, dest.toFile(), additionalMappings, relativeClass); + } + + private static Path getTempDirectory() { + try { + return Files.createTempDirectory(Constants.DATA_DIR + "_data_"); + } catch(IOException e) { + log.warn("We couldn't get a temp directory for writing. This could cause some items to break"); + } + return null; + } + + public static void setPermissionsParentally(Path toTraverse, boolean worldWrite) { + Path stepper = toTraverse.toAbsolutePath(); + // Assume we shouldn't go higher than 2nd-level (e.g. "/etc", "C:\Program Files\", etc) + while(stepper.getParent() != null && !stepper.getRoot().equals(stepper.getParent())) { + File file = stepper.toFile(); + file.setReadable(true, false); + file.setExecutable(true, false); + file.setWritable(true, !worldWrite); + if (SystemUtilities.isWindows() && worldWrite) { + WindowsUtilities.setWritable(stepper); + } + stepper = stepper.getParent(); + } + } + + public static void setPermissionsRecursively(Path toRecurse, boolean worldWrite) { + try (Stream paths = Files.walk(toRecurse)) { + paths.forEach((path)->{ + if(SystemUtilities.isWindows() && worldWrite) { + // By default, NSIS sets owner to "Administrator", preventing non-admins from writing + // Add "Authenticated Users" write permission using + WindowsUtilities.setWritable(path); + } + if (path.toFile().isDirectory()) { + // Executable bit in Unix allows listing files + path.toFile().setExecutable(true, false); + } + path.toFile().setReadable(true, false); + path.toFile().setWritable(true, !worldWrite); + }); + } catch (IOException e) { + log.warn("An error occurred setting permissions: {}", toRecurse); + } + } + + public static void setExecutableRecursively(Path toRecurse, boolean ownerOnly) { + File folder = toRecurse.toFile(); + if(SystemUtilities.isWindows() || !folder.exists() || !folder.isDirectory()) { + return; + } + + // "provision.json" found, assume we're in the provisioning directory, only process scripts and installers + boolean isProvision = toRecurse.resolve(PROVISION_FILE).toFile().exists(); + + try (Stream paths = Files.walk(toRecurse)) { + paths.forEach((path)->{ + if (path.toFile().isDirectory()) { + // Executable bit in Unix allows listing files + path.toFile().setExecutable(true, ownerOnly); + } else if(!isProvision || ProvisionInstaller.shouldBeExecutable(path)) { + path.toFile().setExecutable(true, ownerOnly); + } + }); + } catch (IOException e) { + log.warn("An error occurred setting permissions: {}", toRecurse); + } + } + + public static void cleanup() { + if(FileUtilities.TEMP_DIR != null) { + FileUtils.deleteQuietly(FileUtilities.TEMP_DIR.toFile()); + } + } +} diff --git a/old code/tray/src/qz/utils/FileWatcher.java b/old code/tray/src/qz/utils/FileWatcher.java new file mode 100755 index 0000000..3f8b7ed --- /dev/null +++ b/old code/tray/src/qz/utils/FileWatcher.java @@ -0,0 +1,143 @@ +package qz.utils; + +import org.apache.commons.io.input.ReversedLinesFileReader; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.communication.FileIO; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.io.RandomAccessFile; +import java.nio.channels.ClosedChannelException; +import java.nio.charset.Charset; +import java.nio.file.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +public class FileWatcher { + + private static final Logger log = LogManager.getLogger(FileWatcher.class); + + private static Thread watchThread; + private static WatchService watchService; + + private static HashSet fileIOs = new HashSet<>(); + + + public synchronized static void startWatchThread() throws IOException { + if (watchThread != null && watchThread.isAlive()) { + log.debug("File WatchService is already started, reusing"); + return; + } + + log.debug("Starting File WatchService"); + watchService = FileSystems.getDefault().newWatchService(); + watchThread = new Thread(() -> { + boolean alive = true; + + while(alive) { + try { + WatchKey wk = watchService.take(); + // pollEvents() blocks, it must be interrupted + for(WatchEvent event : wk.pollEvents()) { + fileChanged((Path)wk.watchable(), event.context().toString(), event.kind().toString()); + } + wk.reset(); + } + catch(ClosedChannelException | InterruptedException | ClosedWatchServiceException closed) { + log.error("File WatchService ending"); + if(closed instanceof ClosedChannelException) { + log.error("Stream is closed, could not send message"); + } + alive = false; + } + } + }); + watchThread.start(); + } + + public static void registerWatch(FileIO fileIO) throws IOException { + fileIO.setWk(fileIO.getAbsolutePath().register(watchService, + StandardWatchEventKinds.ENTRY_MODIFY, + StandardWatchEventKinds.ENTRY_CREATE, + StandardWatchEventKinds.ENTRY_DELETE)); + + fileIOs.add(fileIO); + } + + public static void deregisterWatch(FileIO fileIO) { + fileIOs.remove(fileIO); + } + + + private synchronized static void fileChanged(Path path, String fileName, String type) throws ClosedChannelException { + Path filePath = path.resolve(fileName); + for(FileIO fio : fileIOs) { + if (!fio.isMatch(fileName)) continue; + + String fileData = null; + if (fio.getAbsolutePath().equals(path.normalize().toAbsolutePath())) { + if (!type.equals("ENTRY_DELETE") && fio.returnsContents() && !Files.isDirectory(filePath)) { + try { + switch(fio.getReadType()) { + case BYTES: + fileData = getBytes(filePath, fio); + break; + case LINES: + default: + fileData = getLines(filePath, fio); + } + } + catch(IOException e) { + log.error("Failed to read file due to {}", e.toString()); + fio.sendError("Failed to read file data due to " + e.getClass().getName()); + } + } + + fio.fileChanged(fileName, type, fileData); + } + } + + } + + private synchronized static String getBytes(Path path, FileIO listener) throws IOException { + try(RandomAccessFile raf = new RandomAccessFile(path.toFile(), "r")) { + byte[] bytes = listener.getBytes() == -1? new byte[(int)raf.length()]:new byte[(int)Math.min(raf.length(), listener.getBytes())]; + if (listener.isReversed()) { raf.seek(raf.length() - bytes.length); } + raf.readFully(bytes); + + return new String(bytes, Charset.forName("UTF-8")); + } + } + + private synchronized static String getLines(Path path, FileIO listener) throws IOException { + ArrayList linesRead = new ArrayList<>(); + + String buffer; + if (listener.isReversed()) { + try(ReversedLinesFileReader reader = new ReversedLinesFileReader(path.toFile())) { + int count = 0; + while((buffer = reader.readLine()) != null && count++ != listener.getLines()) { + // Warning, this will strip "\r" from the data + linesRead.add(buffer); + } + + Collections.reverse(linesRead); //ensure last X lines are returned in natural order + } + } else { + try(BufferedReader reader = new BufferedReader(new FileReader(path.toFile()))) { + int count = 0; + while((buffer = reader.readLine()) != null && count++ != listener.getLines()) { + // Warning, this will strip "\r" from the data + linesRead.add(buffer); + } + } + } + + return StringUtils.join(linesRead, "\n"); + } + +} diff --git a/old code/tray/src/qz/utils/GtkUtilities.java b/old code/tray/src/qz/utils/GtkUtilities.java new file mode 100755 index 0000000..7257bc4 --- /dev/null +++ b/old code/tray/src/qz/utils/GtkUtilities.java @@ -0,0 +1,124 @@ +package qz.utils; + +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.Pointer; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.lang.reflect.Method; + +public class GtkUtilities { + private static final Logger log = LogManager.getLogger(GtkUtilities.class); + + /** + * Initializes Gtk2/3 and returns the desktop scaling factor, usually 1.0 or 2.0 + */ + public static double getScaleFactor() { + try { + GTK gtkHandle = getGtkInstance(); + if (gtkHandle != null && gtkHandle.gtk_init_check(0, null)) { + log.debug("Initialized Gtk"); + + if (gtkHandle instanceof GTK2) { + return getGtk2ScaleFactor((GTK2)gtkHandle); + } else { + return getGtk3ScaleFactor((GTK3)gtkHandle); + } + } else { + log.warn("An error occurred initializing the Gtk library"); + } + } catch(Throwable t) { + log.warn("An error occurred initializing the Gtk library", t); + } + return 1; + } + + private static GTK getGtkInstance() { + log.debug("Finding preferred Gtk version..."); + switch(getGtkMajorVersion()) { + case 2: + return GTK2.INSTANCE; + case 3: + return GTK3.INSTANCE; + default: + log.warn("Not a compatible Gtk version"); + } + return null; + } + + /** + * Get the major version of Gtk (e.g. 2, 3) + * UNIXToolkit is unavailable on Windows or Mac; reflection is required. + * @return Major version if found, zero if not. + */ + private static int getGtkMajorVersion() { + try { + Class toolkitClass = Class.forName("sun.awt.UNIXToolkit"); + Method versionMethod = toolkitClass.getDeclaredMethod("getGtkVersion"); + Enum versionInfo = (Enum)versionMethod.invoke(toolkitClass); + Method numberMethod = versionInfo.getClass().getDeclaredMethod("getNumber"); + int version = ((Integer)numberMethod.invoke(versionInfo)).intValue(); + log.debug("Found Gtk{}", version); + return version; + } catch(Throwable t) { + log.warn("Could not obtain GtkVersion information from UNIXToolkit: {}", t.getMessage()); + } + return 0; + } + + private static double getGtk2ScaleFactor(GTK2 gtk2) { + Pointer display = gtk2.gdk_display_get_default(); + log.debug("Gtk 2.10+ detected, calling \"gdk_screen_get_resolution\""); + Pointer screen = gtk2.gdk_display_get_default_screen(display); + return gtk2.gdk_screen_get_resolution(screen) / 96.0d; + } + + private static double getGtk3ScaleFactor(GTK3 gtk3) { + Pointer display = gtk3.gdk_display_get_default(); + int gtkMinorVersion = gtk3.gtk_get_minor_version(); + if (gtkMinorVersion < 10) { + log.warn("Gtk 3.10+ is required to detect scaling factor, skipping."); + } else if (gtkMinorVersion >= 22) { + log.debug("Gtk 3.22+ detected, calling \"gdk_monitor_get_scale_factor\""); + Pointer monitor = gtk3.gdk_display_get_primary_monitor(display); + return gtk3.gdk_monitor_get_scale_factor(monitor); + } else if (gtkMinorVersion >= 10) { + log.debug("Gtk 3.10+ detected, calling \"gdk_screen_get_monitor_scale_factor\""); + Pointer screen = gtk3.gdk_display_get_default_screen(display); + return gtk3.gdk_screen_get_monitor_scale_factor(screen, 0); + } + return 1; + } + + /** + * Gtk2/Gtk3 wrapper + */ + private interface GTK extends Library { + // Gtk2.0+ + boolean gtk_init_check(int argc, String[] argv); + Pointer gdk_display_get_default(); + Pointer gdk_display_get_default_screen (Pointer display); + } + + private interface GTK3 extends GTK { + GTK3 INSTANCE = Native.load("gtk-3", GTK3.class); + + // Gtk 3.0+ + int gtk_get_minor_version (); + + // Gtk 3.10-3.21 + int gdk_screen_get_monitor_scale_factor (Pointer screen, int monitor_num); + + // Gtk 3.22+ + Pointer gdk_display_get_primary_monitor (Pointer display); + int gdk_monitor_get_scale_factor (Pointer monitor); + } + + private interface GTK2 extends GTK { + GTK2 INSTANCE = Native.load("gtk-x11-2.0", GTK2.class); + + // Gtk 2.1-3.0 + double gdk_screen_get_resolution(Pointer screen); + } +} diff --git a/old code/tray/src/qz/utils/JsonWriter.java b/old code/tray/src/qz/utils/JsonWriter.java new file mode 100755 index 0000000..6dc36b0 --- /dev/null +++ b/old code/tray/src/qz/utils/JsonWriter.java @@ -0,0 +1,151 @@ +/** + * @author Brett B. + * + * Copyright (C) 2019 QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + + +package qz.utils; + +import org.apache.commons.io.Charsets; +import org.apache.commons.io.FileUtils; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; + +/** + * A minimally intrusive JSON writer + */ +public class JsonWriter { + protected static final Logger log = LogManager.getLogger(JsonWriter.class); + + public static boolean write(String path, String data, boolean overwrite, boolean delete) throws IOException, JSONException { + File f = new File(path); + if(!f.getParentFile().exists()) { + log.warn("Warning, the parent folder of {} does not exist, skipping.", path); + return false; + } + + if (data == null) { + log.warn("Data is null, nothing to merge"); + return true; + } + + JSONObject config = f.exists() && f.length() > 0 ? new JSONObject(FileUtils.readFileToString(f, Charsets.UTF_8)) : new JSONObject(); + JSONObject append = new JSONObject(data); + + if (!delete) { + merge(config, append, overwrite); + } else { + remove(config, append); + } + + FileUtils.write(f, config.toString(2), StandardCharsets.UTF_8); + + return true; + } + + public static boolean contains(File path, String data) { + try { + if (!path.exists() || (data == null && data.isEmpty())) { + return false; + } + + String jsonData = FileUtils.readFileToString(path, StandardCharsets.UTF_8); + JSONObject before = new JSONObject(jsonData); + JSONObject after = new JSONObject(jsonData); + merge(after, new JSONObject(data), true); + return before.toString().equals(after.toString()); + } catch(JSONException | IOException ignore) { + return false; + } + } + + /** + * Appends all keys from {@code merger} to {@code base} + * + * @param base Root JSON object to merge into + * @param merger JSON Object of keys to merge + * @param overwrite If existing keys in {@code base} should be overwritten if defined in {@code merger} + */ + private static void merge(JSONObject base, JSONObject merger, boolean overwrite) throws JSONException { + Iterator itr = merger.keys(); + while(itr.hasNext()) { + String key = (String)itr.next(); + + Object baseVal = base.opt(key); + Object mergeVal = merger.opt(key); + + if (baseVal == null) { + //add new key + base.put(key, mergeVal); + } else if (baseVal instanceof JSONObject && mergeVal instanceof JSONObject) { + //deep copy sub-keys + merge((JSONObject)baseVal, (JSONObject)mergeVal, overwrite); + } else if (overwrite) { + //force new key val if existing and allowed + base.put(key, mergeVal); + } else if (baseVal instanceof JSONArray && mergeVal instanceof JSONArray) { + JSONArray baseArr = (JSONArray)baseVal; + JSONArray mergeArr = (JSONArray)mergeVal; + + //lists only merged if not overriding values + for(int i = 0; i < mergeArr.length(); i++) { + //check if value is already in the base array + boolean exists = false; + for(int j = 0; j < baseArr.length(); j++) { + if (baseArr.get(j).equals(mergeArr.get(i))) { + exists = true; + break; + } + } + + if (!exists) { + baseArr.put(mergeArr.get(i)); + } + } + } + } + } + + /** + * Removes all keys in {@code deletion} from {@code base} + * + * @param base Root JSON object to delete from + * @param deletion JSON object of keys to delete + */ + private static void remove(JSONObject base, JSONObject deletion) { + Iterator itr = deletion.keys(); + while(itr.hasNext()) { + String key = (String)itr.next(); + + Object baseVal = base.opt(key); + Object delVal = deletion.opt(key); + + if (baseVal instanceof JSONObject && delVal instanceof JSONObject) { + //only delete sub-keys + remove((JSONObject)baseVal, (JSONObject)delVal); + } else if (baseVal instanceof JSONArray && delVal instanceof JSONArray) { + //only delete elements in list + for(int i = 0; i < ((JSONArray)delVal).length(); i++) { + ((JSONArray)baseVal).remove(((JSONArray)delVal).opt(i)); + } + } else if (baseVal != null) { + //delete entire key + base.remove(key); + } + } + } + +} diff --git a/old code/tray/src/qz/utils/LibUtilities.java b/old code/tray/src/qz/utils/LibUtilities.java new file mode 100755 index 0000000..1fd1725 --- /dev/null +++ b/old code/tray/src/qz/utils/LibUtilities.java @@ -0,0 +1,194 @@ +package qz.utils; + +import com.sun.jna.Platform; +import javafx.application.Application; +import org.usb4java.Loader; +import qz.build.provision.params.Os; +import qz.common.Constants; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Stream; + +/** + * Helper for setting various booth paths for finding native libraries + * e.g. "java.library.path", "jna.boot.library.path" ... etc + */ +public class LibUtilities { + // Files indicating whether or not we can load natives from an external location + private static final String[] INDICATOR_RESOURCES = {"/com/sun/jna/" + Platform.RESOURCE_PREFIX }; + private static final String LIB_DIR = "./libs"; + private static final String MAC_LIB_DIR = "../Frameworks"; + + private static final LibUtilities INSTANCE = new LibUtilities(); + + // Track if libraries are externalized into a "libs" folder + private final boolean externalized; + + // The base library path + private final Path basePath; + + // Common native file extensions, by platform + private static HashMap extensionMap = new HashMap<>(); + static { + extensionMap.put(Os.WINDOWS, new String[]{ "dll" }); + extensionMap.put(Os.MAC, new String[]{ "jnilib", "dylib" }); + extensionMap.put(Os.LINUX, new String[]{ "so" }); + extensionMap.put(Os.UNKNOWN, new String[]{ "so" }); + } + + public LibUtilities() { + this(calculateBasePath(), calculateExternalized()); + } + + public LibUtilities(Path basePath, boolean externalized) { + this.basePath = basePath; + this.externalized = externalized; + } + + public static LibUtilities getInstance() { + return INSTANCE; + } + + public void bind() { + if (externalized) { + bindProperties("jna.boot.library.path", // jna + "jna.library.path", // hid4java + "jssc.boot.library.path" // jssc + ); + bindUsb4Java(); + } + // JavaFX is always externalized + if (Constants.JAVA_VERSION.getMajorVersion() >= 11) { + // Calculate basePath for IDE + Path fxBase = SystemUtilities.isJar()? basePath: + findNativeLib("glass", SystemUtilities.getJarParentPath("../lib").normalize()); + bindProperty("java.library.path", fxBase); // javafx + } + } + + /** + * Search recursively for a native library in the specified path + */ + private static Path findNativeLib(String libName, Path basePath) { + String[] extensions = extensionMap.get(SystemUtilities.getOs()); + String prefix = !SystemUtilities.isWindows() ? "lib" : ""; + List found = new ArrayList<>(); + try (Stream walkStream = Files.walk(basePath)) { + walkStream.filter(p -> p.toFile().isFile()).forEach(f -> { + for(String extension : extensions) { + if (f.getFileName().toString().equals(prefix + libName + "." + extension)) { + found.add(f.getParent()); + } + } + }); + } catch(IOException ignore) {} + return found.size() > 0 ? found.get(0) : null; + } + + /** + * Calculates the base native library path based on the jar path + */ + private static Path calculateBasePath() { + return SystemUtilities.getJarParentPath().resolve( + useFrameworks() ? MAC_LIB_DIR : LIB_DIR + ).normalize(); + } + + /** + * Whether to use the standard "libs" directory + */ + private static boolean useFrameworks() { + return SystemUtilities.isMac() && SystemUtilities.isInstalled(); + } + + private void bindProperties(String ... properties) { + Arrays.stream(properties).forEach(this::bindProperty); + } + + /** + * Binds a system property to the calculated basePath + */ + private void bindProperty(String property) { + bindProperty(property, basePath); + } + + /** + * Binds a system property to the specified basePath + */ + private void bindProperty(String property, Path basePath) { + if(property == null || basePath == null) { + return; + } + if(!property.equals("java.library.path")) { + System.setProperty(property, basePath.toString()); + } else { + // Special case for "java.library.path", used by JavaFX + SystemUtilities.insertPathProperty( + "java.library.path", + basePath.toString(), + "/jni" /* appends to end if not found */ + ); + } + } + + /** + * Using reflection, force usb4java to load from the specified boot path + */ + private void bindUsb4Java() { + try { + // Make usb4java think it's already unzipped it's native resources + Field loaded = Loader.class.getDeclaredField("loaded"); + loaded.setAccessible(true); + loaded.set(Loader.class, true); + + // Expose private functions (getExtraLibName is only needed for Windows) + Method getPlatform = Loader.class.getDeclaredMethod("getPlatform"); + Method getLibName = Loader.class.getDeclaredMethod("getLibName"); + Method getExtraLibName = Loader.class.getDeclaredMethod("getExtraLibName"); + getPlatform.setAccessible(true); + getLibName.setAccessible(true); + getExtraLibName.setAccessible(true); + + // Simulate Loader.load's path calculation + String lib = (String)getLibName.invoke(Loader.class); + String extraLib = (String)getExtraLibName.invoke(Loader.class); + if (extraLib != null) System.load(basePath.resolve(extraLib).toString()); + System.load(basePath.resolve(lib).toString()); + } catch(Throwable ignore) {} + } + + // TODO: Determine fx "libs" or "${basedir}/lib/javafx" for the running jre and remove + private boolean detectJavaFxConflict() { + // If running from the IDE, make sure we're not using the wrong libs + URL url = Application.class.getResource("/" + Application.class.getName().replace('.', '/') + ".class"); + String graphicsJar = url.toString().replaceAll("file:/|jar:", "").replaceAll("!.*", ""); + switch(SystemUtilities.getOs()) { + case WINDOWS: + return !graphicsJar.contains("windows"); + case MAC: + return !graphicsJar.contains("osx") && !graphicsJar.contains("mac"); + default: + return !graphicsJar.contains("linux"); + } + } + + /** + * Detect if the JAR has native resources bundled, if not, we'll assume they've been externalized + */ + private static boolean calculateExternalized() { + for(String resource : INDICATOR_RESOURCES) + if(SystemUtilities.class.getResource(resource) != null) + return false; + + return true; + } +} diff --git a/old code/tray/src/qz/utils/LoggerUtilities.java b/old code/tray/src/qz/utils/LoggerUtilities.java new file mode 100755 index 0000000..11d54c2 --- /dev/null +++ b/old code/tray/src/qz/utils/LoggerUtilities.java @@ -0,0 +1,25 @@ +package qz.utils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class LoggerUtilities { + + /** + * Helper method for parse warnings + * + * @param expectedType Expected entry type + * @param name Option name + * @param actualValue Invalid value passed + */ + public static void optionWarn(Logger log, String expectedType, String name, Object actualValue) { + if (actualValue == null || String.valueOf(actualValue).isEmpty()) { return; } //no need to report an unsupplied value + log.warn("Cannot read {} as a {} for {}, using default", actualValue, expectedType, name); + } + + /** Gets a correctly cast root logger to add appenders on top */ + public static org.apache.logging.log4j.core.Logger getRootLogger() { + return (org.apache.logging.log4j.core.Logger)LogManager.getRootLogger(); + } + +} diff --git a/old code/tray/src/qz/utils/MacUtilities.java b/old code/tray/src/qz/utils/MacUtilities.java new file mode 100755 index 0000000..f86ab4b --- /dev/null +++ b/old code/tray/src/qz/utils/MacUtilities.java @@ -0,0 +1,307 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.utils; + +import com.apple.OSXAdapterWrapper; +import org.apache.commons.io.FileUtils; +import org.dyorgio.jna.platform.mac.*; +import com.github.zafarkhaja.semver.Version; +import com.sun.jna.NativeLong; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.common.TrayManager; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +/** + * Utility class for MacOS specific functions. + * + * @author Tres Finocchiaro + */ +public class MacUtilities { + private static final Logger log = LogManager.getLogger(MacUtilities.class); + private static Dialog aboutDialog; + private static TrayManager trayManager; + private static String bundleId; + private static Boolean jdkSupportsTemplateIcon; + private static boolean templateIconForced = false; + private static boolean sandboxed = System.getenv("APP_SANDBOX_CONTAINER_ID") != null; + + public static void showAboutDialog() { + if (aboutDialog != null) { aboutDialog.setVisible(true); } + } + + public static void showExitPrompt() { + if (trayManager != null) { trayManager.exit(0); } + } + + /** + * Adds a listener to register the Apple "About" dialog to call {@code setVisible()} on the specified Dialog + */ + public static void registerAboutDialog(Dialog aboutDialog) { + MacUtilities.aboutDialog = aboutDialog; + + try { + OSXAdapterWrapper.setAboutHandler(MacUtilities.class, MacUtilities.class.getDeclaredMethod("showAboutDialog")); + } + catch(Exception e) { + e.printStackTrace(); + } + } + + /** + * Calculates CFBundleIdentifier for macOS + * @return + */ + public static String getBundleId() { + if(bundleId == null) { + ArrayList parts = new ArrayList(Arrays.asList(Constants.ABOUT_URL.split("/"))); + for(String part : parts) { + if(part.contains(".")) { + // Try to use this section as the .com, etc + String[] domain = part.toLowerCase(Locale.ENGLISH).split("\\."); + // Convert to reverse-domain syntax + for(int i = domain.length -1; i >= 0; i--) { + // Skip "www", "www2", etc + if(i == 0 && domain[i].startsWith("www")) { + break; + } + bundleId = (bundleId == null ? "" : bundleId) + domain[i] + "."; + } + } + } + } + if(bundleId != null) { + bundleId += Constants.PROPS_FILE; + } else { + bundleId = "io.qz.fallback." + Constants.PROPS_FILE; + } + return bundleId; + } + + /** + * Adds a listener to register the Apple "Quit" to call {@code trayManager.exit(0)} + */ + public static void registerQuitHandler(TrayManager trayManager) { + MacUtilities.trayManager = trayManager; + + try { + OSXAdapterWrapper.setQuitHandler(MacUtilities.class, MacUtilities.class.getDeclaredMethod("showExitPrompt")); + } + catch(Exception e) { + e.printStackTrace(); + } + } + + /** + * Runs a shell command to determine if "Dark" desktop theme is enabled + * @return true if enabled, false if not + */ + public static boolean isDarkDesktop() { + try { + return "Dark".equalsIgnoreCase(NSUserDefaults.standard().stringForKey(new NSString("AppleInterfaceStyle")).toString()); + } catch(Exception e) { + log.warn("An exception occurred obtaining theme information, falling back to command line instead."); + return !ShellUtilities.execute(new String[] {"defaults", "read", "-g", "AppleInterfaceStyle"}, new String[] {"Dark"}, true, true).isEmpty(); + } + } + + public static int getScaleFactor() { + // Java 9+ per JDK-8172962 + if (Constants.JAVA_VERSION.greaterThanOrEqualTo(Version.valueOf("9.0.0"))) { + GraphicsDevice graphicsDevice = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice(); + GraphicsConfiguration graphicsConfig = graphicsDevice.getDefaultConfiguration(); + return (int)graphicsConfig.getDefaultTransform().getScaleX(); + } + // Java 7, 8 + try { + // Use reflection to avoid compile errors on non-macOS environments + Object screen = Class.forName("sun.awt.CGraphicsDevice").cast(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice()); + Method getScaleFactor = screen.getClass().getDeclaredMethod("getScaleFactor"); + Object obj = getScaleFactor.invoke(screen); + if (obj instanceof Integer) { + return ((Integer)obj).intValue(); + } + } catch (Exception e) { + log.warn("Unable to determine screen scale factor. Defaulting to 1.", e); + } + return 1; + } + + /** + * Checks for presence of JDK-8252015 using reflection + */ + public static boolean jdkSupportsTemplateIcon() { + if(jdkSupportsTemplateIcon == null) { + try { + // before JDK-8252015: setNativeImage(long, long, boolean) + // after JDK-8252015: setNativeImage(long, long, boolean, boolean) + Class.forName("sun.lwawt.macosx.CTrayIcon").getDeclaredMethod("setNativeImage", long.class, long.class, boolean.class, boolean.class); + jdkSupportsTemplateIcon = true; + } + catch(ClassNotFoundException | NoSuchMethodException ignore) { + jdkSupportsTemplateIcon = false; + } + } + return jdkSupportsTemplateIcon; + } + + /** + * The human-readable display version of the Mac + */ + public static String getOsDisplayVersion() { + String displayVersion; + String[] command = {"sw_vers"}; + String output = ShellUtilities.executeRaw(command); + if(!output.trim().isEmpty()) { + displayVersion = ""; + String[] lines = output.split("\\n"); + if (lines.length >= 3) { + for(int line = 0; line < 3; line++) { + // Get value after ":", e.g. "ProductName: macOS" + String[] parts = lines[line].split(":", 2); + if (parts.length > 1) { + if (line < 2) { + displayVersion += parts[1].trim() + " "; + } else { + displayVersion += "(" + parts[1].trim() + ")"; + } + } + } + } + } else { + displayVersion = System.getProperty("os.version", "0.0.0"); + } + + return displayVersion; + } + + public static void toggleTemplateIcon(TrayIcon icon) { + // Check if icon has a menu + if (icon.getPopupMenu() == null) { + throw new IllegalStateException("PopupMenu needs to be set on TrayIcon first"); + } + // Check if icon is on SystemTray + if (icon.getImage() == null) { + throw new IllegalStateException("TrayIcon needs to be added on SystemTray first"); + } + // Check if icon is on SystemTray + if (!Arrays.asList(SystemTray.getSystemTray().getTrayIcons()).contains(icon)) { + throw new IllegalStateException("TrayIcon needs to be added on SystemTray first"); + } + + // Prevent second invocation; causes icon to disappear + if(templateIconForced) { + return; + } else { + templateIconForced = true; + } + + try { + Field ptrField = Class.forName("sun.lwawt.macosx.CFRetainedResource").getDeclaredField("ptr"); + ptrField.setAccessible(true); + + Field field = TrayIcon.class.getDeclaredField("peer"); + field.setAccessible(true); + long cTrayIconAddress = ptrField.getLong(field.get(icon)); + + long cPopupMenuAddressTmp = 0; + if (icon.getPopupMenu() != null) { + field = MenuComponent.class.getDeclaredField("peer"); + field.setAccessible(true); + cPopupMenuAddressTmp = ptrField.getLong(field.get(icon.getPopupMenu())); + } + final long cPopupMenuAddress = cPopupMenuAddressTmp; + + final NativeLong statusItem = FoundationUtil.invoke(new NativeLong(cTrayIconAddress), "theItem"); + NativeLong awtView = FoundationUtil.invoke(statusItem, "view"); + final NativeLong image = Foundation.INSTANCE.object_getIvar(awtView, Foundation.INSTANCE.class_getInstanceVariable(FoundationUtil.invoke(awtView, "class"), "image")); + FoundationUtil.invoke(image, "setTemplate:", true); + FoundationUtil.runOnMainThreadAndWait(() -> { + FoundationUtil.invoke(statusItem, "setView:", FoundationUtil.NULL); + NativeLong target; + if (SystemUtilities.getOsVersion().greaterThanOrEqualTo(Version.forIntegers(10, 10))) { + target = FoundationUtil.invoke(statusItem, "button"); + } else { + target = statusItem; + } + FoundationUtil.invoke(target, "setImage:", image); + //FoundationUtil.invoke(statusItem, "setLength:", length); + + if (cPopupMenuAddress != 0) { + FoundationUtil.invoke(statusItem, "setMenu:", FoundationUtil.invoke(new NativeLong(cPopupMenuAddress), "menu")); + } else { + new ActionCallback(() -> { + final ActionListener[] listeners = icon.getActionListeners(); + final int now = (int) System.currentTimeMillis(); + for (int i = 0; i < listeners.length; i++) { + final int iF = i; + SwingUtilities.invokeLater(() -> listeners[iF].actionPerformed(new ActionEvent(icon, now + iF, null))); + } + }).installActionOnNSControl(target); + } + }); + } catch (Throwable ignore) {} + } + + public static void setFocus() { + try { + NSApplication.sharedApplication().activateIgnoringOtherApps(true); + } catch(Throwable t) { + log.warn("Couldn't set focus using JNA, falling back to command line instead"); + ShellUtilities.executeAppleScript("tell application \"System Events\" \n" + + "set frontmost of every process whose unix id is " + UnixUtilities.getProcessId() + " to true \n" + + "end tell"); + } + } + + public static boolean nativeFileCopy(Path source, Path destination) { + Path tempFile = null; + try { + // AppleScript's "duplicate" requires an existing destination + if (!destination.toFile().isDirectory()) { + // To perform this in a single operation in AppleScript, the source and dest + // file names must match. Copy to a temp directory first to retain desired name. + tempFile = Files.createTempDirectory("qz_cert_").resolve(destination.getFileName()); + log.debug("Copying {} to {} to obtain the desired name", source, tempFile); + source = Files.copy(source, tempFile); + destination = destination.getParent(); + } + return ShellUtilities.executeAppleScript( + "tell application \"Finder\" to duplicate " + + "file (POSIX file \"" + source + "\" as alias) " + + "to folder (POSIX file \"" + destination + "\" as alias) " + + "with replacing"); + } catch(Throwable t) { + log.warn("Unable to perform native file copy using AppleScript", t); + } finally { + if(tempFile != null) { + FileUtils.deleteQuietly(tempFile.getParent().toFile()); + } + } + return false; + } + + public static boolean isSandboxed() { + return sandboxed; + } +} diff --git a/old code/tray/src/qz/utils/NetworkUtilities.java b/old code/tray/src/qz/utils/NetworkUtilities.java new file mode 100755 index 0000000..0f80c9b --- /dev/null +++ b/old code/tray/src/qz/utils/NetworkUtilities.java @@ -0,0 +1,220 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.utils; + +import com.sun.jna.platform.win32.Kernel32Util; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; +import java.net.*; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Properties; + +/** + * @author Tres + */ +public class NetworkUtilities { + + private static final Logger log = LogManager.getLogger(NetworkUtilities.class); + + public static int SOCKET_TIMEOUT = 4 * 60 * 1000; + + private static NetworkUtilities instance; + private static String systemName = SystemUtilities.getHostName(); + private static String userName = System.getProperty("user.name"); + + // overridable in preferences, see "networking.hostname", "networking.port" + private static String defaultHostname = "google.com"; + private static int defaultPort = 443; + private String hostname; + private int port; + + private ArrayList devices; + private Device primaryDevice; + private IOException primaryException; + + + private NetworkUtilities(String hostname, int port) { + try { + this.hostname = hostname; + this.port = port; + primaryDevice = new Device(getPrimaryInetAddress(hostname, port), true); + } catch(IOException e) { + log.warn("Unable to determine primary network device", e); + primaryException = e; + } + } + + public static NetworkUtilities getInstance(String hostname, int port) { + // New instance if host or port have changed + if(instance == null || !instance.hostname.equals(hostname) || instance.port != port) { + instance = new NetworkUtilities(hostname, port); + } + + return instance; + } + + public static JSONArray getDevicesJSON(JSONObject params) throws JSONException, IOException { + JSONArray network = new JSONArray(); + + for(Device device : getInstance(params.optString("hostname", defaultHostname), + params.optInt("port", defaultPort)).gatherDevices()) { + network.put(device.toJSON()); + } + return network; + } + + public static JSONObject getDeviceJSON(JSONObject params) throws JSONException, IOException { + Device primary = getInstance(params.optString("hostname", defaultHostname), + params.optInt("port", defaultPort)).primaryDevice; + + if (primary != null) { + return primary.toJSON(); + } + throw instance.primaryException; + } + + private ArrayList gatherDevices() throws SocketException { + if (devices == null) { + devices = new ArrayList<>(); + + Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + while(interfaces.hasMoreElements()) { + NetworkInterface iface = interfaces.nextElement(); + + Device next = new Device(iface); + next.primary = next.equals(primaryDevice); + + devices.add(next); + } + } + + return devices; + } + + private static InetAddress getPrimaryInetAddress(String hostname, int port) throws IOException { + log.info("Initiating a temporary connection to \"{}:{}\" to determine main Network Interface", hostname, port); + + Socket socket = new Socket(); + socket.setSoTimeout(SOCKET_TIMEOUT); + socket.connect(new InetSocketAddress(hostname, port)); + + return socket.getLocalAddress(); + } + + /** + * Sets the defaultHostname and defaultPort by parsing the properties file + */ + public static void setPreferences(Properties props) { + String hostName = props.getProperty("networking.hostname"); + if(hostName != null && !hostName.trim().isEmpty()) { + defaultHostname = hostName; + } + + String port = props.getProperty("networking.port"); + if(port != null && !port.trim().isEmpty()) { + try { + defaultPort = Integer.parseInt(port); + } catch(Exception parseError) { + log.warn("Unable to parse \"networking.port\"", parseError); + } + } + } + + private static class Device { + + String mac, ip, id, name; + ArrayList ip4, ip6; + boolean up, primary; + + Device(InetAddress inet, boolean primary) throws SocketException { + this(NetworkInterface.getByInetAddress(inet)); + ip = inet.getHostAddress(); //use primary + this.primary = primary; + } + + Device(NetworkInterface iface) { + try { mac = ByteUtilities.bytesToHex(iface.getHardwareAddress()); } catch(Exception ignore) {} + try { up = iface.isUp(); } catch(SocketException ignore) {} + + ip4 = new ArrayList<>(); + ip6 = new ArrayList<>(); + + name = iface.getDisplayName(); + + Enumeration addresses = iface.getInetAddresses(); + while(addresses.hasMoreElements()) { + InetAddress address = addresses.nextElement(); + if (address instanceof Inet4Address) { + ip4.add(address.getHostAddress()); + } else if (address instanceof Inet6Address) { + String ip6 = address.getHostAddress(); + if (ip6.contains("%")) { + String[] split = ip6.split("%"); + this.ip6.add(split[0]); + id = split[split.length - 1]; + } else { + this.ip6.add(ip6); + } + } else { + log.warn("InetAddress type {} unsupported", address.getClass().getName()); + } + + if (ip6.size() > 0) { + ip = ip6.get(0); + } else if (ip4.size() > 0) { + ip = ip4.get(0); + } + } + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof Device) { + Device device = (Device)obj; + boolean ip4match = ip4 != null && ip4.containsAll(device.ip4); + boolean ip6match = ip6 != null && ip6.containsAll(device.ip6); + return mac != null && mac.equals(device.mac) && ip4match && ip6match; + } + + return false; + } + + static JSONArray toJSONArray(ArrayList list) { + if (list != null && list.size() > 0) { + JSONArray array = new JSONArray(); + list.forEach(array::put); + return array; + } + + return null; + } + + JSONObject toJSON() throws JSONException { + return new JSONObject() + .put("name", name) + .put("mac", mac) + .put("ip", ip) + .put("ip4", toJSONArray(ip4)) + .put("ip6", toJSONArray(ip6)) + .put("primary", primary) + .put("up", up) + .put("hostname", systemName) + .put("username", userName) + .put("id", id); + } + } + +} diff --git a/old code/tray/src/qz/utils/PrefsSearch.java b/old code/tray/src/qz/utils/PrefsSearch.java new file mode 100755 index 0000000..3c03b6f --- /dev/null +++ b/old code/tray/src/qz/utils/PrefsSearch.java @@ -0,0 +1,117 @@ +package qz.utils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.installer.certificate.CertificateManager; +import qz.installer.certificate.KeyPairWrapper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +import static qz.installer.certificate.KeyPairWrapper.Type.CA; +import static qz.installer.certificate.KeyPairWrapper.Type.SSL; + +/** + * Convenience class for searching for preferences on a user, app and System.getProperty(...) level + */ +public class PrefsSearch { + private static final Logger log = LogManager.getLogger(PrefsSearch.class); + private static Properties appProps = null; + + private static String getProperty(String[] names, String defaultVal, boolean searchSystemProperties, Properties ... propArray) { + String returnVal; + + // If none are provided, ensure we have some types of properties to iterate over + if(propArray.length == 0) { + if(appProps == null) { + appProps = CertificateManager.loadProperties(new KeyPairWrapper(SSL), new KeyPairWrapper(CA)); + } + propArray = new Properties[]{ appProps }; + } + + for(String n : names) { + // First, honor System property + if (searchSystemProperties && (returnVal = System.getProperty(n)) != null) { + log.info("Picked up system property {}={}", n, returnVal); + return returnVal; + } + + for(Properties props : propArray) { + // Second, honor properties file(s) + if (props != null) { + if ((returnVal = props.getProperty(n)) != null) { + log.info("Picked up property {}={}", n, returnVal); + return returnVal; + } + } + } + } + + // Last, return default property + return defaultVal; + } + + /* + * Typed String[] helper implementations + */ + private static int getInt(String[] names, int defaultVal, boolean searchSystemProperties, Properties ... propsArray) { + try { + return Integer.parseInt(getProperty(names, "", searchSystemProperties, propsArray)); + } catch(NumberFormatException ignore) {} + return defaultVal; + } + + private static boolean getBoolean(String[] names, boolean defaultVal, boolean searchSystemProperties, Properties ... propsArray) { + return Boolean.parseBoolean(getProperty(names, "" + defaultVal, searchSystemProperties, propsArray)); + } + + /* + * Typed ArgValue implementations + */ + public static String getString(ArgValue argValue, boolean searchSystemProperties, Properties ... propsArray) { + return getProperty(argValue.getMatches(), (String)argValue.getDefaultVal(), searchSystemProperties, propsArray); + } + + public static int getInt(ArgValue argValue, boolean searchSystemProperties, Properties ... propsArray) { + return getInt(argValue.getMatches(), (Integer)argValue.getDefaultVal(), searchSystemProperties, propsArray); + } + + public static boolean getBoolean(ArgValue argValue, boolean searchSystemProperties, Properties ... propsArray) { + return getBoolean(argValue.getMatches(), (Boolean)argValue.getDefaultVal(), searchSystemProperties, propsArray); + } + + /* + * Typed ArgValue implementations (searchSystemProperties = true) + */ + public static String getString(ArgValue argValue, Properties ... propsArray) { + return getString(argValue, true, propsArray); + } + + public static int getInt(ArgValue argValue, Properties ... propsArray) { + return getInt(argValue, true, propsArray); + } + + public static List getIntegerArray(ArgValue argValue, Properties ... propsArray) { + return parseIntegerArray(getString(argValue, propsArray)); + } + + public static List parseIntegerArray(String commaSeparated) { + List parsed = new ArrayList<>(); + try { + if (commaSeparated != null && !commaSeparated.isEmpty()) { + String[] split = commaSeparated.split(","); + for(String item : split) { + parsed.add(Integer.parseInt(item)); + } + } + } catch(NumberFormatException nfe) { + log.warn("Failed parsing {} as a valid integer array", commaSeparated, nfe); + } + return parsed; + } + + public static boolean getBoolean(ArgValue argValue, Properties ... propsArray) { + return getBoolean(argValue, true, propsArray); + } +} diff --git a/old code/tray/src/qz/utils/PrintingUtilities.java b/old code/tray/src/qz/utils/PrintingUtilities.java new file mode 100755 index 0000000..061543c --- /dev/null +++ b/old code/tray/src/qz/utils/PrintingUtilities.java @@ -0,0 +1,298 @@ +package qz.utils; + +import com.sun.jna.platform.win32.*; +import org.apache.commons.pool2.impl.GenericKeyedObjectPool; +import org.apache.commons.ssl.Base64; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.eclipse.jetty.websocket.api.Session; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.communication.WinspoolEx; +import qz.printer.PrintOptions; +import qz.printer.PrintOutput; +import qz.printer.PrintServiceMatcher; +import qz.printer.action.PrintProcessor; +import qz.printer.action.ProcessorFactory; +import qz.printer.info.NativePrinter; +import qz.printer.status.CupsUtils; +import qz.ws.PrintSocketClient; + +import javax.print.PrintException; +import java.awt.print.PrinterAbortException; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +public class PrintingUtilities { + + private static final Logger log = LogManager.getLogger(PrintingUtilities.class); + + private static GenericKeyedObjectPool processorPool; + + + private PrintingUtilities() {} + + public enum Type { + PIXEL, RAW + } + + public enum Format { + COMMAND, DIRECT, HTML, IMAGE, PDF + } + + /** + * TODO: Move this to a dedicated class + */ + public enum Flavor { + BASE64, FILE, HEX, PLAIN, XML; + + // TODO: Refactor DeviceUtilities to use optString("flavor") instead of optString("type") + @Deprecated + public static Flavor parse(String value, Flavor fallbackIfEmpty) { + if(value != null && !value.isEmpty()) { + return Flavor.valueOf(value.toUpperCase(Locale.ENGLISH)); + } + return fallbackIfEmpty; + } + + public static Flavor parse(JSONObject json, Flavor fallbackIfEmpty) { + return parse(json.optString("flavor", ""), fallbackIfEmpty); + } + + public String toString(byte[] bytes) { + return ByteUtilities.toString(this, bytes); + } + + public byte[] read(String data) throws IOException { + return read(data, null); + } + + public byte[] read(String data, String xmlTag) throws IOException { + try { + switch(this) { + case BASE64: + return Base64.decodeBase64(data); + case FILE: + return FileUtilities.readRawFile(data); + case HEX: + return ByteUtilities.hexStringToByteArray(data.trim()); + case XML: + // Assume base64 encoded string inside the specified XML tag + return Base64.decodeBase64(FileUtilities.readXMLFile(data, xmlTag).getBytes(StandardCharsets.UTF_8)); + case PLAIN: + default: + // Reading "plain" data is only supported through JSON/websocket, so we can safely assume it's always UTF8 + return data.getBytes(StandardCharsets.UTF_8); + } + } catch(Exception e) { + log.warn("An error occurred parsing data from " + this.name(), e); + throw new IOException("Error parsing data from " + this.name()); + } + } + } + + public static Type getPrintType(JSONObject data) { + if (data == null) { + return Type.RAW; + } else { + return Type.valueOf(data.optString("type", "RAW").toUpperCase(Locale.ENGLISH)); + } + } + + public static Format getPrintFormat(Type type, JSONObject data) { + //Check for RAW type to coerce COMMAND format handling + if (type == Type.RAW) { + return Format.COMMAND; + } else { + return Format.valueOf(data.optString("format", "COMMAND").toUpperCase(Locale.ENGLISH)); + } + } + + public synchronized static PrintProcessor getPrintProcessor(Format format) { + try { + if (processorPool == null) { + processorPool = new GenericKeyedObjectPool<>(new ProcessorFactory()); + + long memory = Runtime.getRuntime().maxMemory() / 1000000; + if (memory < Constants.MEMORY_PER_PRINT) { + log.warn("Memory available is less than minimum required ({}/{} MB)", memory, Constants.MEMORY_PER_PRINT); + } + if (memory < Long.MAX_VALUE) { + int maxInst = Math.max(1, (int)(memory / Constants.MEMORY_PER_PRINT)); + log.debug("Allowing {} simultaneous processors based on memory available ({} MB)", maxInst, memory); + processorPool.setMaxTotal(maxInst); + processorPool.setMaxTotalPerKey(maxInst); + } + } + + log.trace("Waiting for processor, {}/{} already in use", processorPool.getNumActive(), processorPool.getMaxTotal()); + return processorPool.borrowObject(format); + } + catch(Exception e) { + throw new IllegalArgumentException(String.format("Unable to find processor for %s type", format.name())); + } + } + + /** + * Version 2.1 introduced the flavor attribute to apply better control on raw data. + * Essentially format became flavor, type become format, and type was rewritten. + * Though a few exceptions exist due to the way additional raw options used to be handled. + *

+ * This method will take the data object, and if it uses any old terminology it will update the value to the new set. + * + * @param dataArr JSONArray of printData, will update any data values by reference + */ + private static void convertVersion(JSONArray dataArr) throws JSONException { + for(int i = 0; i < dataArr.length(); i++) { + JSONObject data = dataArr.optJSONObject(i); + if (data == null) { data = new JSONObject(); } + + if (!data.isNull("flavor")) { return; } //flavor exists only in new version, no need to convert any data + + if (!data.isNull("format")) { + String format = data.getString("format").toUpperCase(Locale.ENGLISH); + if (Arrays.asList("BASE64", "FILE", "HEX", "PLAIN", "XML").contains(format)) { + data.put("flavor", format); + data.remove("format"); + } + } + + if (!data.isNull("type")) { + String type = data.getString("type").toUpperCase(Locale.ENGLISH); + if (Arrays.asList("HTML", "IMAGE", "PDF").contains(type)) { + data.put("type", "PIXEL"); + data.put("format", type); + } + } + } + } + + public static void releasePrintProcessor(PrintProcessor processor) { + try { + log.trace("Returning processor back to pool"); + processorPool.returnObject(processor.getFormat(), processor); + } + catch(Exception ignore) {} + } + + /** + * Determine print variables and send data to printer + * + * @param session WebSocket session + * @param UID ID of call from web API + * @param params Params of call from web API + */ + public static void processPrintRequest(Session session, String UID, JSONObject params) throws JSONException { + JSONArray printData = params.getJSONArray("data"); + convertVersion(printData); + + // grab first data object to determine type for entire set + JSONObject firstData = printData.optJSONObject(0); + Type type = getPrintType(firstData); + Format format = getPrintFormat(type, firstData); + + PrintProcessor processor = PrintingUtilities.getPrintProcessor(format); + log.debug("Using {} to print", processor.getClass().getName()); + + try { + PrintOutput output = new PrintOutput(params.optJSONObject("printer")); + PrintOptions options = new PrintOptions(params.optJSONObject("options"), output, format); + + if(type != Type.RAW && !output.isSetService()) { + throw new Exception(String.format("%s cannot print to a raw %s", type, output.isSetFile() ? "file" : "host")); + } + + processor.parseData(params.getJSONArray("data"), options); + processor.print(output, options); + log.info("Printing complete"); + + PrintSocketClient.sendResult(session, UID, null); + } + catch(PrinterAbortException e) { + log.warn("Printing cancelled"); + PrintSocketClient.sendError(session, UID, "Printing cancelled"); + } + catch(Exception e) { + log.error("Failed to print", e); + PrintSocketClient.sendError(session, UID, e); + } + finally { + PrintingUtilities.releasePrintProcessor(processor); + } + } + + public static void cancelJobs(Session session, String UID, JSONObject params) { + try { + NativePrinter printer = PrintServiceMatcher.matchPrinter(params.getString("printerName")); + if (printer == null) { + throw new PrintException("Printer \"" + params.getString("printerName") + "\" not found"); + } + int paramJobId = params.optInt("jobId", -1); + ArrayList jobIds = getActiveJobIds(printer); + + if (paramJobId >= 0) { + if (jobIds.contains(paramJobId)) { + jobIds.clear(); + jobIds.add(paramJobId); + } else { + String error = "Job# " + paramJobId + " is not part of the '" + printer.getName() + "' print queue"; + log.error(error); + PrintSocketClient.sendError(session, UID, error); + return; + } + } + log.info("Canceling {} jobs from {}", jobIds.size(), printer.getName()); + + for(int jobId : jobIds) { + cancelJobById(jobId, printer); + } + } + catch(JSONException | Win32Exception | PrintException e) { + log.error("Failed to cancel jobs", e); + PrintSocketClient.sendError(session, UID, e); + } + } + + private static void cancelJobById(int jobId, NativePrinter printer) { + if (SystemUtilities.isWindows()) { + WinNT.HANDLEByReference phPrinter = getWmiPrinter(printer); + // TODO: Change to "Winspool" when JNA 5.14.0+ is bundled + if (!WinspoolEx.INSTANCE.SetJob(phPrinter.getValue(), jobId, 0, null, WinspoolEx.JOB_CONTROL_DELETE)) { + Win32Exception e = new Win32Exception(Kernel32.INSTANCE.GetLastError()); + log.warn("Job deletion error for job#{}, {}", jobId, e); + } + } else { + CupsUtils.cancelJob(jobId); + } + } + + private static ArrayList getActiveJobIds(NativePrinter printer) { + if (SystemUtilities.isWindows()) { + WinNT.HANDLEByReference phPrinter = getWmiPrinter(printer); + Winspool.JOB_INFO_1[] jobs = WinspoolUtil.getJobInfo1(phPrinter); + ArrayList jobIds = new ArrayList<>(); + // Blindly add all jobs despite Microsoft's API claiming otherwise + // See also: https://github.com/qzind/tray/issues/1305 + for(Winspool.JOB_INFO_1 job : jobs) { + jobIds.add(job.JobId); + } + return jobIds; + } else { + return CupsUtils.listJobs(printer.getPrinterId()); + } + } + + private static WinNT.HANDLEByReference getWmiPrinter(NativePrinter printer) throws Win32Exception { + WinNT.HANDLEByReference phPrinter = new WinNT.HANDLEByReference(); + // TODO: Change to "Winspool" when JNA 5.14.0+ is bundled + if (!WinspoolEx.INSTANCE.OpenPrinter(printer.getName(), /*out*/ phPrinter, null)) { + throw new Win32Exception(Kernel32.INSTANCE.GetLastError()); + } + return phPrinter; + } +} diff --git a/old code/tray/src/qz/utils/SerialUtilities.java b/old code/tray/src/qz/utils/SerialUtilities.java new file mode 100755 index 0000000..fbe0bc1 --- /dev/null +++ b/old code/tray/src/qz/utils/SerialUtilities.java @@ -0,0 +1,279 @@ +package qz.utils; + +import jssc.SerialPort; +import jssc.SerialPortException; +import jssc.SerialPortList; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.eclipse.jetty.websocket.api.Session; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.communication.SerialIO; +import qz.communication.SerialOptions; +import qz.ws.PrintSocketClient; +import qz.ws.SocketConnection; +import qz.ws.StreamEvent; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; + +import static jssc.SerialPort.*; + +/** + * @author Tres + */ +public class SerialUtilities { + + private static final Logger log = LogManager.getLogger(SerialUtilities.class); + + private static final List VALID_BAUD = Arrays.asList(BAUDRATE_110, + BAUDRATE_300, + BAUDRATE_600, + BAUDRATE_1200, + BAUDRATE_2400, + BAUDRATE_4800, + BAUDRATE_9600, + BAUDRATE_14400, + BAUDRATE_19200, + BAUDRATE_38400, + BAUDRATE_57600, + BAUDRATE_115200, + BAUDRATE_128000, + BAUDRATE_256000); + + + /** + * @return Array of serial ports available on the attached system. + */ + public static String[] getSerialPorts() { + return SerialPortList.getPortNames(); + } + + /** + * @return JSON array of {@code getSerialPorts()} result. + */ + public static JSONArray getSerialPortsJSON() { + String[] ports = getSerialPorts(); + JSONArray portJSON = new JSONArray(); + + for(String name : ports) { + portJSON.put(name); + } + + return portJSON; + } + + + /** + * Parses the SerialPort's {@code DATABITS_x} value that corresponds with the provided {@code data}. + * + * @param data Data bits value to parse + * @return The passed data bits value as a {@code SerialPort} constant value if valid, or -1 if invalid; + */ + public static int parseDataBits(String data) { + data = data.trim(); + + switch(data) { + case "0": + log.trace("Parsed serial setting: 0 (Auto)"); + return 0; + case "5": + log.trace("Parsed serial setting: DATABITS_5"); + return SerialPort.DATABITS_5; + case "6": + log.trace("Parsed serial setting: DATABITS_6"); + return SerialPort.DATABITS_6; + case "7": + log.trace("Parsed serial setting: DATABITS_7"); + return SerialPort.DATABITS_7; + case "8": + case "": + log.trace("Parsed serial setting: DATABITS_8"); + return SerialPort.DATABITS_8; + default: + log.error("Data bits value of {} not supported", data); + return -1; + } + } + + /** + * Parses the SerialPort's {@code STOPBITS_x} value that corresponds with the provided {@code stop}. + * + * @param stop Stop bits value to parse + * @return The passed stop bits value as a {@code SerialPort} constant value if valid, or -1 if invalid; + */ + public static int parseStopBits(String stop) { + stop = stop.trim(); + + switch(stop) { + case "0": + log.trace("Parsed serial setting: 0 (Auto)"); + return 0; + case "": + case "1": + log.trace("Parsed serial setting: STOPBITS_1"); + return SerialPort.STOPBITS_1; + case "2": + log.trace("Parsed serial setting: STOPBITS_2"); + return SerialPort.STOPBITS_2; + case "1.5": + case "1_5": + log.trace("Parsed serial setting: STOPBITS_1_5"); + return SerialPort.STOPBITS_1_5; + default: + log.error("Stop bits value of {} could not be parsed", stop); + return -1; + } + } + + /** + * Parses the SerialPort's {@code FLOWCONTROL_x} value that corresponds with the provided {@code control}. + * + * @param control Flow control value to parse + * @return The passed flow control value as a {@code SerialPort} constant value if valid, or -1 if invalid; + */ + public static int parseFlowControl(String control) { + control = control.trim().toLowerCase(Locale.ENGLISH); + + switch(control) { + case "auto": + log.trace("Parsed serial setting: Auto"); + return 0; + case "": + case "n": + case "none": + log.trace("Parsed serial setting: FLOWCONTROL_NONE"); + return SerialPort.FLOWCONTROL_NONE; + case "xonxoff_in": + log.trace("Parsed serial setting: FLOWCONTROL_XONXOFF_IN"); + return SerialPort.FLOWCONTROL_XONXOFF_IN; + case "xonxoff_out": + log.trace("Parsed serial setting: FLOWCONTROL_XONXOFF_OUT"); + return SerialPort.FLOWCONTROL_XONXOFF_OUT; + case "x": + case "xonxoff": + log.trace("Parsed serial setting: FLOWCONTROL_XONXOFF_INOUT"); + return SerialPort.FLOWCONTROL_XONXOFF_IN | SerialPort.FLOWCONTROL_XONXOFF_OUT; + case "rtscts_in": + log.trace("Parsed serial setting: FLOWCONTROL_RTSCTS_IN"); + return SerialPort.FLOWCONTROL_RTSCTS_IN; + case "rtscts_out": + log.trace("Parsed serial setting: FLOWCONTROL_RTSCTS_OUT"); + return SerialPort.FLOWCONTROL_RTSCTS_OUT; + case "p": + case "rtscts": + log.trace("Parsed serial setting: FLOWCONTROL_RTSCTS_INOUT"); + return SerialPort.FLOWCONTROL_RTSCTS_IN | SerialPort.FLOWCONTROL_RTSCTS_OUT; + default: + log.error("Flow control value of {} could not be parsed", control); + return -1; + } + } + + /** + * Parses the SerialPort's {@code PARITY_x} value that corresponds with the provided {@code parity}. + * + * @param parity Parity value to parse + * @return The passed parity value as a {@code SerialPort} constant value if valid, or -1 if invalid. + */ + public static int parseParity(String parity) { + parity = parity.trim().toLowerCase(Locale.ENGLISH); + + switch(parity) { + case "auto": + log.trace("Parsed serial setting: Auto"); + return 0; + case "": + case "n": + case "none": + log.trace("Parsed serial setting: PARITY_NONE"); + return SerialPort.PARITY_NONE; + case "e": + case "even": + log.trace("Parsed serial setting: PARITY_EVEN"); + return SerialPort.PARITY_EVEN; + case "o": + case "odd": + log.trace("Parsed serial setting: PARITY_ODD"); + return SerialPort.PARITY_ODD; + case "m": + case "mark": + log.trace("Parsed serial setting: PARITY_MARK"); + return SerialPort.PARITY_MARK; + case "s": + case "space": + log.trace("Parsed serial setting: PARITY_SPACE"); + return SerialPort.PARITY_SPACE; + default: + log.error("Parity value of {} not supported", parity); + return -1; + } + } + + /** + * Parses the SerialPort's {@code BAUDRATE_x} value that corresponds with the provided {@code rate}. + * + * @param rate Baud rate to parse + * @return The passed baud rate as a {@code SerialPort} constant value if valid, or -1 if invalid. + */ + public static int parseBaudRate(String rate) { + int baud = -1; + + if (rate.trim().isEmpty()) { + baud = SerialPort.BAUDRATE_9600; + } else { + try { baud = Integer.decode(rate.trim()); } catch(NumberFormatException ignore) {} + } + + if (baud == 0) { + log.trace("Parsed serial setting: 0 (Auto)"); + } else if (VALID_BAUD.contains(baud)) { + log.trace("Parsed serial setting: BAUDRATE_{}", baud); + } else { + log.error("Baud rate of {} not supported", rate); + baud = -1; + } + + return baud; + } + + + public static void setupSerialPort(final Session session, String UID, SocketConnection connection, JSONObject params) throws JSONException { + final String portName = params.getString("port"); + if (connection.getSerialPort(portName) != null) { + PrintSocketClient.sendError(session, UID, String.format("Serial port [%s] is already open.", portName)); + return; + } + + try { + SerialOptions props = new SerialOptions(params.optJSONObject("options"), true); + final SerialIO serial = new SerialIO(portName, connection); + + if (serial.open(props)) { + connection.addSerialPort(portName, serial); + + //apply listener here, so we can send all replies to the browser + serial.applyPortListener(spe -> { + String output = serial.processSerialEvent(spe); + + if (output != null) { + log.debug("Received serial output: {}", output); + StreamEvent event = new StreamEvent(StreamEvent.Stream.SERIAL, StreamEvent.Type.RECEIVE) + .withData("portName", portName).withData("output", output); + PrintSocketClient.sendStream(session, event, serial); + } + }); + + PrintSocketClient.sendResult(session, UID, null); + } else { + PrintSocketClient.sendError(session, UID, String.format("Unable to open serial port [%s]", portName)); + } + } + catch(SerialPortException e) { + PrintSocketClient.sendError(session, UID, e); + } + } + +} diff --git a/old code/tray/src/qz/utils/ShellUtilities.java b/old code/tray/src/qz/utils/ShellUtilities.java new file mode 100755 index 0000000..d7821e4 --- /dev/null +++ b/old code/tray/src/qz/utils/ShellUtilities.java @@ -0,0 +1,319 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.utils; + +import org.apache.commons.io.Charsets; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.awt.*; +import java.io.*; +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.*; + +/** + * Utility class for managing all {@code Runtime.exec(...)} functions. + * + * @author Tres Finocchiaro + */ +public class ShellUtilities { + + private static final Logger log = LogManager.getLogger(ShellUtilities.class); + + // Display envp values in errors and console logs + private static boolean debugEnvp = false; + + // Shell environment overrides. null = don't override + private static Map env = new HashMap<>(System.getenv()); + @Deprecated /* TODO: Make private, use getEnvp() instead */ + public static String[] envp = getEnvp(); + + // Make sure all shell calls are LANG=en_US.UTF-8 + static { + switch(SystemUtilities.getOs()) { + case WINDOWS: + break; + case MAC: + // Enable LANG overrides + addEnvp("SOFTWARE", ""); + default: + // Functional equivalent of "export LANG=en_US.UTF-8" + addEnvp("LANG", "C"); + } + } + + public static boolean elevateCopy(Path source, Path dest) { + source = source.toAbsolutePath().normalize(); + dest = dest.toAbsolutePath().normalize(); + + if(SystemUtilities.isWindows()) { + // JNA/Explorer will prompt if insufficient access + if(!WindowsUtilities.nativeFileCopy(source, dest)) { + // Fallback to a powershell trick + return WindowsUtilities.elevatedFileCopy(source, dest); + } + return true; + } else if(SystemUtilities.isMac()){ + // JNA/Finder will prompt if insufficient access + return MacUtilities.nativeFileCopy(source, dest); + } else { + // No reliable JNA method; Use pkexec/gksu/etc + return UnixUtilities.elevatedFileCopy(source, dest); + } + } + + public static boolean execute(String... commandArray) { + return execute(commandArray, null, false); + } + + public static boolean execute(String[] commandArray, boolean silent) { + return execute(commandArray, null, silent); + } + + public static boolean execute(String[] commandArray, File workingDir) { + return execute(commandArray, workingDir, false); + } + + /** + * Executes a synchronous shell command and returns true if the {@code Process.exitValue()} is {@code 0}. + * + * @param commandArray array of command pieces to supply to the shell environment to e executed as a single command + * @param workingDir working directory to start the process from + * @param silent Specify whether to suppress the command from the log files + * @return {@code true} if {@code Process.exitValue()} is {@code 0}, otherwise {@code false}. + */ + public static boolean execute(String[] commandArray, File workingDir, boolean silent) { + if (!silent) { + log.debug("Executing: {}", Arrays.toString(commandArray)); + } + try { + // Create and execute our new process + Process p = Runtime.getRuntime().exec(commandArray, envp, workingDir); + // Consume output to prevent deadlock + while (p.getInputStream().read() != -1) {} + while (p.getErrorStream().read() != -1) {} + p.waitFor(); + return p.exitValue() == 0; + } + catch(InterruptedException ex) { + log.warn("InterruptedException waiting for a return value: {} envp: {}", Arrays.toString(commandArray), envpToString(), ex); + } + catch(IOException ex) { + log.error("IOException executing: {} envp: {}", Arrays.toString(commandArray), envpToString(), ex); + } + + return false; + } + + /** + * Executes a synchronous shell command and return the result. + * + * @param commandArray array of shell commands to execute + * @param searchFor array of return values to look for, case sensitivity matters + * @return The first matching string value + */ + public static String execute(String[] commandArray, String[] searchFor) { + return execute(commandArray, searchFor, true, false); + } + + /** + * Executes a synchronous shell command and return the result. + * + * @param commandArray array of shell commands to execute + * @param searchFor array of return values to look for, or {@code null} + * to return the first line of standard output + * @param caseSensitive whether or not to perform case-sensitive search + * @return The first matching an element of {@code searchFor}, unless + * {@code searchFor} is null ,then the first line of standard output + */ + public static String execute(String[] commandArray, String[] searchFor, boolean caseSensitive, boolean silent) { + if (!silent) { + log.debug("Executing: {}", Arrays.toString(commandArray)); + } + BufferedReader stdInput = null; + try { + // Create and execute our new process + Process p = Runtime.getRuntime().exec(commandArray, envp); + stdInput = new BufferedReader(new InputStreamReader(p.getInputStream(), Charsets.UTF_8)); + String s; + while((s = stdInput.readLine()) != null) { + if (searchFor == null) { + return s.trim(); + } + for(String search : searchFor) { + if (caseSensitive) { + if (s.contains(search.trim())) { + return s.trim(); + } + } else { + if (s.toLowerCase(Locale.ENGLISH).contains(search.toLowerCase(Locale.ENGLISH).trim())) { + return s.trim(); + } + } + } + } + } + catch(IOException ex) { + log.error("IOException executing: {} envp: {}", Arrays.toString(commandArray), envpToString(), ex); + } + finally { + if (stdInput != null) { + try { stdInput.close(); } catch(Exception ignore) {} + } + } + + return ""; + } + + public static String executeRaw(String ... commandArray) { + return executeRaw(commandArray, false); + } + + /** + * Executes a synchronous shell command and return the raw character result. + * + * @param commandArray array of shell commands to execute + * @return The entire raw standard output of command + */ + public static String executeRaw(String[] commandArray, boolean silent) { + if(!silent) { + log.debug("Executing: {}", Arrays.toString(commandArray)); + } + + InputStreamReader in = null; + try { + Process p = Runtime.getRuntime().exec(commandArray, envp); + in = new InputStreamReader(p.getInputStream(), Charsets.UTF_8); + StringBuilder out = new StringBuilder(); + int c; + while((c = in.read()) != -1) + out.append((char)c); + + return out.toString(); + } + catch(IOException ex) { + if(!silent) { + log.error("IOException executing: {} envp: {}", Arrays.toString(commandArray), envpToString(), ex); + } + } + finally { + if (in != null) { + try { in.close(); } catch(Exception ignore) {} + } + } + + return ""; + } + + /** + * Gets the computer's "hostname" from command line + * + * This should only be used as a fallback for when JNA is not available, + * see SystemUtilities.getHostName() instead. + */ + static String getHostName() { + return execute(new String[] {"hostname"}, new String[] {""}); + } + + /** + * Checks that the currently running OS is Apple and executes a native + * AppleScript macro against the OS. Returns true if the + * {@code Process.exitValue()} is {@code 0}. + * + * @param scriptBody AppleScript to execute + * @return true if the {@code Process.exitValue()} is {@code 0}. + */ + public static boolean executeAppleScript(String scriptBody) { + if (!SystemUtilities.isMac()) { + log.error("AppleScript can only be invoked from Apple OS"); + return false; + } + + return execute("osascript", "-e", scriptBody); + } + + public static void browseAppDirectory() { + browseDirectory(SystemUtilities.getJarParentPath()); + } + + public static void browseDirectory(String directory) { + browseDirectory(new File(directory)); + } + + public static void browseDirectory(Path path) { + browseDirectory(path.toFile()); + } + + public static void browseDirectory(File directory) { + try { + if (!SystemUtilities.isMac()) { + Desktop.getDesktop().open(directory); + } else { + // Mac tries to open the .app rather than browsing it. Instead, pass a child to select it in finder + File[] files = directory.listFiles(); + if (files != null && files.length > 0) { + try { + // Use browseFileDirectory (JDK9+) via reflection + Method m = Desktop.class.getDeclaredMethod("browseFileDirectory", File.class); + m.invoke(Desktop.getDesktop(), files[0].getCanonicalFile()); + } + catch(ReflectiveOperationException e) { + // Fallback to open -R + ShellUtilities.execute("open", "-R", files[0].getCanonicalPath()); + } + } + } + } + catch(IOException io) { + if (SystemUtilities.isLinux()) { + // Fallback on xdg-open for Linux + ShellUtilities.execute("xdg-open", directory.getPath()); + } + } + } + + /** + * Provides fast envp manipulation for starting processes with additional environmental variables + * + * @param paired Pairs of values, e.g. { "FOO", "BAR" } where the environment will set FOO=BAR + * @return + */ + public static synchronized String[] addEnvp(Object ... paired) { + if(paired.length % 2 != 0) { + throw new UnsupportedOperationException("Values must be provided in pairs"); + } + + for(int i = 0; i < paired.length / 2; i++) { + env.put(paired[2 * i].toString(), paired[2 * i + 1].toString()); + } + envp = null; + return getEnvp(); + } + + public static synchronized String[] getEnvp() { + if(envp == null) { + String[] temp = new String[env.size()]; + int i = 0; + for(Map.Entry o : env.entrySet()) + temp[i++] = o.getKey() + "=" + o.getValue(); + envp = temp; + } + return envp; + } + + public static String envpToString() { + if(debugEnvp) { + return Arrays.toString(envp); + } + return "(suppressed)"; + } +} diff --git a/old code/tray/src/qz/utils/SocketUtilities.java b/old code/tray/src/qz/utils/SocketUtilities.java new file mode 100755 index 0000000..ebf2f94 --- /dev/null +++ b/old code/tray/src/qz/utils/SocketUtilities.java @@ -0,0 +1,80 @@ +package qz.utils; + +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.eclipse.jetty.websocket.api.Session; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.communication.SocketIO; +import qz.ws.PrintSocketClient; +import qz.ws.SocketConnection; +import qz.ws.StreamEvent; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +public class SocketUtilities { + + private static final Logger log = LogManager.getLogger(SocketUtilities.class); + + public static void setupSocket(final Session session, String UID, SocketConnection connection, JSONObject params) throws JSONException { + final String host = params.getString("host"); + final int port = params.getInt("port"); + final String location = String.format("%s:%s", host, port); + + if (connection.getNetworkSocket(location) != null) { + PrintSocketClient.sendError(session, UID, String.format("Socket [%s] is already open", location)); + return; + } + + //TODO - move to dedicated options class? + Charset encoding = StandardCharsets.UTF_8; + if (!params.isNull("options")) { + JSONObject options = params.getJSONObject("options"); + + if (!options.isNull("encoding")) { + encoding = Charset.forName(options.getString("encoding")); + } + } + + try { + final SocketIO socket = new SocketIO(host, port, encoding, connection); + + if (socket.open()) { + connection.addNetworkSocket(location, socket); + + new Thread(() -> { + StreamEvent event = new StreamEvent(StreamEvent.Stream.SOCKET, StreamEvent.Type.RECEIVE) + .withData("host", host).withData("port", port); + + try { + while(socket.isOpen()) { + String response = socket.processSocketResponse(); + + if (response != null) { + log.debug("Received socket response: {}", response); + PrintSocketClient.sendStream(session, event.withData("response", response), socket); + } + } + } + catch(IOException e) { + StreamEvent eventErr = new StreamEvent(StreamEvent.Stream.SOCKET, StreamEvent.Type.ERROR) + .withData("host", host).withData("port", port).withException(e); + PrintSocketClient.sendStream(session, eventErr, socket); + } + + try { Thread.sleep(100); } catch(Exception ignore) {} + }).start(); + + PrintSocketClient.sendResult(session, UID, null); + } else { + PrintSocketClient.sendError(session, UID, String.format("Unable to open socket [%s]", location)); + } + } + catch(IOException e) { + PrintSocketClient.sendError(session, UID, e); + } + } + +} diff --git a/old code/tray/src/qz/utils/StringUtilities.java b/old code/tray/src/qz/utils/StringUtilities.java new file mode 100755 index 0000000..70de51e --- /dev/null +++ b/old code/tray/src/qz/utils/StringUtilities.java @@ -0,0 +1,13 @@ +package qz.utils; + +import org.apache.commons.lang3.StringUtils; + +public class StringUtilities { + + public static final String[] HTML_ENTITIES = {"&", "<", ">", "\"", "'", "/"}; + public static final String[] HTML_REPLACED = {"&", "<", ">", """, "'", "/"}; + + public static String escapeHtmlEntities(String text) { + return StringUtils.replaceEach(text, HTML_ENTITIES, HTML_REPLACED); + } +} diff --git a/old code/tray/src/qz/utils/SystemUtilities.java b/old code/tray/src/qz/utils/SystemUtilities.java new file mode 100755 index 0000000..6bbffcc --- /dev/null +++ b/old code/tray/src/qz/utils/SystemUtilities.java @@ -0,0 +1,812 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.utils; + +import com.github.zafarkhaja.semver.ParseException; +import com.github.zafarkhaja.semver.Version; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.ssl.Base64; +import org.joor.Reflect; +import org.joor.ReflectException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.build.provision.params.Arch; +import qz.build.provision.params.Os; +import qz.common.Constants; +import qz.common.TrayManager; +import qz.installer.Installer; + +import javax.swing.*; +import java.awt.*; +import java.awt.geom.Area; +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Method; +import java.net.URLDecoder; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Random; +import java.util.TimeZone; + +/** + * Utility class for OS detection functions. + * + * @author Tres Finocchiaro + */ +public class SystemUtilities { + static final String OS_NAME = System.getProperty("os.name"); + static final String OS_ARCH = System.getProperty("os.arch"); + private static final Os OS_TYPE = Os.bestMatch(OS_NAME); + private static final Arch JRE_ARCH = Arch.bestMatch(OS_ARCH); + private static final Logger log = LogManager.getLogger(TrayManager.class); + + private static double windowScaleFactor = -1; + private static final Locale defaultLocale = Locale.getDefault(); + + static { + if(!isWindows() && !isMac()) { + // Force hid4java to use libusb: https://github.com/qzind/tray/issues/853 + try { + Reflect.on("org.hid4java.jna.HidApi").set("useLibUsbVariant", true); + } catch(ReflectException ignore) {} + } + } + + private static Boolean darkDesktop; + private static Boolean darkTaskbar; + private static Boolean hasMonocle; + private static String classProtocol; + private static Version osVersion; + private static String osName; + private static String osDisplayVersion; + private static Path jarPath; + private static Integer pid; + + public static Os getOs() { + return OS_TYPE; + } + + public static Arch getArch() { + return JRE_ARCH; + } + + /** + * Call to workaround Locale-specific bugs (See issue #680) + * Please call restoreLocale() as soon as possible + */ + public static synchronized void swapLocale() { + Locale.setDefault(Locale.ENGLISH); + } + + public static synchronized void restoreLocale() { + Locale.setDefault(defaultLocale); + } + + public static String toISO(Date d) { + DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'", Locale.ENGLISH); + TimeZone tz = TimeZone.getTimeZone("UTC"); + df.setTimeZone(tz); + return df.format(d); + } + + public static String timeStamp() { + return toISO(new Date()); + } + + /** + * The semantic version of the OS (e.g. "1.2.3") + */ + public static Version getOsVersion() { + if (osVersion == null) { + try { + switch(OS_TYPE) { + case WINDOWS: + // Windows is missing patch release, read it from registry + osVersion = WindowsUtilities.getOsVersion(); + break; + default: + String version = System.getProperty("os.version", "0.0.0"); + while(version.split("\\.").length < 3) { + version += ".0"; + } + osVersion = Version.valueOf(version); + } + } catch(ParseException | IllegalArgumentException e) { + log.warn("Unable to parse OS version as a semantic version", e); + osVersion = Version.forIntegers(0, 0, 0); + } + } + return osVersion; + } + + /** + * The human-readable display version of the OS (e.g. "22.04.1 LTS (Jammy Jellyfish)") + */ + public static String getOsDisplayVersion() { + if (osDisplayVersion == null) { + switch(OS_TYPE) { + case WINDOWS: + osDisplayVersion = WindowsUtilities.getOsDisplayVersion(); + break; + case MAC: + osDisplayVersion = MacUtilities.getOsDisplayVersion(); + break; + case LINUX: + osDisplayVersion = UnixUtilities.getOsDisplayVersion(); + break; + default: + osDisplayVersion = System.getProperty("os.version", "0.0.0"); + } + } + return osDisplayVersion; + } + + /** + * The human-readable display name of the OS (e.g. "Windows 10" or "Ubuntu") + */ + public static String getOsDisplayName() { + if(osName == null) { + switch(OS_TYPE) { + case LINUX: + // "Linux" is too generic, get the flavor (e.g. Ubuntu, Fedora) + osName = UnixUtilities.getOsDisplayName(); + break; + default: + osName = System.getProperty("os.name", "Unknown"); + } + } + return osName; + } + + public static boolean isAdmin() { + switch(OS_TYPE) { + case WINDOWS: + return ShellUtilities.execute("net", "session"); + default: + return whoami().equals("root"); + } + } + + public static String whoami() { + String whoami = System.getProperty("user.name"); + if(whoami == null || whoami.trim().isEmpty()) { + // Fallback on Command line + whoami = ShellUtilities.executeRaw("whoami").trim(); + } + return whoami; + } + + public static Version getJavaVersion() { + return getJavaVersion(System.getProperty("java.version")); + } + + /** + * Call a java command (e.g. java) with "--version" and parse the output + * The double dash "--" is since JDK9 but important to send the command output to stdout + */ + public static Version getJavaVersion(Path javaCommand) { + return getJavaVersion(ShellUtilities.executeRaw(javaCommand.toString(), "--version")); + } + + public static int getProcessId() { + if(pid == null) { + // Try Java 9+ + if(Constants.JAVA_VERSION.getMajorVersion() >= 9) { + pid = getProcessIdJigsaw(); + } + // Try JNA + if(pid == null || pid == -1) { + pid = SystemUtilities.isWindows() ? WindowsUtilities.getProcessId() : UnixUtilities.getProcessId(); + } + } + return pid; + } + + private static int getProcessIdJigsaw() { + try { + Class processHandle = Class.forName("java.lang.ProcessHandle"); + Method current = processHandle.getDeclaredMethod("current"); + Method pid = processHandle.getDeclaredMethod("pid"); + Object processHandleInstance = current.invoke(processHandle); + Object pidValue = pid.invoke(processHandleInstance); + if(pidValue instanceof Long) { + return ((Long)pidValue).intValue(); + } + } catch(Throwable t) { + log.warn("Could not get process ID using Java 9+, will attempt to fallback to JNA", t); + } + return -1; + } + + /** + * Handle Java versioning nuances + * To eventually be replaced with java.lang.Runtime.Version (JDK9+) + */ + public static Version getJavaVersion(String version) { + String[] parts = version.trim().split("\\D+"); + + int major = 1; + int minor = 0; + int patch = 0; + String meta = ""; + + try { + switch(parts.length) { + default: + case 4: + meta = parts[3]; + case 3: + patch = Integer.parseInt(parts[2]); + case 2: + minor = Integer.parseInt(parts[1]); + major = Integer.parseInt(parts[0]); + break; + case 1: + major = Integer.parseInt(parts[0]); + if (major <= 8) { + // Force old 1.x style formatting + minor = major; + major = 1; + } + } + } catch(NumberFormatException e) { + log.warn("Could not parse Java version \"{}\"", e); + } + if(meta.trim().isEmpty()) { + return Version.forIntegers(major, minor, patch); + } else { + return Version.forIntegers(major, minor, patch).setBuildMetadata(meta); + } + } + + /** + * Determines the currently running Jar's absolute path on the local filesystem + * todo: make this return a sane directory for running via ide + * + * @return A String value representing the absolute path to the currently running + * jar + */ + public static Path getJarPath() { + // jarPath won't change, send the cached value if we have it + if (jarPath != null) return jarPath; + try { + String url = URLDecoder.decode(SystemUtilities.class.getProtectionDomain().getCodeSource().getLocation().getPath(), "UTF-8"); + jarPath = new File(url).toPath(); + if (jarPath == null) return null; + jarPath = jarPath.toAbsolutePath(); + } catch(InvalidPathException | UnsupportedEncodingException ex) { + log.error("Unable to determine Jar path", ex); + } + return jarPath; + } + + /** + * Returns the folder containing the running jar + * or null if no .jar is found (such as running from IDE) + */ + public static Path getJarParentPath(){ + Path path = getJarPath(); + if (path == null || path.getParent() == null) return null; + return path.getParent(); + } + + /** + * Returns the jar's parent path, or a fallback if we're not a jar + */ + public static Path getJarParentPath(String relativeFallback) { + return getJarParentPath().resolve(SystemUtilities.isJar() ? "": relativeFallback).normalize(); + } + + /** + * Returns the app's path, calculated from the jar location + * or working directory if none can be found + */ + public static Path getAppPath() { + Path appPath = getJarParentPath(); + if(appPath == null) { + // We should never get here + appPath = Paths.get(System.getProperty("user.dir")); + } + + // Assume we're installed and running from /Applications/QZ Tray.app/Contents/Resources/qz-tray.jar + if(appPath.endsWith("Resources")) { + return appPath.getParent().getParent(); + } + // For all other use-cases, qz-tray.jar is installed in the root of the application + return appPath; + } + + /** + * Determine if the current Operating System is Windows + * + * @return {@code true} if Windows, {@code false} otherwise + */ + public static boolean isWindows() { + return OS_TYPE == Os.WINDOWS; + } + + /** + * Determine if the current Operating System is Mac OS + * + * @return {@code true} if Mac OS, {@code false} otherwise + */ + public static boolean isMac() { + return OS_TYPE == Os.MAC; + } + + /** + * Determine if the current Operating System is Linux + * + * @return {@code true} if Linux, {@code false} otherwise + */ + public static boolean isLinux() { + return OS_TYPE == Os.LINUX; + } + + /** + * Determine if the current Operating System is Unix + * + * @return {@code true} if Unix, {@code false} otherwise + */ + public static boolean isUnix() { + if(OS_NAME != null) { + String osLower = OS_NAME.toLowerCase(Locale.ENGLISH); + return OS_TYPE == Os.MAC || OS_TYPE == Os.SOLARIS || OS_TYPE == Os.LINUX || + osLower.contains("nix") || osLower.indexOf("aix") > 0; + } + return false; + } + + /** + * Determine if the current Operating System is Solaris + * + * @return {@code true} if Solaris, {@code false} otherwise + */ + public static boolean isSolaris() { + return OS_TYPE == Os.SOLARIS; + } + + public static boolean isDarkTaskbar() { + return isDarkTaskbar(false); + } + + public static boolean isDarkTaskbar(boolean recheck) { + if(darkTaskbar == null || recheck) { + if (isWindows()) { + darkTaskbar = WindowsUtilities.isDarkTaskbar(); + } else if(isMac()) { + // Ignore, we'll set the template flag using JNA + darkTaskbar = false; + } else { + // Linux doesn't differentiate; return the cached darkDesktop value + darkTaskbar = isDarkDesktop(); + } + } + return darkTaskbar.booleanValue(); + } + + public static boolean isDarkDesktop() { + return isDarkDesktop(false); + } + + public static boolean isDarkDesktop(boolean recheck) { + if (darkDesktop == null || recheck) { + // Check for Dark Mode on MacOS + if (isMac()) { + darkDesktop = MacUtilities.isDarkDesktop(); + } else if (isWindows()) { + darkDesktop = WindowsUtilities.isDarkDesktop(); + } else { + darkDesktop = UnixUtilities.isDarkMode(); + } + } + return darkDesktop.booleanValue(); + } + + public static void adjustThemeColors() { + Constants.WARNING_COLOR = isDarkDesktop() ? Constants.WARNING_COLOR_DARK : Constants.WARNING_COLOR_LITE; + Constants.TRUSTED_COLOR = isDarkDesktop() ? Constants.TRUSTED_COLOR_DARK : Constants.TRUSTED_COLOR_LITE; + } + + public static boolean prefersMaskTrayIcon() { + if (Constants.MASK_TRAY_SUPPORTED) { + if (SystemUtilities.isMac()) { + // Assume a pid of -1 is a broken JNA + return getProcessId() != -1; + } else if (SystemUtilities.isWindows() && SystemUtilities.getOsVersion().getMajorVersion() >= 10) { + return true; + } + } + return false; + } + + public static boolean setSystemLookAndFeel(boolean headless) { + if(headless) { + return false; + } + try { + UIManager.getDefaults().put("Button.showMnemonics", Boolean.TRUE); + boolean darculaThemeNeeded = true; + if(!isMac() && (isUnix() && UnixUtilities.isDarkMode())) { + darculaThemeNeeded = false; + } + if(isDarkDesktop() && darculaThemeNeeded) { + UIManager.setLookAndFeel("com.bulenkov.darcula.DarculaLaf"); + } else { + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } + adjustThemeColors(); + return true; + } catch (Throwable t) { + log.warn("Error getting the default look and feel"); + } + return false; + } + + /** + * Attempts to center a dialog provided a center point from a web browser at 96-dpi + * Useful for tracking a browser window on multiple-monitor setups + * @param dialog A dialog whom's width and height are used for calculating center-fit position + * @param position The center point of a screen as calculated from a web browser at 96-dpi + * @return true if the operation is successful + */ + public static void centerDialog(Dialog dialog, Point position) { + // Assume 0,0 are bad coordinates + if (position == null || (position.getX() == 0 && position.getY() == 0)) { + log.debug("Invalid dialog position provided: {}, we'll center on first monitor instead", position); + dialog.setLocationRelativeTo(null); + return; + } + + //adjust for dpi scaling + double dpiScale = getWindowScaleFactor(true); + if (dpiScale == 0) { + log.debug("Invalid window scale value: {}, we'll center on the primary monitor instead", dpiScale); + dialog.setLocationRelativeTo(null); + return; + } + + Rectangle rect = new Rectangle((int)(position.x * dpiScale), (int)(position.y * dpiScale), dialog.getWidth(), dialog.getHeight()); + rect.translate(-dialog.getWidth() / 2, -dialog.getHeight() / 2); + Point p = new Point((int)rect.getCenterX(), (int)rect.getCenterY()); + log.debug("Calculated dialog centered at: {}", p); + + if (!isWindowLocationValid(rect)) { + log.debug("Dialog position provided is out of bounds: {}, we'll center on the primary monitor instead", p); + dialog.setLocationRelativeTo(null); + return; + } + + dialog.setLocation(rect.getLocation()); + } + + /** + * Validates if a given rectangle is within screen bounds + */ + public static boolean isWindowLocationValid(Rectangle window) { + if(GraphicsEnvironment.isHeadless()) { + return false; + } + + GraphicsDevice[] devices = GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices(); + Area area = new Area(); + for(GraphicsDevice gd : devices) { + for(GraphicsConfiguration gc : gd.getConfigurations()) { + area.add(new Area(gc.getBounds())); + } + } + return area.contains(window); + } + + /** + * Shim for detecting window screen-placement scaling + * See issues #284, #448 + * @return Logical dpi scale as dpi/96 + */ + private static double getWindowScaleFactor(boolean forceRefresh) { + if(windowScaleFactor == -1 || forceRefresh) { + // MacOS is always 1 + if (isMac()) { + return windowScaleFactor = 1; + } + // Windows/Linux on JDK8 honors scaling + if (Constants.JAVA_VERSION.lessThan(Version.valueOf("11.0.0"))) { + return windowScaleFactor = Toolkit.getDefaultToolkit().getScreenResolution() / 96.0; + } + // Windows on JDK11 is always 1 + if (isWindows()) { + return windowScaleFactor = 1; + } + // Linux/Unix on JDK11 requires JNA calls to Gdk + return windowScaleFactor = UnixUtilities.getScaleFactor(); + } + return windowScaleFactor; + } + + public static double getWindowScaleFactor() { + return getWindowScaleFactor(false); + } + + public static Dimension scaleWindowDimension(Dimension orig) { + return scaleWindowDimension(orig.getWidth(), orig.getHeight()); + } + + public static Dimension scaleWindowDimension(double width, double height) { + double scaleFactor = getWindowScaleFactor(); + return new Dimension( + (int)(width * scaleFactor), + (int)(height * scaleFactor) + ); + } + + /** + * Detects if HiDPI is enabled + * Warning: Due to behavioral differences between OSs, JDKs and poor + * detection techniques this function should only be used to fix rare + * edge-case bugs. + * + * See also SystemUtilities.getWindowScaleFactor() + * @return true if HiDPI is detected + */ + public static boolean isHiDPI() { + if(isMac()) { + return MacUtilities.getScaleFactor() > 1; + } else if(isWindows()) { + return WindowsUtilities.getScaleFactor() > 1; + } + // Fallback to a JNA Gdk technique + return UnixUtilities.getScaleFactor() > 1; + } + + /** + * Detects if running from IDE or jar + * @return true if running from a jar, false if running from IDE + */ + public static boolean isJar() { + if (classProtocol == null) { + classProtocol = SystemUtilities.class.getResource("").getProtocol(); + } + return "jar".equals(classProtocol); + } + + /** + * Todo: + * @return true if running from a jar, false if running from IDE + */ + public static boolean isInstalled() { + Path path = getJarParentPath(); + if(path == null) { + return false; + } + // Assume dist or out are signs we're running from some form of build directory + return !path.endsWith("dist") && !path.endsWith("out"); + } + + /** + * Allows in-line insertion of a property before another + * @param value the end of a value to insert before, assumes to end with File.pathSeparator + */ + public static void insertPathProperty(String property, String value, String insertBefore) { + insertPathProperty(property, value, File.pathSeparator, insertBefore); + } + + private static void insertPathProperty(String property, String value, String delimiter, String insertBefore) { + String currentValue = System.getProperty(property); + if(currentValue == null || currentValue.trim().isEmpty()) { + // Set it directly, there's nothing there + System.setProperty(property, value); + return; + } + // Blindly split on delimiter, safe according to POSIX standards + // See also: https://stackoverflow.com/a/29213487/3196753 + String[] paths = currentValue.split(delimiter); + StringBuilder finalProperty = new StringBuilder(); + boolean inserted = false; + for(String path : paths) { + if(!inserted && path.endsWith(insertBefore)) { + finalProperty.append(value + delimiter); + inserted = true; + } + finalProperty.append(path + delimiter); + } + // Add to end if delimiter wasn't found + if(!inserted) { + finalProperty.append(value); + } + // Truncate trailing delimiter + if(StringUtils.endsWith(finalProperty, delimiter)) { + finalProperty.setLength(finalProperty.length() - delimiter.length()); + } + System.setProperty(property, finalProperty.toString()); + } + + public static boolean isJDK() { + String path = System.getProperty("sun.boot.library.path"); + if(path != null) { + String javacPath = ""; + if(path.endsWith(File.separator + "bin")) { + javacPath = path; + } else { + int libIndex = path.lastIndexOf(File.separator + "lib"); + if(libIndex > 0) { + javacPath = path.substring(0, libIndex) + File.separator + "bin"; + } + } + if(!javacPath.isEmpty()) { + return new File(javacPath, "javac").exists() || new File(javacPath, "javac.exe").exists(); + } + } + return false; + } + + public static boolean hasMonocle() { + if(hasMonocle == null) { + try { + Class.forName("com.sun.glass.ui.monocle.MonoclePlatformFactory"); + hasMonocle = true; + } catch (ClassNotFoundException | UnsupportedClassVersionError e) { + hasMonocle = false; + } + } + return hasMonocle; + } + + public static final Version[] JDK_8266929_VERSIONS = { + Version.valueOf("11.0.11"), + Version.valueOf("1.8.0+291"), + Version.valueOf("1.8.0+292") + }; + + /** + * Fixes JDK-8266929 by clearing the oidTable + * See also: https://github.com/qzind/tray/issues/814 + */ + public static void clearAlgorithms() { + boolean needsPatch = false; + for(Version affected : JDK_8266929_VERSIONS) { + if(affected.getMajorVersion() == 1) { + // Java 1.8 honors build/update information + if(affected.compareWithBuildsTo(Constants.JAVA_VERSION) == 0) { + needsPatch = true; + } + } else if (affected.compareTo(Constants.JAVA_VERSION) == 0) { + // Java 9.0+ ignores build/update information + needsPatch = true; + } + } + if(!needsPatch) { + log.debug("Skipping JDK-8266929 patch for {}", Constants.JAVA_VERSION); + return; + } + try { + log.info("Applying JDK-8266929 patch"); + Class algorithmIdClass = Class.forName("sun.security.x509.AlgorithmId"); + java.lang.reflect.Field oidTableField = algorithmIdClass.getDeclaredField("oidTable"); + oidTableField.setAccessible(true); + // Set oidTable to null + oidTableField.set(algorithmIdClass, null); + // Java 1.8 + if(Constants.JAVA_VERSION.getMajorVersion() == 1) { + java.lang.reflect.Field initOidTableField = algorithmIdClass.getDeclaredField("initOidTable"); + initOidTableField.setAccessible(true); + // Set init flag back to false + initOidTableField.set(algorithmIdClass, false); + } + log.info("Successfully applied JDK-8266929 patch"); + } catch (Exception e) { + log.warn("Unable to apply JDK-8266929 patch. Some algorithms may fail.", e); + } + } + + public static String getHostName() { + String hostName = SystemUtilities.isWindows() ? WindowsUtilities.getHostName() : UnixUtilities.getHostName(); + if(hostName == null || hostName.trim().isEmpty()) { + log.warn("Couldn't get hostname using internal techniques, will fallback to command line instead"); + hostName = ShellUtilities.getHostName().toUpperCase(); // uppercase to match others + } + return hostName; + } + + /** + * A challenge which can only be calculated by an app installed and running on this machine + * Calculates two bytes: + * - First byte is a salted version of the timestamp of qz-tray.jar + * - Second byte is a throw-away byte + * - Bytes are converted to Base64 and returned as a String + */ + public static String calculateSaltedChallenge() { + int salt = new Random().nextInt(9); + long salted = (calculateChallenge() * 10) + salt; + long obfuscated = salted * new Random().nextInt(9); + ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES * 2); + buffer.putLong(obfuscated); + buffer.putLong(0, salted); + return new String(Base64.encodeBase64(buffer.array(), false), StandardCharsets.UTF_8); + } + + private static long calculateChallenge() { + Path jarPath; + if(SystemUtilities.isJar()) { + jarPath = getJarPath(); + } else { + // Running from IDE: Try to guess .jar location + if(SystemUtilities.isMac()) { + // 2.2+ + jarPath = Paths.get(Installer.getInstance().getDestination(), "Contents", "Resources", Constants.PROPS_FILE + ".jar"); + if(!jarPath.toFile().exists()) { + // 2.1 + jarPath = Paths.get(Installer.getInstance().getDestination(), Constants.PROPS_FILE + ".jar"); + } + } else { + jarPath = Paths.get(Installer.getInstance().getDestination(), Constants.PROPS_FILE + ".jar"); + } + } + if (jarPath.toFile().exists()) { + return jarPath.toFile().lastModified(); + } + return -1L; // Fallback + } + + /** + * Decodes challenge string to see if it originated from this application + * - Base64 string is decoded into two bytes + * - First byte is unsalted + * - Second byte is ignored + * - If unsalted value of first byte matches the timestamp of qz-tray.jar, return true + * - If unsalted value doesn't match or if any exceptions occurred, we assume the message is invalid + */ + public static boolean validateSaltedChallenge(String message) { + try { + log.info("Attempting to validating challenge: {}", message); + byte[] decoded = Base64.decodeBase64(message); + ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES * 2); + buffer.put(decoded); + // Explicit cast, per https://github.com/qzind/tray/issues/1055 + ((Buffer)buffer).flip();//need flip + long salted = buffer.getLong(0); // only first byte matters + long challenge = salted / 10L; + return challenge == calculateChallenge(); + } catch(Exception ignore) { + log.warn("An exception occurred validating challenge: {}", message, ignore); + } + return false; + } + + /** + * Cross-platform SystemTray detector + */ + public static boolean isSystemTraySupported(boolean headless) { + if(!headless) { + switch(getOs()) { + case WINDOWS: + if(WindowsUtilities.isHiddenSystemTray()) { + return false; + } + break; + case MAC: + break; + default: + // Linux System Tray support is abysmal, always use TaskbarTrayIcon + return false; + } + return SystemTray.isSupported(); + } + return false; + } +} diff --git a/old code/tray/src/qz/utils/UnixUtilities.java b/old code/tray/src/qz/utils/UnixUtilities.java new file mode 100755 index 0000000..8586467 --- /dev/null +++ b/old code/tray/src/qz/utils/UnixUtilities.java @@ -0,0 +1,269 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2021 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.utils; + +import com.github.zafarkhaja.semver.Version; +import com.sun.jna.Library; +import com.sun.jna.Native; +import com.sun.jna.platform.unix.LibC; +import org.apache.commons.io.FileUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; + +import java.awt.*; +import java.io.BufferedReader; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.*; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.stream.Stream; + +/** + * Helper functions for both Linux and Unix + */ +public class UnixUtilities { + private static final Logger log = LogManager.getLogger(UnixUtilities.class); + private static final String[] OS_NAME_KEYS = {"NAME", "DISTRIB_ID"}; + private static final String[] OS_VERSION_KEYS = {"VERSION", "DISTRIB_RELEASE"}; + private static final String[] KNOWN_ELEVATORS = {"pkexec", "gksu", "gksudo", "kdesudo" }; + private static final String[] OS_RELEASE_FILES = {"/etc/os-release", "/usr/lib/os-release", "/etc/lsb-release", "/etc/redhat-release"}; + private static String uname; + private static String unixRelease; + private static String unixVersion; + private static Integer pid; + private static String foundElevator; + + static String getHostName() { + String hostName = null; + try { + byte[] bytes = new byte[255]; + if (LibC.INSTANCE.gethostname(bytes, bytes.length) == 0) { + hostName = Native.toString(bytes); + } + } catch(Throwable ignore) {} + return hostName; + } + + static int getProcessId() { + if(pid == null) { + try { + pid = UnixUtilities.CLibrary.INSTANCE.getpid(); + } + catch(UnsatisfiedLinkError | NoClassDefFoundError e) { + log.warn("Could not obtain process ID. This usually means JNA isn't working. Returning -1."); + pid = -1; + } + } + return pid; + } + + private interface CLibrary extends Library { + CLibrary INSTANCE = Native.load("c", CLibrary.class); + int getpid(); + } + + /** + * Returns the output of {@code uname -a} shell command, useful for parsing the Linux Version + * + * @return the output of {@code uname -a}, or null if not running Linux + */ + public static String getUname() { + if (SystemUtilities.isUnix() && uname == null) { + uname = ShellUtilities.execute( + new String[] {"uname", "-a"}, + (String[])null + ); + } + + return uname; + } + + /** + * Returns the name of the OS, trying to obtain distro information if available + */ + public static String getOsDisplayName() { + if (unixRelease == null) { + try { + Map map = getReleaseMap(); + for (String nameKey: OS_NAME_KEYS) { + if (map.containsKey(nameKey)) { + unixRelease = map.get(nameKey); + break; + } + } + } catch(IOException e) { + log.warn("Could not find a suitable os-release file {}", Arrays.toString(OS_RELEASE_FILES)); + } + if(unixRelease == null) { + log.warn("Could not find name key {} in files {}", Arrays.toString(OS_NAME_KEYS), Arrays.toString(OS_RELEASE_FILES)); + unixRelease = System.getProperty("os.name", "Unknown"); + } + } + return unixRelease; + } + + /** + * The human-readable display version of the Linux/Unix OS + */ + public static String getOsDisplayVersion() { + if (unixVersion == null) { + try { + Map map = getReleaseMap(); + for(String versionKey : OS_VERSION_KEYS) { + if (map.containsKey(versionKey)) { + unixVersion = map.get(versionKey); + break; + } + } + } + catch(IOException e) { + log.warn("Could not find a suitable os-release file {}", Arrays.toString(OS_RELEASE_FILES)); + } + if(unixVersion == null) { + log.warn("Could not find version key {} in files {}", Arrays.toString(OS_VERSION_KEYS), Arrays.toString(OS_RELEASE_FILES)); + // If we can't get version info from a file, run the "lsb_release" command + String lsbRelease = ShellUtilities.executeRaw(new String[] {"lsb_release", "-ds"}).trim(); + if(!lsbRelease.isEmpty()) { + unixVersion = lsbRelease; + } else { + unixVersion = System.getProperty("os.version", "0.0.0"); + } + } + } + return unixVersion; + } + + private static Map getReleaseMap() throws IOException { + HashMap map = new HashMap<>(); + BufferedReader reader = null; + try { + Path release = findOsReleaseFile(); + reader = new BufferedReader(new FileReader(release.toFile())); + String line; + while((line = reader.readLine()) != null) { + String[] tokens = line.split("=", 2); + if (tokens.length != 2) continue; + map.put(tokens[0], tokens[1].replaceAll("\"", "")); + } + } finally{ + if(reader != null) { + reader.close(); + } + } + return map; + } + + private static String findElevator() throws IOException { + if(foundElevator == null) { + for(String elevator : KNOWN_ELEVATORS) { + if (ShellUtilities.execute("which", elevator)) { + foundElevator = elevator; + break; + } + } + throw new IOException("Can't find an installed utility " + Arrays.toString(KNOWN_ELEVATORS) + " to elevate permissions."); + } + return foundElevator; + } + + private static Path findOsReleaseFile() throws FileNotFoundException { + // Search by name for the supported distros, in order of preference + for(String release : OS_RELEASE_FILES) { + Path path = Paths.get(release); + if (Files.exists(path)) return path; + } + Stream s; + try { + s = Files.find( + // If that fails, try to find any *-release file + Paths.get("/etc/"), + 1, + (path, basicFileAttributes) -> path.getFileName().toString().endsWith("-release"), + FileVisitOption.FOLLOW_LINKS + ); + // If no element is found this will throw a NoSuchElementException + return s.findFirst().get(); + } catch(Exception ignore) {} + throw new FileNotFoundException("Could not find os-release file"); + } + + public static boolean elevatedFileCopy(Path source, Path destination) { + // Don't prompt if it's not needed + try { + // Note: preserveFileDate=false per https://github.com/qzind/tray/issues/1011 + FileUtils.copyFile(source.toFile(), destination.toFile(), false); + return true; + } catch(IOException ignore) {} + + try { + String[] command = {findElevator(), "cp", source.toString(), destination.toString()}; + return ShellUtilities.execute(command); + } catch(IOException io) { + log.error("Copy failed. You'll have do this manually.", io); + } + return false; + } + + /** + * Runs a shell command to determine if "Dark" desktop theme is enabled + * @return true if enabled, false if not + */ + public static boolean isDarkMode() { + return !ShellUtilities.execute(new String[] { "gsettings", "get", "org.gnome.desktop.interface", "gtk-theme" }, new String[] { "dark" }, true, true).isEmpty(); + } + + public static double getScaleFactor() { + if (Constants.JAVA_VERSION.lessThan(Version.valueOf("11.0.0"))) { + return Toolkit.getDefaultToolkit().getScreenResolution() / 96.0; + } + return GtkUtilities.getScaleFactor(); + } + + /** + * Returns whether the output of {@code uname -a} shell command contains "Ubuntu" + * + * @return {@code true} if this OS is Ubuntu + */ + public static boolean isUbuntu() { + if(!SystemUtilities.isLinux()) { + return false; + } + getUname(); + return uname != null && uname.contains("Ubuntu"); + } + + /** + * Returns whether the output of {@code uname -a} shell command contains "Debian" + * + * @return {@code true} if this OS is Debian + */ + public static boolean isDebian() { + if(!SystemUtilities.isLinux()) { + return false; + } + getUname(); + return uname != null && uname.contains("Debian"); + } + + + /** + * Returns whether the output of cat /etc/redhat-release/code> shell command contains "Fedora" + * + * @return {@code true} if this OS is Fedora + */ + public static boolean isFedora() { + if(!SystemUtilities.isLinux()) return false; + return getOsDisplayName() != null && getOsDisplayName().contains("Fedora"); + } +} diff --git a/old code/tray/src/qz/utils/UsbUtilities.java b/old code/tray/src/qz/utils/UsbUtilities.java new file mode 100755 index 0000000..b7601c8 --- /dev/null +++ b/old code/tray/src/qz/utils/UsbUtilities.java @@ -0,0 +1,238 @@ +package qz.utils; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.exceptions.WebSocketException; +import qz.communication.DeviceException; +import qz.communication.DeviceIO; +import qz.communication.DeviceOptions; +import qz.ws.PrintSocketClient; +import qz.ws.SocketConnection; +import qz.ws.StreamEvent; + +import javax.usb.*; +import javax.usb.util.UsbUtil; +import java.util.ArrayList; +import java.util.List; + +public class UsbUtilities { + + private static final Logger log = LogManager.getLogger(UsbUtilities.class); + + public static Integer hexToInt(String hex) { + if (hex == null || hex.isEmpty()) { + return null; + } + + if (hex.startsWith("0x")) { hex = hex.substring(2); } + return Integer.parseInt(hex, 16); + } + + public static Byte hexToByte(String hex) { + if (hex == null || hex.isEmpty()) { + return null; + } + + if (hex.startsWith("0x")) { hex = hex.substring(2); } + return (byte)Integer.parseInt(hex, 16); + } + + + public static List getUsbDevices(boolean includeHubs) throws DeviceException { + try { + return getUsbDevices(UsbHostManager.getUsbServices().getRootUsbHub(), includeHubs); + } + catch(UsbException e) { + throw new DeviceException(e); + } + } + + private static List getUsbDevices(UsbHub hub, boolean includeHubs) { + List devices = new ArrayList<>(); + + for(Object attached : hub.getAttachedUsbDevices()) { + UsbDevice device = (UsbDevice)attached; + + if (device.isUsbHub()) { + if (includeHubs) { + devices.add(device); + } + + devices.addAll(getUsbDevices((UsbHub)device, includeHubs)); + } else { + devices.add(device); + } + } + + return devices; + } + + public static JSONArray getUsbDevicesJSON(boolean includeHubs) throws DeviceException, JSONException { + List devices = getUsbDevices(includeHubs); + JSONArray deviceJSON = new JSONArray(); + + for(UsbDevice device : devices) { + UsbDeviceDescriptor desc = device.getUsbDeviceDescriptor(); + + JSONObject descJSON = new JSONObject(); + descJSON.put("vendorId", UsbUtil.toHexString(desc.idVendor())); + descJSON.put("productId", UsbUtil.toHexString(desc.idProduct())); + descJSON.put("hub", device.isUsbHub()); + + deviceJSON.put(descJSON); + } + + return deviceJSON; + } + + public static UsbDevice findDevice(Short vendorId, Short productId) throws DeviceException { + try { + UsbDevice device = findDevice(UsbHostManager.getUsbServices().getRootUsbHub(), vendorId, productId); + if(device == null) { + throw new DeviceException(String.format("Could not find USB device matching [ vendorId: '%s', productId: '%s' ]", + "0x" + UsbUtil.toHexString(vendorId), + "0x" + UsbUtil.toHexString(productId))); + } + return device; + } + catch(UsbException e) { + throw new DeviceException(e); + } + } + + private static UsbDevice findDevice(UsbHub hub, Short vendorId, Short productId) { + if (vendorId == null) { + throw new IllegalArgumentException("Vendor ID cannot be null"); + } + if (productId == null) { + throw new IllegalArgumentException("Product ID cannot be null"); + } + + for(Object attached : hub.getAttachedUsbDevices()) { + UsbDevice device = (UsbDevice)attached; + + UsbDeviceDescriptor desc = device.getUsbDeviceDescriptor(); + if (desc.idVendor() == vendorId && desc.idProduct() == productId) { + return device; + } + + if (device.isUsbHub()) { + device = findDevice((UsbHub)device, vendorId, productId); + if (device != null) { + return device; + } + } + } + + return null; + } + + public static List getDeviceInterfaces(Short vendorId, Short productId) throws DeviceException { + return findDevice(vendorId, productId).getActiveUsbConfiguration().getUsbInterfaces(); + } + + public static JSONArray getDeviceInterfacesJSON(DeviceOptions dOpts) throws DeviceException { + JSONArray ifaceJSON = new JSONArray(); + + List ifaces = getDeviceInterfaces(dOpts.getVendorId().shortValue(), dOpts.getProductId().shortValue()); + for(Object o : ifaces) { + UsbInterface iface = (UsbInterface)o; + UsbInterfaceDescriptor desc = iface.getUsbInterfaceDescriptor(); + + ifaceJSON.put(UsbUtil.toHexString(desc.bInterfaceNumber())); + } + + return ifaceJSON; + } + + public static List getInterfaceEndpoints(Short vendorId, Short productId, Byte iface) throws DeviceException { + if (iface == null) { + throw new IllegalArgumentException("Device interface cannot be null"); + } + + UsbInterface usbInterface = findDevice(vendorId, productId).getActiveUsbConfiguration().getUsbInterface(iface); + if(usbInterface != null) { + return usbInterface.getUsbEndpoints(); + } + throw new DeviceException(String.format("Could not find USB interface matching [ vendorId: '%s', productId: '%s', interface: '%s' ]", + "0x" + UsbUtil.toHexString(vendorId), + "0x" + UsbUtil.toHexString(productId), + "0x" + UsbUtil.toHexString(iface))); + + } + + public static JSONArray getInterfaceEndpointsJSON(DeviceOptions dOpts) throws DeviceException { + JSONArray endJSON = new JSONArray(); + + List endpoints = getInterfaceEndpoints(dOpts.getVendorId().shortValue(), dOpts.getProductId().shortValue(), dOpts.getInterfaceId()); + for(Object o : endpoints) { + UsbEndpoint endpoint = (UsbEndpoint)o; + UsbEndpointDescriptor desc = endpoint.getUsbEndpointDescriptor(); + + endJSON.put(UsbUtil.toHexString(desc.bEndpointAddress())); + } + + return endJSON; + } + + + // shared by usb and hid streaming + public static void setupUsbStream(final Session session, String UID, SocketConnection connection, final DeviceOptions dOpts, final StreamEvent.Stream streamType) { + final DeviceIO usb = connection.getDevice(dOpts); + + if (usb != null) { + if (!usb.isStreaming()) { + usb.setStreaming(true); + + new Thread() { + @Override + public void run() { + int interval = dOpts.getInterval(); + int size = dOpts.getResponseSize(); + Byte endpoint = dOpts.getEndpoint(); + + StreamEvent event = new StreamEvent(streamType, StreamEvent.Type.RECEIVE) + .withData("vendorId", usb.getVendorId()).withData("productId", usb.getProductId()); + + try { + while(usb.isOpen() && usb.isStreaming()) { + byte[] response = usb.readData(size, endpoint); + JSONArray hex = new JSONArray(); + for(byte b : response) { + hex.put(UsbUtil.toHexString(b)); + } + + PrintSocketClient.sendStream(session, event.withData("output", hex), usb); + + try { Thread.sleep(interval); } catch(Exception ignore) {} + } + } + catch(WebSocketException | DeviceException e) { + // Calling usb.close() can cause a hard-crash on macOS + usb.setStreaming(false); + log.error("USB stream error", e); + + if(session.isOpen()) { + StreamEvent eventErr = new StreamEvent(streamType, StreamEvent.Type.ERROR).withException(e) + .withData("vendorId", usb.getVendorId()).withData("productId", usb.getProductId()); + PrintSocketClient.sendStream(session, eventErr, (Runnable)() -> { + } /** No-op prevents re-throw **/); + } + } + } + }.start(); + + PrintSocketClient.sendResult(session, UID, null); + } else { + PrintSocketClient.sendError(session, UID, String.format("USB Device [v:%s p:%s] is already streaming data.", dOpts.getVendorId(), dOpts.getProductId())); + } + } else { + PrintSocketClient.sendError(session, UID, String.format("USB Device [v:%s p:%s] must be claimed first.", dOpts.getVendorId(), dOpts.getProductId())); + } + } + +} diff --git a/old code/tray/src/qz/utils/WindowsUtilities.java b/old code/tray/src/qz/utils/WindowsUtilities.java new file mode 100755 index 0000000..480c12a --- /dev/null +++ b/old code/tray/src/qz/utils/WindowsUtilities.java @@ -0,0 +1,467 @@ +/** + * @author Tres Finocchiaro + * + * Copyright (C) 2021 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ +package qz.utils; + +import com.github.zafarkhaja.semver.Version; +import com.sun.jna.Native; +import com.sun.jna.platform.win32.*; +import com.sun.jna.ptr.IntByReference; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.build.provision.params.Arch; +import qz.common.Constants; + +import java.awt.*; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.*; +import java.util.*; +import java.util.List; + +import static com.sun.jna.platform.win32.WinReg.*; +import static qz.utils.SystemUtilities.*; + +import static java.nio.file.attribute.AclEntryPermission.*; +import static java.nio.file.attribute.AclEntryFlag.*; + +public class WindowsUtilities { + protected static final Logger log = LogManager.getLogger(WindowsUtilities.class); + private static final String THEME_REG_KEY = "Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; + private static final String SPOOLER_REG_KEY = "Software\\Microsoft\\Windows NT\\CurrentVersion\\Print\\Printers"; + private static final String TRAY_REG_CHEVRON_KEY = "Software\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\CurrentVersion\\TrayNotify"; + private static final String TRAY_REG_POLICY_KEY = "Software\\Microsoft\\Windows\\CurrentVersion\\Policies\\Explorer"; + private static final String AUTHENTICATED_USERS_SID = "S-1-5-11"; + private static final int WINDOWS_10_BUILD_NUMBER = 10000; + private static Boolean isWow64; + private static Integer pid; + private static HashMap printerSpoolerLocations = new HashMap<>(); + + private static String defaultSpoolerLocation; + + public static boolean isDarkDesktop() { + // 0 = Dark Theme. -1/1 = Light Theme + Integer regVal; + if((regVal = getRegInt(HKEY_CURRENT_USER, THEME_REG_KEY, "AppsUseLightTheme")) != null) { + // Fallback on apps theme + return regVal == 0; + } + return false; + } + + public static boolean isDarkTaskbar() { + // -1/0 = Dark Theme. 1 = Light Theme + Integer regVal; + if((regVal = getRegInt(HKEY_CURRENT_USER, THEME_REG_KEY, "SystemUsesLightTheme")) != null) { + // Prefer system theme + return regVal == 0; + } + return true; + } + + public static Version getOsVersion() { + WinNT.OSVERSIONINFO versionInfo = new WinNT.OSVERSIONINFO(); + // GetVersionEx is deprecated, but has no sane replacement. https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getversionexa + if (!Kernel32.INSTANCE.GetVersionEx(versionInfo)) throw new RuntimeException(); + + String build = ""; + if (versionInfo.dwBuildNumber.longValue() >= WINDOWS_10_BUILD_NUMBER) { + // UBR or "Update Build Revision" was introduced in win10/server 2016. It reflects the monthly rollup version number. + build = getRegInt(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "UBR").toString(); + } + + Version osVersion = Version.forIntegers( + versionInfo.dwMajorVersion.intValue(), + versionInfo.dwMinorVersion.intValue(), + versionInfo.dwBuildNumber.intValue() + ); + + if(!build.trim().isEmpty()) { + osVersion.setBuildMetadata(build); + } + return osVersion; + } + + /** + * The human-readable display version of the Windows machine + */ + public static String getOsDisplayVersion() { + try { + // Product name is the 'real' name of the os, e.g. Windows 10 Home + String productName = getRegString(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "ProductName"); + Version version = SystemUtilities.getOsVersion(); + String extraInfo = ""; + + if (version.getPatchVersion() < WINDOWS_10_BUILD_NUMBER) { + WinNT.OSVERSIONINFO versionInfo = new WinNT.OSVERSIONINFO(); + Kernel32.INSTANCE.GetVersionEx(versionInfo); + // CSD is the servicePack string in long form e.g. Service Pack 3 + extraInfo += " " + Native.toString(versionInfo.szCSDVersion); + } else { + // ReleaseID was both introduced and retired for Windows 10. If 'DisplayVersion' exists, we can ignore ReleaseId, as it will be '2009' forever + int releaseID = Integer.parseInt(getRegString(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "ReleaseId")); + // DisplayVersion is the last 2 digits of the year, followed by H1 or H2 depending on the year-half. e.g. 22H2 + if (Advapi32Util.registryValueExists(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "DisplayVersion")) { + extraInfo += " Version: " + getRegString(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion", "DisplayVersion"); + } else { + extraInfo += " Release: " + releaseID; + } + } + return productName + " " + version.toString().replace("+", ".") + extraInfo; + } catch(Exception e) { + log.warn("Couldn't get detailed OS version info, using cli fallback {}", e.getMessage()); + } + try { + // The ver command an internal command of cmd.exe. It must be executed through cmd + String ver = ShellUtilities.executeRaw(new String[] {"cmd.exe", "/c", "ver"}).replaceAll("\\n", ""); + if(!ver.trim().isEmpty()) { + return ver; + } + throw new Exception("Empty output received from \"ver\" command"); + } catch(Exception e) { + log.warn("CLI fallback failed {}", e.getMessage()); + } + return "Unknown"; + } + + /** + * Check known configurations which hide the Windows SystemTray area + */ + public static boolean isHiddenSystemTray() { + // Windows 11 22H2+: Check chevron is visible (assume "1" if key is missing) + Integer chevronVisibility = getRegInt(HKEY_CURRENT_USER, TRAY_REG_CHEVRON_KEY, "SystemTrayChevronVisibility"); + if(chevronVisibility == null) chevronVisibility = 1; + + // Windows 2003+: Check user policy (assume "0" if key is missing) + Integer explorerPolicy = getRegInt(HKEY_CURRENT_USER, TRAY_REG_POLICY_KEY, "NoTrayItemsDisplay"); + if(explorerPolicy == null) explorerPolicy = 0; + + // Return true if either flag is set + return chevronVisibility == 0 || explorerPolicy == 1; + } + + public static double getScaleFactor() { + if (Constants.JAVA_VERSION.lessThan(Version.valueOf("9.0.0"))) { + WinDef.HDC hdc = GDI32.INSTANCE.CreateCompatibleDC(null); + if (hdc != null) { + int actual = GDI32.INSTANCE.GetDeviceCaps(hdc, 10 /* VERTRES */); + int logical = GDI32.INSTANCE.GetDeviceCaps(hdc, 117 /* DESKTOPVERTRES */); + GDI32.INSTANCE.DeleteDC(hdc); + if (logical != 0 && logical/actual > 1) { + return (double)logical/actual; + } + } + } + return Toolkit.getDefaultToolkit().getScreenResolution() / 96.0d; + } + + public static Path getSpoolerLocation(String printerName) throws FileNotFoundException { + // TODO: If the spooler restarts, the spooler location could change, detect spooler restart + if (printerSpoolerLocations.containsKey(printerName)) return printerSpoolerLocations.get(printerName); + + String regValue = getRegString(HKEY_LOCAL_MACHINE, SPOOLER_REG_KEY + printerName, "SpoolDirectory"); + if (regValue == null || regValue.isEmpty()) { + if (defaultSpoolerLocation == null || defaultSpoolerLocation.isEmpty()) { + defaultSpoolerLocation = getRegString(HKEY_LOCAL_MACHINE, SPOOLER_REG_KEY, "DefaultSpoolDirectory"); + } + regValue = defaultSpoolerLocation; + } + Path spoolerLocation = Paths.get(regValue); + if (regValue == null || regValue.isEmpty() || !Files.isDirectory(spoolerLocation)) throw new FileNotFoundException("Failed to locate spooler output."); + printerSpoolerLocations.put(printerName, spoolerLocation); + return spoolerLocation; + } + + // gracefully swallow InvocationTargetException + public static Integer getRegInt(HKEY root, String key, String value) { + try { + if (Advapi32Util.registryKeyExists(root, key) && Advapi32Util.registryValueExists(root, key, value)) { + return Advapi32Util.registryGetIntValue(root, key, value); + } + } catch(Exception e) { + log.warn("Couldn't get registry value {}\\\\{}\\\\{}", getHkeyName(root), key, value); + } + return null; + } + + // gracefully swallow InvocationTargetException + public static String getRegString(HKEY root, String key, String value) { + try { + if (Advapi32Util.registryKeyExists(root, key) && Advapi32Util.registryValueExists(root, key, value)) { + return Advapi32Util.registryGetStringValue(root, key, value); + } + } catch(Exception e) { + log.warn("Couldn't get registry value {}\\\\{}\\\\{}", getHkeyName(root), key, value); + } + return null; + } + + /** + * Deletes all matching data values directly beneath the specified key + */ + public static boolean deleteRegData(HKEY root, String key, String data) { + boolean success = true; + if (Advapi32Util.registryKeyExists(root, key)) { + for(Map.Entry entry : Advapi32Util.registryGetValues(root, key).entrySet()) { + if(entry.getValue().equals(data)) { + try { + Advapi32Util.registryDeleteValue(root, key, entry.getKey()); + } catch(Exception e) { + log.warn("Couldn't delete value {}\\\\{}\\\\{}", getHkeyName(root), key, entry.getKey()); + success = false; + } + } + } + } + return success; + } + + // gracefully swallow InvocationTargetException + public static String[] getRegMultiString(HKEY root, String key, String value) { + try { + if (Advapi32Util.registryKeyExists(root, key) && Advapi32Util.registryValueExists(root, key, value)) { + return Advapi32Util.registryGetStringArray(root, key, value); + } + } catch(Exception e) { + log.warn("Couldn't get registry value {}\\{}\\{}", root, key, value); + } + return null; + } + + // gracefully swallow InvocationTargetException + public static boolean deleteRegKey(HKEY root, String key) { + try { + if (Advapi32Util.registryKeyExists(root, key)) { + Advapi32Util.registryDeleteKey(root, key); + return true; + } + } catch(Exception e) { + log.warn("Couldn't delete value {}\\\\{}", getHkeyName(root), key); + } + return false; + } + + // gracefully swallow InvocationTargetException + public static boolean deleteRegValue(HKEY root, String key, String value) { + try { + if (Advapi32Util.registryValueExists(root, key, value)) { + Advapi32Util.registryDeleteValue(root, key, value); + return true; + } + } catch(Exception e) { + log.warn("Couldn't delete value {}\\\\{}\\\\{}", getHkeyName(root), key, value); + } + return false; + } + + /** + * Adds a registry entry at key/0, incrementing as needed + */ + public static boolean addNumberedRegValue(HKEY root, String key, Object data) { + try { + // Recursively create keys as needed + String partialKey = ""; + for(String section : key.split("\\\\")) { + if (partialKey.isEmpty()) { + partialKey += section; + } else { + partialKey += "\\" + section; + } + if(!Advapi32Util.registryKeyExists(root, partialKey)) { + Advapi32Util.registryCreateKey(root, partialKey); + } + } + // Make sure it doesn't already exist + for(Map.Entry entry : Advapi32Util.registryGetValues(root, key).entrySet()) { + if(entry.getValue().equals(data)) { + log.info("Registry data {}\\\\{}\\\\{} already has {}, skipping.", getHkeyName(root), key, entry.getKey(), data); + return true; + } + } + // Find the next available number and iterate + int counter=0; + while(Advapi32Util.registryValueExists(root, key, counter + "")) { + counter++; + } + String value = String.valueOf(counter); + if (data instanceof String) { + Advapi32Util.registrySetStringValue(root, key, value, (String)data); + } else if (data instanceof Integer) { + Advapi32Util.registrySetIntValue(root, key, value, (Integer)data); + } else { + throw new Exception("Registry values of type " + data.getClass() + " aren't supported"); + } + return true; + } catch(Exception e) { + log.error("Could not write numbered registry value at {}\\\\{}", getHkeyName(root), key, e); + } + return false; + } + + public static boolean addRegValue(HKEY root, String key, String value, Object data) { + try { + // Recursively create keys as needed + String partialKey = ""; + for(String section : key.split("\\\\")) { + if (partialKey.isEmpty()) { + partialKey += section; + } else { + partialKey += "\\" + section; + } + if(!Advapi32Util.registryKeyExists(root, partialKey)) { + Advapi32Util.registryCreateKey(root, partialKey); + } + } + if (data instanceof String) { + Advapi32Util.registrySetStringValue(root, key, value, (String)data); + } else if (data instanceof Integer) { + Advapi32Util.registrySetIntValue(root, key, value, (Integer)data); + } else { + throw new Exception("Registry values of type " + data.getClass() + " aren't supported"); + } + return true; + } catch(Exception e) { + log.error("Could not write registry value {}\\\\{}\\\\{}", getHkeyName(root), key, value, e); + } + return false; + } + + /** + * Use reflection to get readable HKEY name, useful for debugging errors + */ + private static String getHkeyName(HKEY hkey) { + for(Field f : WinReg.class.getFields()) { + if (f.getName().startsWith("HKEY_")) { + try { + if (f.get(HKEY.class).equals(hkey)) { + return f.getName(); + } + } catch(IllegalAccessException e) { + log.warn("Can't get name of HKEY", e); + } + } + } + return "UNKNOWN"; + } + + public static void setWritable(Path path) { + try { + UserPrincipal authenticatedUsers = path.getFileSystem().getUserPrincipalLookupService() + .lookupPrincipalByGroupName(Advapi32Util.getAccountBySid(AUTHENTICATED_USERS_SID).name); + AclFileAttributeView view = Files.getFileAttributeView(path, AclFileAttributeView.class); + + // Create ACL to give "Authenticated Users" "modify" access + AclEntry entry = AclEntry.newBuilder() + .setType(AclEntryType.ALLOW) + .setPrincipal(authenticatedUsers) + .setFlags(DIRECTORY_INHERIT, + FILE_INHERIT) + .setPermissions(WRITE_NAMED_ATTRS, + WRITE_ATTRIBUTES, + DELETE, + WRITE_DATA, + READ_ACL, + APPEND_DATA, + READ_ATTRIBUTES, + READ_DATA, + EXECUTE, + SYNCHRONIZE, + READ_NAMED_ATTRS) + .build(); + + List acl = view.getAcl(); + acl.add(0, entry); // insert before any DENY entries + view.setAcl(acl); + } catch(IOException e) { + log.warn("Could not set writable: {}", path, e); + } + } + + static String getHostName() { + String hostName = null; + try { + // GetComputerName() is limited to 15 chars, use GetComputerNameEx instead + char buffer[] = new char[255]; + IntByReference lpnSize = new IntByReference(buffer.length); + Kernel32.INSTANCE.GetComputerNameEx(WinBase.COMPUTER_NAME_FORMAT.ComputerNameDnsHostname, buffer, lpnSize); + hostName = Native.toString(buffer).toUpperCase(Locale.ENGLISH); // Force uppercase for backwards compatibility + } catch(Throwable ignore) {} + if(hostName == null || hostName.trim().isEmpty()) { + log.warn("Couldn't get hostname using Kernel32, will fallback to environmental variable COMPUTERNAME instead"); + hostName = System.getenv("COMPUTERNAME"); // always uppercase + } + return hostName; + } + + public static boolean nativeFileCopy(Path source, Path destination) { + try { + ShellAPI.SHFILEOPSTRUCT op = new ShellAPI.SHFILEOPSTRUCT(); + op.wFunc = ShellAPI.FO_COPY; + op.fFlags = Shell32.FOF_NOCOPYSECURITYATTRIBS | Shell32.FOF_NOCONFIRMATION; + op.pFrom = op.encodePaths(new String[] {source.toString()}); + op.pTo = op.encodePaths(new String[] {destination.toString()}); + return Shell32.INSTANCE.SHFileOperation(op) == 0 && op.fAnyOperationsAborted == false; + } catch(Throwable t) { + log.warn("Unable to perform native file copy using JNA", t); + } + return false; + } + + public static boolean elevatedFileCopy(Path source, Path destination) { + // Recursively start powershell.exe, but elevated + String args = String.format("'Copy-Item',-Path,'%s',-Destination,'%s'", source, destination); + String[] command = {"Start-Process", "powershell.exe", "-ArgumentList", args, "-Wait", "-Verb", "RunAs"}; + return ShellUtilities.execute("powershell.exe", "-command", String.join(" ", command)); + } + + static int getProcessId() { + if(pid == null) { + try { + pid = Kernel32.INSTANCE.GetCurrentProcessId(); + } + catch(UnsatisfiedLinkError | NoClassDefFoundError e) { + log.warn("Could not obtain process ID. This usually means JNA isn't working. Returning -1."); + pid = -1; + } + } + return pid; + } + + public static boolean isWindowsXP() { + return isWindows() && OS_NAME.contains("xp"); + } + + + /** + * Detect 32-bit JVM on 64-bit Windows + * @return + */ + public static boolean isWow64() { + if(isWow64 == null) { + isWow64 = false; + if (SystemUtilities.isWindows()) { + if (SystemUtilities.getArch() != Arch.X86_64) { + isWow64 = System.getenv("PROGRAMFILES(x86)") != null; + } + } + } + return isWow64; + } + public static boolean isSystemAccount() { + String whoami = SystemUtilities.whoami(); + return "system".equalsIgnoreCase(whoami) || + "nt authority\\system".equalsIgnoreCase(whoami) || + // Special handling for session-less logins + (getHostName() + "$").equalsIgnoreCase(whoami); + } +} diff --git a/old code/tray/src/qz/ws/HttpAboutServlet.java b/old code/tray/src/qz/ws/HttpAboutServlet.java new file mode 100755 index 0000000..8e81516 --- /dev/null +++ b/old code/tray/src/qz/ws/HttpAboutServlet.java @@ -0,0 +1,193 @@ +package qz.ws; + +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.eclipse.jetty.servlet.DefaultServlet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.jetty.servlet.FilterHolder; +import qz.common.AboutInfo; +import qz.installer.certificate.CertificateManager; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; + +/** + * HTTP JSON endpoint for serving QZ Tray information + */ +public class HttpAboutServlet extends DefaultServlet { + + private static final Logger log = LogManager.getLogger(PrintSocketServer.class); + + private static final int JSON_INDENT = 2; + private CertificateManager certificateManager; + private String allowOrigin; + + public HttpAboutServlet(CertificateManager certificateManager, String allowOrigin) { + this.certificateManager = certificateManager; + this.allowOrigin = allowOrigin; + } + + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) { + response.setHeader("Access-Control-Allow-Origin", allowOrigin); + if ("application/json".equals(request.getHeader("Accept")) || "/json".equals(request.getServletPath())) { + generateJsonResponse(request, response); + } else if ("application/x-x509-ca-cert".equals(request.getHeader("Accept")) || request.getServletPath().startsWith("/cert/")) { + generateCertResponse(request, response); + } else { + generateHtmlResponse(request, response); + } + } + + private void generateHtmlResponse(HttpServletRequest request, HttpServletResponse response) { + StringBuilder display = new StringBuilder(); + + display.append("") + .append("") + .append("") + .append("

About

"); + + display.append(newTable()); + + JSONObject aboutData = AboutInfo.gatherAbout(request.getServerName(), certificateManager); + try { + display.append(generateFromKeys(aboutData, true)); + } + catch(JSONException e) { + log.error("Failed to read JSON data", e); + display.append("Failed to write information"); + } + display.append(""); + + display.append(""); + + try { + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("text/html"); + response.getOutputStream().print(display.toString()); + } + catch(Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.warn("Exception occurred loading html page {}", e.getMessage()); + } + } + + private void generateJsonResponse(HttpServletRequest request, HttpServletResponse response) { + JSONObject aboutData = AboutInfo.gatherAbout(request.getServerName(), certificateManager); + + try { + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("application/json"); + response.getOutputStream().write(aboutData.toString(JSON_INDENT).getBytes(StandardCharsets.UTF_8)); + } + catch(Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.warn("Exception occurred writing JSONObject {}", aboutData); + } + } + + private void generateCertResponse(HttpServletRequest request, HttpServletResponse response) { + try { + String alias = request.getServletPath().split("/")[2]; + String certData = AboutInfo.formatCert(certificateManager.getKeyPair(alias).getCert().getEncoded()); + + if (certData != null) { + response.setStatus(HttpServletResponse.SC_OK); + response.setContentType("application/x-x509-ca-cert"); + + response.getOutputStream().print(certData); + } else { + response.setStatus(HttpServletResponse.SC_NOT_FOUND); + response.getOutputStream().print("Could not find certificate with alias \"" + alias + "\" to download."); + } + } + catch(Exception e) { + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + log.warn("Exception occurred loading certificate: {}", e.getMessage()); + } + } + + private StringBuilder generateFromKeys(JSONObject obj, boolean printTitle) throws JSONException { + StringBuilder rows = new StringBuilder(); + + Iterator itr = obj.keys(); + while(itr.hasNext()) { + String key = (String)itr.next(); + + if (printTitle) { + rows.append(titleRow(key)); + } + + if (obj.optJSONObject(key) != null) { + rows.append(generateFromKeys(obj.getJSONObject(key), false)); + } else { + if ("data".equals(key)) { //special case - replace with a "Download" button + obj.put(key, "Download certificate"); + } + rows.append(contentRow(key, obj.get(key))); + } + } + + return rows; + } + + + private String newTable() { + return ""; + } + + private String titleRow(String title) { + return ""; + } + + private String contentRow(String key, Object value) throws JSONException { + if (value instanceof JSONArray) { + return contentRow(key, (JSONArray)value); + } else { + return contentRow(key, String.valueOf(value)); + } + } + + private String contentRow(String key, JSONArray value) throws JSONException { + StringBuilder valueCell = new StringBuilder(); + for(int i = 0; i < value.length(); i++) { + if (value.optJSONObject(i) != null) { + valueCell.append(newTable()); + valueCell.append(generateFromKeys(value.getJSONObject(i), false)); + valueCell.append("
" + title + "
"); + } else { + valueCell.append(value.getString(i)).append("
"); + } + } + + return contentRow(key, valueCell.toString()); + } + + private String contentRow(String key, String value) { + return "" + key + " " + value + ""; + } + + /** + * Support for preflight header filters per https://wicg.github.io/private-network-access/ + * - Origin filter + * - Private-network check + */ + public static FilterHolder originFilter(String allowOrigin) { + return new FilterHolder((servletRequest, servletResponse, filterChain) -> { + HttpServletResponse response = (HttpServletResponse)servletResponse; + HttpServletRequest request = (HttpServletRequest)servletRequest; + response.setHeader("Access-Control-Allow-Origin", allowOrigin); + if("true".equals(request.getHeader("Access-Control-Request-Private-Network"))) { + // Only add header if it was specified by the browser + response.setHeader("Access-Control-Allow-Private-Network", "true"); + } + filterChain.doFilter(request, response); + }); + } + +} diff --git a/old code/tray/src/qz/ws/PrintSocketClient.java b/old code/tray/src/qz/ws/PrintSocketClient.java new file mode 100755 index 0000000..f23b0a7 --- /dev/null +++ b/old code/tray/src/qz/ws/PrintSocketClient.java @@ -0,0 +1,883 @@ +package qz.ws; +import jssc.SerialPortException; +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.*; +import org.eclipse.jetty.websocket.api.exceptions.CloseException; +import org.eclipse.jetty.websocket.api.exceptions.WebSocketException; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeRequest; +import org.eclipse.jetty.websocket.server.JettyServerUpgradeResponse; +import org.usb4java.LoaderException; +import qz.auth.Certificate; +import qz.auth.RequestState; +import qz.common.Constants; +import qz.common.TrayManager; +import qz.communication.*; +import qz.printer.PrintServiceMatcher; +import qz.printer.status.StatusMonitor; +import qz.utils.*; +import qz.ws.substitutions.Substitutions; + +import qz.auth.PairingAuth; +import javax.servlet.http.HttpServletResponse; +import javax.usb.util.UsbUtil; +import java.awt.*; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.io.Reader; +import java.net.InetSocketAddress; +import java.nio.channels.ClosedChannelException; +import java.nio.file.*; +import java.security.cert.CertificateException; +import java.util.ArrayList; +import java.util.Base64; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.util.HashMap; +import java.util.Locale; +import java.util.concurrent.Semaphore; +import java.util.concurrent.TimeoutException; + + +@WebSocket +public class PrintSocketClient { + + private static final Logger log = LogManager.getLogger(PrintSocketClient.class); + + private final TrayManager trayManager = PrintSocketServer.getTrayManager(); + + private static final Semaphore dialogAvailable = new Semaphore(1, true); + + //websocket port -> Connection + private static final HashMap openConnections = new HashMap<>(); + + private Server server; + + // Shared secret for HMAC authentication (should be securely loaded) + private static final String PAIRING_SECRET = "your_pairing_secret_here"; + private static final String HMAC_ALGO = "HmacSHA256"; + + /** + * Verifies HMAC for a message and secret + */ + private boolean verifyHMAC(String message, String hmac) { + try { + Mac mac = Mac.getInstance(HMAC_ALGO); + SecretKeySpec keySpec = new SecretKeySpec(PAIRING_SECRET.getBytes(), HMAC_ALGO); + mac.init(keySpec); + byte[] computed = mac.doFinal(message.getBytes()); + String computedBase64 = Base64.getEncoder().encodeToString(computed); + return computedBase64.equals(hmac); + } catch (Exception e) { + log.error("HMAC verification failed", e); + return false; + } + } + + + public PrintSocketClient(Server server) { + this.server = server; + // Warn if pairing config is missing but allow QZ Tray to start + if (!PairingAuth.isConfigured()) { + String configPath = PairingAuth.getConfigFilePath(); + log.warn("Pairing configuration missing. Expected at: {}", configPath); + log.warn("Please configure via Advanced > Pairing Configuration menu."); + trayManager.displayWarningMessage("⚠️ Pairing config missing.\nExpected at: " + configPath + "\nConfigure via: Advanced > Pairing Configuration"); + } else { + log.info("Pairing configured for site: {} (from: {})", PairingAuth.getSite(), PairingAuth.getConfigFilePath()); + } + } + + + @OnWebSocketConnect + public void onConnect(Session session) { + // Allow WebSocket connections (they come from the browser, not the Flask server) + // HMAC authentication will be checked on each message + log.info("Connection opened from {} on socket port {}", session.getRemoteAddress(), ((InetSocketAddress)session.getLocalAddress()).getPort()); + trayManager.displayInfoMessage("Client connected"); + openConnections.put(((InetSocketAddress)session.getRemoteAddress()).getPort(), new SocketConnection(Certificate.UNKNOWN)); + } + + @OnWebSocketClose + public void onClose(Session session, int closeCode, String reason) { + log.info("Connection closed: {} - {}", closeCode, reason); + trayManager.displayInfoMessage("Client disconnected"); + + Integer port = ((InetSocketAddress)session.getRemoteAddress()).getPort(); + SocketConnection closed = openConnections.remove(port); + if (closed != null) { + try { + closed.disconnect(); + } + catch(Exception e) { + log.error("Failed to close communication channel", e); + } + } + } + + @OnWebSocketError + public void onError(Session session, Throwable error) { + if (error instanceof EOFException || error instanceof ClosedChannelException) { return; } + + if (error instanceof CloseException && error.getCause() instanceof TimeoutException) { + log.error("Timeout error (Lost connection with client)", error); + return; + } + + log.error("Connection error", error); + trayManager.displayErrorMessage(error.getMessage()); + } + + @OnWebSocketMessage + public void onMessage(Session session, Reader reader) throws IOException { + String message = IOUtils.toString(reader); + if (message == null || message.isEmpty()) { + sendError(session, null, "Message is empty"); + return; + } + + // Warn if pairing config is not set, but allow operation + if (!PairingAuth.isConfigured()) { + log.warn("Message received but pairing not configured. HMAC validation skipped."); + } else { + // HMAC authentication: compute expected signature + String expectedHmac = PairingAuth.hmacSignature(message); + if (expectedHmac == null) { + sendError(session, null, "HMAC signature computation failed."); + return; + } + log.debug("Expected HMAC for message: {}", expectedHmac); + // Note: The HMAC should be checked against a value sent with the message + // For now, we're just logging it for debugging + } + + JSONObject jsonMsg = null; + try { + jsonMsg = new JSONObject(message); + } catch (JSONException e) { + log.error("Bad JSON: {}", e.getMessage()); + sendError(session, null, e); + return; + } + + String UID = null; + try { + JSONObject json = cleanupMessage(jsonMsg); + log.debug("Message: {}", json); + UID = json.optString("uid"); + + Integer connectionPort = ((InetSocketAddress)session.getRemoteAddress()).getPort(); + SocketConnection connection = openConnections.get(connectionPort); + RequestState request = new RequestState(connection.getCertificate(), json); + final String tUID = UID; + new Thread(() -> { + try { + processMessage(session, json, connection, request); + } + catch(UnsatisfiedLinkError | LoaderException e) { + log.error("A component is missing or broken, preventing this feature from working", e); + sendError(session, tUID, "Sorry, this feature is unavailable at this time"); + } + catch(JSONException e) { + log.error("Bad JSON: {}", e.getMessage()); + sendError(session, tUID, e); + } + catch(InvalidPathException | FileSystemException e) { + log.error("FileIO exception occurred", e); + sendError(session, tUID, String.format("FileIO exception occurred: %s: %s", e.getClass().getSimpleName(), e.getMessage())); + } + catch(Exception e) { + log.error("Problem processing message", e); + sendError(session, tUID, e); + } + }).start(); + } + // JSONException is not thrown here, so no catch needed + catch(Exception e) { + log.error("Problem processing message", e); + sendError(session, UID, e); + } + } + + private JSONObject cleanupMessage(JSONObject msg) { + msg.remove("promise"); //never needed java side + + //remove unused properties from older js api's + SocketMethod call = SocketMethod.findFromCall(msg.optString("call")); + if (!call.isDialogShown()) { + msg.remove("signature"); + msg.remove("signAlgorithm"); + } + + return msg; + } + + private boolean validSignature(Certificate certificate, JSONObject message) throws JSONException { + JSONObject copy = new JSONObject(message, new String[] {"call", "params", "timestamp"}); + String signature = message.optString("signature"); + String algorithm = message.optString("signAlgorithm", "SHA1").toUpperCase(Locale.ENGLISH); + + return certificate.isSignatureValid(Certificate.Algorithm.valueOf(algorithm), signature, copy.toString().replaceAll("\\\\/", "/")); + } + + /** + * Determine which method was called from web API + * + * @param session WebSocket session + * @param json JSON received from web API + */ + private void processMessage(Session session, JSONObject json, SocketConnection connection, RequestState request) throws JSONException, SerialPortException, DeviceException, IOException { + // perform client-side substitutions + if(Substitutions.areActive()) { + Substitutions substitutions = Substitutions.getInstance(); + if (substitutions != null) { + json = substitutions.replace(json); + } + } + + String UID = json.optString("uid"); + SocketMethod call = SocketMethod.findFromCall(json.optString("call")); + JSONObject params = json.optJSONObject("params"); + if (params == null) { params = new JSONObject(); } + + if (call == SocketMethod.INVALID && (UID == null || UID.isEmpty())) { + //incorrect message format, likely incompatible qz version + session.close(4003, "Connected to incompatible " + Constants.ABOUT_TITLE + " version"); + return; + } + + String prompt = call.getDialogPrompt(); + if (call == SocketMethod.PRINT) { + //special formatting for print dialogs + JSONObject pr = params.optJSONObject("printer"); + if (pr != null) { + prompt = String.format(prompt, pr.optString("name", pr.optString("file", pr.optString("host", "an undefined location")))); + } else { + sendError(session, UID, "A printer must be specified before printing"); + return; + } + } + + // CERTIFICATE CHECK DISABLED - We use pairing key authentication instead + // Original QZ Tray certificate validation is bypassed + // All connections are allowed; authentication is handled by pairing keys + /* + if (call.isDialogShown() + && !allowedFromDialog(request, prompt, findDialogPosition(session, json.optJSONObject("position")))) { + sendError(session, UID, "Request blocked"); + return; + } + */ + + // Log that we're using pairing-based authentication + if (call.isDialogShown()) { + log.debug("Request allowed via pairing-key authentication (certificate check bypassed): {}", call.getCallName()); + } + + if (call != SocketMethod.GET_VERSION) { + trayManager.voidIdleActions(); + } + + // used in usb calls + DeviceOptions dOpts = new DeviceOptions(params, DeviceOptions.DeviceMode.parse(call.getCallName())); + + + //call appropriate methods + switch(call) { + case PRINTERS_GET_DEFAULT: + sendResult(session, UID, PrintServiceMatcher.getDefaultPrinter() == null? null: + PrintServiceMatcher.getDefaultPrinter().getName()); + break; + case PRINTERS_FIND: + if (params.has("query")) { + String name = PrintServiceMatcher.findPrinterName(params.getString("query")); + if (name != null) { + sendResult(session, UID, name); + } else { + sendError(session, UID, "Specified printer could not be found."); + } + } else { + JSONArray services = PrintServiceMatcher.getPrintersJSON(false); + JSONArray names = new JSONArray(); + for(int i = 0; i < services.length(); i++) { + names.put(services.getJSONObject(i).getString("name")); + } + + sendResult(session, UID, names); + } + break; + case PRINTERS_DETAIL: + sendResult(session, UID, PrintServiceMatcher.getPrintersJSON(true)); + break; + case PRINTERS_START_LISTENING: + if (StatusMonitor.startListening(connection, session, params)) { + sendResult(session, UID, null); + } else { + sendError(session, UID, "Listening failed."); + } + break; + case PRINTERS_GET_STATUS: + if (StatusMonitor.isListening(connection)) { + StatusMonitor.sendStatuses(connection); + } else { + sendError(session, UID, "No printer listeners started for this client."); + } + sendResult(session, UID, null); + break; + case PRINTERS_STOP_LISTENING: + StatusMonitor.stopListening(connection); + sendResult(session, UID, null); + break; + case PRINTERS_CLEAR_QUEUE: + PrintingUtilities.cancelJobs(session, UID, params); + sendResult(session, UID, null); + break; + case PRINT: + PrintingUtilities.processPrintRequest(session, UID, params); + break; + + case SERIAL_FIND_PORTS: + sendResult(session, UID, SerialUtilities.getSerialPortsJSON()); + break; + case SERIAL_OPEN_PORT: + SerialUtilities.setupSerialPort(session, UID, connection, params); + break; + case SERIAL_SEND_DATA: { + SerialOptions opts = null; + //properties param is deprecated legacy here and will be overridden by options if provided + if (!params.isNull("properties")) { + opts = new SerialOptions(params.optJSONObject("properties"), false); + } + if (!params.isNull("options")) { + opts = new SerialOptions(params.optJSONObject("options"), false); + } + + SerialIO serial = connection.getSerialPort(params.optString("port")); + if (serial != null) { + serial.sendData(params, opts); + sendResult(session, UID, null); + } else { + sendError(session, UID, String.format("Serial port [%s] must be opened first.", params.optString("port"))); + } + break; + } + case SERIAL_CLOSE_PORT: { + SerialIO serial = connection.getSerialPort(params.optString("port")); + if (serial != null) { + serial.close(); + connection.removeSerialPort(params.optString("port")); + sendResult(session, UID, null); + } else { + sendError(session, UID, String.format("Serial port [%s] is not open.", params.optString("port"))); + } + break; + } + + case SOCKET_OPEN_PORT: + SocketUtilities.setupSocket(session, UID, connection, params); + break; + case SOCKET_SEND_DATA: { + String location = String.format("%s:%s", params.optString("host"), params.optInt("port")); + SocketIO socket = connection.getNetworkSocket(location); + if (socket != null) { + socket.sendData(params); + sendResult(session, UID, null); + } else { + sendError(session, UID, String.format("Socket [%s] is not open.", location)); + } + break; + } + case SOCKET_CLOSE_PORT: { + String location = String.format("%s:%s", params.optString("host"), params.optInt("port")); + SocketIO socket = connection.getNetworkSocket(location); + if (socket != null) { + socket.close(); + connection.removeNetworkSocket(location); + sendResult(session, UID, null); + } else { + sendError(session, UID, String.format("Socket [%s] is not open.", location)); + } + break; + } + + case USB_LIST_DEVICES: + sendResult(session, UID, UsbUtilities.getUsbDevicesJSON(params.getBoolean("includeHubs"))); + break; + case USB_LIST_INTERFACES: + sendResult(session, UID, UsbUtilities.getDeviceInterfacesJSON(dOpts)); + break; + case USB_LIST_ENDPOINTS: + sendResult(session, UID, UsbUtilities.getInterfaceEndpointsJSON(dOpts)); + break; + case HID_LIST_DEVICES: + if (SystemUtilities.isWindows()) { + sendResult(session, UID, PJHA_HidUtilities.getHidDevicesJSON()); + } else { + sendResult(session, UID, H4J_HidUtilities.getHidDevicesJSON()); + } + break; + case HID_START_LISTENING: + if (!connection.isDeviceListening()) { + if (SystemUtilities.isWindows()) { + connection.startDeviceListening(new PJHA_HidListener(session)); + } else { + connection.startDeviceListening(new H4J_HidListener(session)); + } + sendResult(session, UID, null); + } else { + sendError(session, UID, "Already listening HID device events"); + } + break; + case HID_STOP_LISTENING: + if (connection.isDeviceListening()) { + connection.stopDeviceListening(); + sendResult(session, UID, null); + } else { + sendError(session, UID, "Not already listening HID device events"); + } + break; + + case USB_CLAIM_DEVICE: + case HID_CLAIM_DEVICE: { + if (connection.getDevice(dOpts) == null) { + DeviceIO device; + if (call == SocketMethod.USB_CLAIM_DEVICE) { + device = new UsbIO(dOpts, connection); + } else { + if (SystemUtilities.isWindows()) { + device = new PJHA_HidIO(dOpts, connection); + } else { + device = new H4J_HidIO(dOpts, connection); + } + } + + if (session.isOpen()) { + connection.openDevice(device, dOpts); + } + + if (device.isOpen()) { + sendResult(session, UID, null); + } else { + sendError(session, UID, "Failed to open connection to device"); + } + } else { + sendError(session, UID, String.format("USB Device [v:%s p:%s] is already claimed.", params.opt("vendorId"), params.opt("productId"))); + } + + break; + } + case USB_CLAIMED: + case HID_CLAIMED: { + sendResult(session, UID, connection.getDevice(dOpts) != null); + break; + } + case USB_SEND_DATA: + case HID_SEND_FEATURE_REPORT: + case HID_SEND_DATA: { + DeviceIO usb = connection.getDevice(dOpts); + if (usb != null) { + + if (call == SocketMethod.HID_SEND_FEATURE_REPORT) { + usb.sendFeatureReport(DeviceUtilities.getDataBytes(params, null), dOpts.getEndpoint()); + } else { + usb.sendData(DeviceUtilities.getDataBytes(params, null), dOpts.getEndpoint()); + } + + sendResult(session, UID, null); + } else { + sendError(session, UID, String.format("USB Device [v:%s p:%s] must be claimed first.", params.opt("vendorId"), params.opt("productId"))); + } + + break; + } + case USB_READ_DATA: + case HID_GET_FEATURE_REPORT: + case HID_READ_DATA: { + DeviceIO usb = connection.getDevice(dOpts); + if (usb != null) { + byte[] response; + + if (call == SocketMethod.HID_GET_FEATURE_REPORT) { + response = usb.getFeatureReport(dOpts.getResponseSize(), dOpts.getEndpoint()); + } else { + response = usb.readData(dOpts.getResponseSize(), dOpts.getEndpoint()); + } + + + JSONArray hex = new JSONArray(); + for(byte b : response) { + hex.put(UsbUtil.toHexString(b)); + } + sendResult(session, UID, hex); + } else { + sendError(session, UID, String.format("USB Device [v:%s p:%s] must be claimed first.", params.opt("vendorId"), params.opt("productId"))); + } + + break; + } + case USB_OPEN_STREAM: + case HID_OPEN_STREAM: { + StreamEvent.Stream stream = (call == SocketMethod.USB_OPEN_STREAM? StreamEvent.Stream.USB:StreamEvent.Stream.HID); + UsbUtilities.setupUsbStream(session, UID, connection, dOpts, stream); + break; + } + case USB_CLOSE_STREAM: + case HID_CLOSE_STREAM: { + DeviceIO usb = connection.getDevice(dOpts); + if (usb != null && usb.isStreaming()) { + usb.setStreaming(false); + sendResult(session, UID, null); + } else { + sendError(session, UID, String.format("USB Device [v:%s p:%s] is not streaming data.", params.opt("vendorId"), params.opt("productId"))); + } + + break; + } + case USB_RELEASE_DEVICE: + case HID_RELEASE_DEVICE: { + DeviceIO usb = connection.getDevice(dOpts); + if (usb != null) { + usb.close(); + + sendResult(session, UID, null); + } else { + sendError(session, UID, String.format("USB Device [v:%s p:%s] is not claimed.", params.opt("vendorId"), params.opt("productId"))); + } + + break; + } + + case FILE_START_LISTENING: { + FileParams fileParams = new FileParams(params); + Path absPath = FileUtilities.getAbsolutePath(params, request, true); + FileIO fileIO = new FileIO(session, params, fileParams.getPath(), absPath); + + if (connection.getFileListener(absPath) == null && !fileIO.isWatching()) { + connection.addFileListener(absPath, fileIO); + + FileUtilities.setupListener(fileIO); + sendResult(session, UID, null); + } else { + sendError(session, UID, "Already listening to path events"); + } + + break; + } + case FILE_STOP_LISTENING: { + // Coerce to trusted state for unsigned request + request.setStatus(RequestState.Validity.TRUSTED); + if (params.isNull("path")) { + connection.removeAllFileListeners(); + sendResult(session, UID, null); + } else { + Path absPath = FileUtilities.getAbsolutePath(params, request, true); + FileIO fileIO = connection.getFileListener(absPath); + + if (fileIO != null) { + fileIO.close(); + FileWatcher.deregisterWatch(fileIO); + connection.removeFileListener(absPath); + sendResult(session, UID, null); + } else { + sendError(session, UID, "Not already listening to path events"); + } + } + + break; + } + case FILE_LIST: { + Path absPath = FileUtilities.getAbsolutePath(params, request, true); + + if (Files.exists(absPath)) { + if (Files.isDirectory(absPath)) { + ArrayList files = new ArrayList<>(); + Files.list(absPath).forEach(file -> files.add(file.getFileName().toString())); + sendResult(session, UID, new JSONArray(files)); + } else { + log.error("Failed to list '{}' (not a directory)", absPath); + sendError(session, UID, "Path is not a directory"); + } + } else { + log.error("Failed to list '{}' (does not exist)", absPath); + sendError(session, UID, "Path does not exist"); + } + + break; + } + case FILE_READ: { + FileParams fileParams = new FileParams(params); + Path absPath = FileUtilities.getAbsolutePath(params, request, false); + if (Files.exists(absPath)) { + if (Files.isReadable(absPath)) { + sendResult(session, UID, fileParams.toString(Files.readAllBytes(absPath))); + } else { + log.error("Failed to read '{}' (not readable)", absPath); + sendError(session, UID, "Path is not readable"); + } + } else { + log.error("Failed to read '{}' (does not exist)", absPath); + sendError(session, UID, "Path does not exist"); + } + + break; + } + case FILE_WRITE: { + FileParams fileParams = new FileParams(params); + Path absPath = FileUtilities.getAbsolutePath(params, request, false, true); + + Files.write(absPath, fileParams.getData(), StandardOpenOption.CREATE, fileParams.getAppendMode()); + FileUtilities.inheritParentPermissions(absPath); + sendResult(session, UID, null); + break; + } + case FILE_REMOVE: { + Path absPath = FileUtilities.getAbsolutePath(params, request, false); + + if (Files.exists(absPath)) { + Files.delete(absPath); + sendResult(session, UID, null); + } else { + log.error("Failed to remove '{}' (does not exist)", absPath); + sendError(session, UID, "Path does not exist"); + } + + break; + } + case NETWORKING_DEVICE_LEGACY: + try { + JSONObject networkDevice = NetworkUtilities.getDeviceJSON(params); + JSONObject legacyDevice = new JSONObject(); + legacyDevice.put("ipAddress", networkDevice.optString("ip", null)); + legacyDevice.put("macAddress", networkDevice.optString("mac", null)); + sendResult(session, UID, legacyDevice); + } catch(IOException e) { + sendError(session, UID, "Unable to determine primary network device: " + e.getClass().getSimpleName() + " " + e.getMessage()); + } + break; + case NETWORKING_DEVICE: + try { + sendResult(session, UID, NetworkUtilities.getDeviceJSON(params)); + } catch(IOException e) { + sendError(session, UID, "Unable to determine primary network device: " + e.getClass().getSimpleName() + " " + e.getMessage()); + } + break; + case NETWORKING_DEVICES: + sendResult(session, UID, NetworkUtilities.getDevicesJSON(params)); + break; + case NETWORKING_HOSTNAME: + sendResult(session, UID, SystemUtilities.getHostName()); + break; + case GET_VERSION: + sendResult(session, UID, Constants.VERSION); + break; + case WEBSOCKET_STOP: + log.info("Another instance of {} is asking this to close", Constants.ABOUT_TITLE); + String challenge = json.optString("challenge", ""); + if(SystemUtilities.validateSaltedChallenge(challenge)) { + log.info("Challenge validated: {}, honoring shutdown request", challenge); + + session.close(SingleInstanceChecker.REQUEST_INSTANCE_TAKEOVER); + try { + server.stop(); + } catch(Exception ignore) {} + trayManager.exit(0); + } else { + log.warn("A valid challenge was not provided: {}, ignoring request to close", challenge); + } + break; + case INVALID: + default: + sendError(session, UID, "Invalid function call: " + json.optString("call", "NONE")); + break; + } + } + + // --- PAIRING LOGIC --- + // The following replaces legacy certificate/domain trust logic. + // Only connections with a valid pairing key and allowed server address are accepted. + // Certificate/domain trust checks are now deactivated in favor of pairing-based authentication. + // --------------------- + + private boolean allowedFromDialog(RequestState request, String prompt, Point position) { + //If cert can be resolved before the lock, do so and return + if (request.hasBlockedCert()) { + return false; + } + if (request.hasSavedCert()) { + return true; + } + + //wait until previous prompts are closed + try { + dialogAvailable.acquire(); + } + catch(InterruptedException e) { + log.warn("Failed to acquire dialog", e); + return false; + } + + //prompt user for access + boolean allowed = trayManager.showGatewayDialog(request, prompt, position); + + dialogAvailable.release(); + + return allowed; + } + + private Point findDialogPosition(Session session, JSONObject positionData) { + Point pos = new Point(0, 0); + if (((InetSocketAddress)session.getRemoteAddress()).getAddress().isLoopbackAddress() && positionData != null + && !positionData.isNull("x") && !positionData.isNull("y")) { + pos.move(positionData.optInt("x"), positionData.optInt("y")); + } + + return pos; + } + + + /** + * Send JSON reply to web API for call {@code messageUID} + * + * @param session WebSocket session + * @param messageUID ID of call from web API + * @param returnValue Return value of method call, can be {@code null} + */ + public static void sendResult(Session session, String messageUID, Object returnValue) { + try { + JSONObject reply = new JSONObject(); + reply.put("uid", messageUID); + reply.put("result", returnValue); + send(session, reply); + } + catch(JSONException | ClosedChannelException e) { + log.error("Send result failed", e); + } + } + + /** + * Send JSON error reply to web API for call {@code messageUID} + * + * @param session WebSocket session + * @param messageUID ID of call from web API + * @param ex Exception to get error message from + */ + public static void sendError(Session session, String messageUID, Exception ex) { + String message = ex.getMessage(); + if (message == null || message.isEmpty()) { + message = ex.getClass().getSimpleName(); + } + + sendError(session, messageUID, message); + } + + /** + * Send JSON error reply to web API for call {@code messageUID} + * + * @param session WebSocket session + * @param messageUID ID of call from web API + * @param errorMsg Error from method call + */ + public static void sendError(Session session, String messageUID, String errorMsg) { + try { + JSONObject reply = new JSONObject(); + reply.putOpt("uid", messageUID); + reply.put("error", errorMsg); + send(session, reply); + } + catch(JSONException | ClosedChannelException e) { + log.error("Send error failed", e); + } + } + + /** + * Send JSON data to web API, to be retrieved by callbacks. + * Used for data sent apart from API calls, since UID's correspond to a single response. + * + * @param session WebSocket session + * @param event StreamEvent with data to send down to web API + */ + public static void sendStream(Session session, StreamEvent event) throws ClosedChannelException { + try { + JSONObject stream = new JSONObject(); + stream.put("type", event.getStreamType()); + stream.put("event", event.toJSON()); + send(session, stream); + } + catch(JSONException e) { + log.error("Send stream failed", e); + } + } + + public static void sendStream(Session session, StreamEvent event, DeviceListener listener) { + try { + sendStream(session, event); + } catch(ClosedChannelException e) { + log.error("Stream is closed, could not send message"); + if(listener != null) { + listener.close(); + } else { + log.error("Channel was closed before stream could be sent, but no close handler is configured."); + } + } + } + + public static void sendStream(Session session, StreamEvent event, Runnable closeHandler) { + try { + sendStream(session, event); + } catch(ClosedChannelException e) { + log.error("Stream is closed, could not send message"); + if(closeHandler != null) { + closeHandler.run(); + } else { + log.error("Channel was closed before stream could be sent, but no close handler is configured."); + } + } + } + + /** + * Raw send method for replies + * + * @param session WebSocket session + * @param reply JSON Object of reply to web API + */ + private static synchronized void send(Session session, JSONObject reply) throws WebSocketException, ClosedChannelException { + try { + session.getRemote().sendString(reply.toString()); + } + catch(IOException e) { + if(e instanceof ClosedChannelException) { + throw (ClosedChannelException)e; + } else if(e.getCause() instanceof ClosedChannelException) { + throw (ClosedChannelException)e.getCause(); + } + log.error("Could not send message", e); + } + } + + /** + * Simulate preflight headers origin filter + * + * TODO: Revisit when specification is updated to include WebSockets https://wicg.github.io/private-network-access/#integration-websockets + */ + public static PrintSocketClient originFilterUpgrade(JettyServerUpgradeRequest req, JettyServerUpgradeResponse resp, Server server, String allowOrigin) { + if(!allowOrigin.equals("*")) { + String origin = req.getHeader("Origin"); + if(!allowOrigin.equals(origin)) { + String message = String.format("Connection-supplied origin value '%s' does not match %s: '%s'; WebSocket connection will fail", origin, ArgValue.SECURITY_WSS_ALLOWORIGIN.getMatch(), allowOrigin); + log.error(message); + try { + resp.sendError(HttpServletResponse.SC_FORBIDDEN, message); + } catch(IOException ignore) {} + return null; + } + } + return new PrintSocketClient(server); + } +} \ No newline at end of file diff --git a/old code/tray/src/qz/ws/PrintSocketServer.java b/old code/tray/src/qz/ws/PrintSocketServer.java new file mode 100755 index 0000000..905b714 --- /dev/null +++ b/old code/tray/src/qz/ws/PrintSocketServer.java @@ -0,0 +1,247 @@ +/** + * @author Robert Casto + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.ws; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.eclipse.jetty.http.HttpVersion; +import org.eclipse.jetty.server.*; +import org.eclipse.jetty.servlet.ServletContextHandler; +import org.eclipse.jetty.servlet.ServletHolder; +import org.eclipse.jetty.util.MultiException; +import org.eclipse.jetty.websocket.server.config.JettyWebSocketServletContainerInitializer; +import org.eclipse.jetty.websocket.servlet.WebSocketUpgradeFilter; +import qz.App; +import qz.common.TrayManager; +import qz.installer.certificate.CertificateManager; +import qz.utils.ArgValue; +import qz.utils.PrefsSearch; + +import javax.servlet.*; +import javax.swing.*; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Created by robert on 9/9/2014. + */ + +public class PrintSocketServer { + + private static final Logger log = LogManager.getLogger(PrintSocketServer.class); + + private static final int MAX_MESSAGE_SIZE = Integer.MAX_VALUE; + private static final AtomicBoolean running = new AtomicBoolean(false); + + private static WebsocketPorts websocketPorts; + private static TrayManager trayManager; + private static Server server; + private static boolean httpsOnly; + private static boolean sniStrict; + private static String wssHost; + private static String wssAllowOrigin; + + public static void runServer(CertificateManager certManager, boolean headless) throws InterruptedException, InvocationTargetException { + SwingUtilities.invokeAndWait(() -> { + PrintSocketServer.setTrayManager(new TrayManager(headless)); + }); + + wssHost = PrefsSearch.getString(ArgValue.SECURITY_WSS_HOST, certManager.getProperties()); + wssAllowOrigin = PrefsSearch.getString(ArgValue.SECURITY_WSS_ALLOWORIGIN, certManager.getProperties()); + httpsOnly = PrefsSearch.getBoolean(ArgValue.SECURITY_WSS_HTTPSONLY, certManager.getProperties()); + sniStrict = PrefsSearch.getBoolean(ArgValue.SECURITY_WSS_SNISTRICT, certManager.getProperties()); + websocketPorts = WebsocketPorts.parseFromProperties(); + + server = findAvailableSecurePort(certManager); + + Connector secureConnector = null; + if (server.getConnectors().length > 0 && !server.getConnectors()[0].isFailed()) { + secureConnector = server.getConnectors()[0]; + } + + if (httpsOnly && secureConnector == null) { + log.error("Failed to start in https-only mode"); + return; + } + + while(!running.get() && websocketPorts.insecureBoundsCheck()) { + try { + ServerConnector connector = new ServerConnector(server); + connector.setPort(websocketPorts.getInsecurePort()); + if(httpsOnly) { + server.setConnectors(new Connector[] {secureConnector}); + } else if (secureConnector != null) { + //setup insecure connector before secure + server.setConnectors(new Connector[] {connector, secureConnector}); + } else { + server.setConnectors(new Connector[] {connector}); + } + + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + + // Allow private-network access + context.addFilter(HttpAboutServlet.originFilter(wssAllowOrigin), "/*", null); + + // Handle WebSocket connections + context.addFilter(WebSocketUpgradeFilter.class, "/", EnumSet.of(DispatcherType.REQUEST)); + JettyWebSocketServletContainerInitializer.configure(context, (ctx, container) -> { + container.addMapping("/*", (req, resp) -> PrintSocketClient.originFilterUpgrade(req, resp, server, wssAllowOrigin)); + container.setMaxTextMessageSize(MAX_MESSAGE_SIZE); + container.setIdleTimeout(Duration.ofMinutes(5)); + }); + + // Handle HTTP landing page + ServletHolder httpServlet = new ServletHolder(new HttpAboutServlet(certManager, wssAllowOrigin)); + httpServlet.setInitParameter("resourceBase", "/"); + + context.addServlet(httpServlet, "/"); + context.addServlet(httpServlet, "/json"); + + server.setHandler(context); + server.setStopAtShutdown(true); + server.start(); + + trayManager.setReloadThread(new Thread(() -> { + try { + trayManager.setDangerIcon(); + running.set(false); + websocketPorts.resetIndices(); + server.stop(); + } + catch(Exception e) { + log.error("Failed to reload", e); + trayManager.displayErrorMessage("Error stopping print socket: " + e.getLocalizedMessage()); + } + })); + + running.set(true); + + log.info("Server started on port(s) " + getPorts(server)); + websocketPorts.setHttpsOnly(httpsOnly); + websocketPorts.setHttpOnly(secureConnector == null); + trayManager.setServer(server, websocketPorts); + server.join(); + } + catch(IOException | MultiException e) { + //order of getConnectors is the order we added them -> insecure first + if (server.isFailed()) { + websocketPorts.nextInsecureIndex(); + } + + //explicitly stop the server, because if only 1 port has an exception the other will still be opened + try { server.stop(); }catch(Exception stopEx) { stopEx.printStackTrace(); } + } + catch(Exception e) { + e.printStackTrace(); + trayManager.displayErrorMessage(e.getLocalizedMessage()); + break; + } + } + } + + private static Server findAvailableSecurePort(CertificateManager certManager) { + Server server = new Server(); + + if (certManager != null) { + final AtomicBoolean runningSecure = new AtomicBoolean(false); + while(!runningSecure.get() && websocketPorts.secureBoundsCheck()) { + try { + // Bind the secure socket on the proper port number (i.e. 8181), add it as an additional connector + SslConnectionFactory sslConnection = new SslConnectionFactory(certManager.configureSslContextFactory(), HttpVersion.HTTP_1_1.asString()); + + // Disable SNI checks for easier print-server testing (replicates Jetty 9.x behavior) + HttpConfiguration httpsConfig = new HttpConfiguration(); + SecureRequestCustomizer customizer = new SecureRequestCustomizer(); + customizer.setSniHostCheck(sniStrict); + httpsConfig.addCustomizer(customizer); + + HttpConnectionFactory httpConnection = new HttpConnectionFactory(httpsConfig); + + ServerConnector secureConnector = new ServerConnector(server, sslConnection, httpConnection); + secureConnector.setHost(wssHost); + secureConnector.setPort(websocketPorts.getSecurePort()); + server.setConnectors(new Connector[] {secureConnector}); + + server.start(); + log.trace("Established secure WebSocket on port {}", websocketPorts.getSecurePort()); + + //only starting to test port availability; insecure port will actually start + server.stop(); + runningSecure.set(true); + } + catch(IOException | MultiException e) { + if (server.isFailed()) { + websocketPorts.nextSecureIndex(); + } + + try { server.stop(); }catch(Exception stopEx) { stopEx.printStackTrace(); } + } + catch(Exception e) { + e.printStackTrace(); + trayManager.displayErrorMessage(e.getLocalizedMessage()); + break; + } + } + } + + if (server.getConnectors().length == 0 || server.getConnectors()[0].isFailed()) { + log.warn("Could not start secure WebSocket"); + } + + return server; + } + + public static void setTrayManager(TrayManager manager) { + trayManager = manager; + } + + public static TrayManager getTrayManager() { + return trayManager; + } + + public static Server getServer() { + return server; + } + + public static AtomicBoolean getRunning() { + return running; + } + + @Deprecated + public static void main(String ... args) { + App.main(args); + } + + + public static WebsocketPorts getWebsocketPorts() { + return websocketPorts; + } + + /** + * Returns a String representation of the ports assigned to the specified Server + */ + public static String getPorts(Server server) { + StringBuilder ports = new StringBuilder(); + for(Connector c : server.getConnectors()) { + if (ports.length() > 0) { + ports.append(", "); + } + + ports.append(((ServerConnector)c).getLocalPort()); + } + + return ports.toString(); + } + +} diff --git a/old code/tray/src/qz/ws/SingleInstanceChecker.java b/old code/tray/src/qz/ws/SingleInstanceChecker.java new file mode 100755 index 0000000..254b53f --- /dev/null +++ b/old code/tray/src/qz/ws/SingleInstanceChecker.java @@ -0,0 +1,187 @@ +/** + * @author Kyle Berezin + * + * Copyright (C) 2016 Tres Finocchiaro, QZ Industries, LLC + * + * LGPL 2.1 This is free software. This software and source code are released under + * the "LGPL 2.1 License". A copy of this license should be distributed with + * this software. http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package qz.ws; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.eclipse.jetty.client.HttpClient; +import org.eclipse.jetty.client.http.HttpClientTransportOverHTTP; +import org.eclipse.jetty.io.ClientConnector; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.websocket.api.CloseStatus; +import org.eclipse.jetty.websocket.api.Session; +import org.eclipse.jetty.websocket.api.annotations.*; +import org.eclipse.jetty.websocket.client.ClientUpgradeRequest; +import org.eclipse.jetty.websocket.client.WebSocketClient; +import qz.common.Constants; +import qz.common.TrayManager; +import qz.utils.ArgValue; +import qz.utils.SystemUtilities; + +import java.io.IOException; +import java.net.URI; +import java.time.Duration; +import java.util.Properties; + +/** + * Created by Kyle on 12/1/2015. + */ + +@WebSocket +public class SingleInstanceChecker { + + private static final Logger log = LogManager.getLogger(SingleInstanceChecker.class); + + public static CloseStatus INSTANCE_ALREADY_RUNNING = new CloseStatus(4441, "Already running"); + public static CloseStatus REQUEST_INSTANCE_TAKEOVER = new CloseStatus(4442, "WebSocket stolen"); + + public static final String STEAL_WEBSOCKET_FLAG = "stealWebsocket"; + public static final String STEAL_WEBSOCKET_PROPERTY = "websocket.steal"; + + private static final int AUTO_CLOSE = 6 * 1000; + private static final int TIMEOUT = 3 * 1000; + + public static boolean stealWebsocket; + + private TrayManager trayManager; + private WebSocketClient client; + + + public SingleInstanceChecker(TrayManager trayManager, int port, boolean usingSecure) { + this.trayManager = trayManager; + log.debug("Checking for a running instance of {} on port {}", Constants.ABOUT_TITLE, port); + autoCloseClient(AUTO_CLOSE); + String uri = String.format("%s//localhost:%d", (usingSecure ? "wss:" : "ws:"), port); + connectTo(uri, usingSecure); + } + + private void connectTo(String uri, boolean usingSecure) { + try { + if (client == null) { + if(usingSecure) { + // Self-signed certs won't be trusted, create "trustAll" connector + SslContextFactory.Client sslContextFactory = new SslContextFactory.Client(true); + ClientConnector clientConnector = new ClientConnector(); + clientConnector.setSslContextFactory(sslContextFactory); + HttpClient httpClient = new HttpClient(new HttpClientTransportOverHTTP(clientConnector)); + client = new WebSocketClient(httpClient); + } else { + client = new WebSocketClient(); + } + client.start(); + client.setConnectTimeout(TIMEOUT); + client.setIdleTimeout(Duration.ofMillis(TIMEOUT)); + client.setStopTimeout(TIMEOUT); + } + + URI targetUri = new URI(uri); + ClientUpgradeRequest request = new ClientUpgradeRequest(); + client.connect(this, targetUri, request); + } + catch(Exception e) { + log.warn("Could not connect to url {}", uri, e); + } + } + + @OnWebSocketClose + public void onClose(int statusCode, String reason) { + log.warn("Remote connection closed - {}", reason); + } + + @OnWebSocketError + public void onError(Throwable e) { + if (!e.getMessage().contains("Connection refused") && !e.getMessage().contains("Failed to upgrade to websocket")) { + log.warn("WebSocket error", e); + } + } + + @OnWebSocketConnect + public void onConnect(Session session) { + try { + session.getRemote().sendString(Constants.PROBE_REQUEST); + } + catch(IOException e) { + log.warn("Could not send data to server", e); + session.close(); + } + } + + @OnWebSocketMessage + public void onMessage(Session session, String message) { + if (message.equals(Constants.PROBE_RESPONSE)) { + log.warn("{} is already running on {}", Constants.ABOUT_TITLE, session.getRemoteAddress().toString()); + if(stealWebsocket) { + stealInstance(session); + } else { + shutDown(session); + } + } + } + + private void shutDown(Session session) { + session.close(INSTANCE_ALREADY_RUNNING); + log.info("{} is shutting down now.", Constants.ABOUT_TITLE); + trayManager.exit(0); + } + + private void stealInstance(Session session) { + log.info("Asking other instance of {} to shut down.", Constants.ABOUT_TITLE); + try { + JSONObject reply = new JSONObject(); + reply.put("call", SocketMethod.WEBSOCKET_STOP.getCallName()); + // Send something unique, only an app running on this PC would know + reply.put("challenge", SystemUtilities.calculateSaltedChallenge()); + session.getRemote().sendString(reply.toString()); + log.info("Remote shutdown message delivered."); + } + catch(IOException | JSONException e) { + log.warn("Unable to send message, giving up.", e); + shutDown(session); + } + } + + private void autoCloseClient(final int millis) { + new Thread(() -> { + try { + Thread.sleep(millis); + if (client != null) { + if (!(client.isStopped() || client.isStopping())) { + client.stop(); + } + } + } + catch(Exception ignore) { + log.error("Couldn't close client after delay"); + } + }).start(); + } + + public static void setPreferences(Properties props) { + // Don't override if already set via command line + if(stealWebsocket) { + log.info("Picked up command line flag: {}", ArgValue.STEAL.getMatches()[0]); + } else { + // Don't override if set by System property + stealWebsocket = Boolean.parseBoolean(System.getProperty(STEAL_WEBSOCKET_FLAG, "false")); + if (stealWebsocket) { + log.info("Picked up flag from system property: {}", STEAL_WEBSOCKET_FLAG); + } else { + stealWebsocket = Boolean.parseBoolean(props.getProperty(STEAL_WEBSOCKET_PROPERTY, "false")); + if (stealWebsocket) { + log.info("Picked up flag from properties file: {}", STEAL_WEBSOCKET_PROPERTY); + } + } + } + log.info("If other instances of {} are found, {} INSTANCE will shut down", Constants.ABOUT_TITLE, stealWebsocket ? "the OTHER" : "THIS"); + } +} diff --git a/old code/tray/src/qz/ws/SocketConnection.java b/old code/tray/src/qz/ws/SocketConnection.java new file mode 100755 index 0000000..9e4e1c0 --- /dev/null +++ b/old code/tray/src/qz/ws/SocketConnection.java @@ -0,0 +1,155 @@ +package qz.ws; + +import jssc.SerialPortException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.auth.Certificate; +import qz.communication.*; +import qz.printer.status.StatusMonitor; +import qz.utils.FileWatcher; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; + +public class SocketConnection { + + private static final Logger log = LogManager.getLogger(SocketConnection.class); + + + private Certificate certificate; + + private DeviceListener deviceListener; + + // serial port -> open SerialIO + private final HashMap openSerialPorts = new HashMap<>(); + // socket 'host:port' -> open ProtocolIO + private final HashMap openNetworkSockets = new HashMap<>(); + + // absolute path -> open file listener + private final HashMap openFiles = new HashMap<>(); + + // DeviceOptions -> open DeviceIO + private final HashMap openDevices = new HashMap<>(); + + + public SocketConnection(Certificate cert) { + certificate = cert; + } + + public Certificate getCertificate() { + return certificate; + } + + public void setCertificate(Certificate newCert) { + certificate = newCert; + } + + + public void addSerialPort(String port, SerialIO io) { + openSerialPorts.put(port, io); + } + + public SerialIO getSerialPort(String port) { + return openSerialPorts.get(port); + } + + public void removeSerialPort(String port) { + openSerialPorts.remove(port); + } + + + public void addNetworkSocket(String location, SocketIO io) { + openNetworkSockets.put(location, io); + } + + public SocketIO getNetworkSocket(String location) { + return openNetworkSockets.get(location); + } + + public void removeNetworkSocket(String location) { + openNetworkSockets.remove(location); + } + + + public boolean isDeviceListening() { + return deviceListener != null; + } + + public void startDeviceListening(DeviceListener listener) { + deviceListener = listener; + } + + public void stopDeviceListening() { + if (deviceListener != null) { + deviceListener.close(); + } + deviceListener = null; + } + + public void addFileListener(Path absolute, FileIO listener) { + openFiles.put(absolute, listener); + } + + public FileIO getFileListener(Path absolute) { + return openFiles.get(absolute); + } + + public void removeFileListener(Path absolute) { + openFiles.remove(absolute); + } + + public void removeAllFileListeners() { + for(Path path : openFiles.keySet()) { + openFiles.get(path).close(); + FileWatcher.deregisterWatch(openFiles.get(path)); + } + + openFiles.clear(); + } + + + public void addDevice(DeviceOptions dOpts, DeviceIO io) { + openDevices.put(dOpts, io); + } + + public DeviceIO getDevice(DeviceOptions dOpts) { + return openDevices.get(dOpts); + } + + public void removeDevice(DeviceOptions dOpts) { + openDevices.remove(dOpts); + } + + public synchronized void openDevice(DeviceIO device, DeviceOptions dOpts) throws DeviceException { + device.open(); + if (device.isOpen()) { + addDevice(dOpts, device); + } + } + + /** + * Explicitly closes all open serial and usb connections setup through this object + */ + public synchronized void disconnect() throws SerialPortException, DeviceException, IOException { + log.info("Closing all communication channels for {}", certificate.getCommonName()); + + for(SerialIO sio : openSerialPorts.values()) { + sio.close(); + } + + for(SocketIO pio : openNetworkSockets.values()) { + pio.close(); + } + + for(DeviceIO dio : openDevices.values()) { + dio.setStreaming(false); + dio.close(); + } + + removeAllFileListeners(); + stopDeviceListening(); + StatusMonitor.stopListening(this); + } + +} \ No newline at end of file diff --git a/old code/tray/src/qz/ws/SocketMethod.java b/old code/tray/src/qz/ws/SocketMethod.java new file mode 100755 index 0000000..e2e2e05 --- /dev/null +++ b/old code/tray/src/qz/ws/SocketMethod.java @@ -0,0 +1,101 @@ +package qz.ws; + +public enum SocketMethod { + PRINTERS_GET_DEFAULT("printers.getDefault", true, "access connected printers"), + PRINTERS_FIND("printers.find", true, "access connected printers"), + PRINTERS_DETAIL("printers.detail", true, "access connected printers"), + PRINTERS_START_LISTENING("printers.startListening", true, "listen for printer status"), + PRINTERS_CLEAR_QUEUE("printers.clearQueue", true, "cancel all pending jobs for a given printer"), + PRINTERS_GET_STATUS("printers.getStatus", false), + PRINTERS_STOP_LISTENING("printers.stopListening", false), + PRINT("print", true, "print to %s"), + + SERIAL_FIND_PORTS("serial.findPorts", true, "access serial ports"), + SERIAL_OPEN_PORT("serial.openPort", true, "open a serial port"), + SERIAL_SEND_DATA("serial.sendData", true, "send data over a serial port"), + SERIAL_CLOSE_PORT("serial.closePort", true, "close a serial port"), + + SOCKET_OPEN_PORT("socket.open", true, "open a socket"), + SOCKET_SEND_DATA("socket.sendData", true, "send data over a socket"), + SOCKET_CLOSE_PORT("socket.close", true, "close a socket"), + + USB_LIST_DEVICES("usb.listDevices", true, "access USB devices"), + USB_LIST_INTERFACES("usb.listInterfaces", true, "access USB devices"), + USB_LIST_ENDPOINTS("usb.listEndpoints", true, "access USB devices"), + USB_CLAIM_DEVICE("usb.claimDevice", true, "claim a USB device"), + USB_CLAIMED("usb.isClaimed", false, "check USB claim status"), + USB_SEND_DATA("usb.sendData", true, "use a USB device"), + USB_READ_DATA("usb.readData", true, "use a USB device"), + USB_OPEN_STREAM("usb.openStream", true, "use a USB device"), + USB_CLOSE_STREAM("usb.closeStream", false, "use a USB device"), + USB_RELEASE_DEVICE("usb.releaseDevice", false, "release a USB device"), + + HID_LIST_DEVICES("hid.listDevices", true, "access USB devices"), + HID_START_LISTENING("hid.startListening", true, "listen for USB devices"), + HID_STOP_LISTENING("hid.stopListening", false), + HID_CLAIM_DEVICE("hid.claimDevice", true, "claim a USB device"), + HID_CLAIMED("hid.isClaimed", false, "check USB claim status"), + HID_SEND_DATA("hid.sendData", true, "use a USB device"), + HID_READ_DATA("hid.readData", true, "use a USB device"), + HID_SEND_FEATURE_REPORT("hid.sendFeatureReport", true, "use a USB device"), + HID_GET_FEATURE_REPORT("hid.getFeatureReport", true, "use a USB device"), + HID_OPEN_STREAM("hid.openStream", true, "use a USB device"), + HID_CLOSE_STREAM("hid.closeStream", false, "use a USB device"), + HID_RELEASE_DEVICE("hid.releaseDevice", false, "release a USB device"), + + FILE_LIST("file.list", true, "view the filesystem"), + FILE_START_LISTENING("file.startListening", true, "listen for filesystem events"), + FILE_STOP_LISTENING("file.stopListening", false), + FILE_READ("file.read", true, "read the content of a file"), + FILE_WRITE("file.write", true, "write to a file"), + FILE_REMOVE("file.remove", true, "delete a file"), + + NETWORKING_DEVICE("networking.device", true), + NETWORKING_DEVICES("networking.devices", true), + NETWORKING_HOSTNAME("networking.hostname", true), + NETWORKING_DEVICE_LEGACY("websocket.getNetworkInfo", true), + GET_VERSION("getVersion", false), + + WEBSOCKET_STOP("websocket.stop", false), + + INVALID("", false); + + + private String callName; + private String dialogPrompt; + private boolean dialogShown; + + SocketMethod(String callName, boolean dialogShown) { + this(callName, dialogShown, "access local resources"); + } + + SocketMethod(String callName, boolean dialogShown, String dialogPrompt) { + this.callName = callName; + + this.dialogShown = dialogShown; + this.dialogPrompt = dialogPrompt; + } + + public boolean isDialogShown() { + return dialogShown; + } + + public String getDialogPrompt() { + return dialogPrompt; + } + + public static SocketMethod findFromCall(String call) { + for(SocketMethod m : SocketMethod.values()) { + if (m.callName.equals(call)) { + return m; + } + } + + return INVALID; + } + + public String getCallName() { + return callName; + } + +} diff --git a/old code/tray/src/qz/ws/StreamEvent.java b/old code/tray/src/qz/ws/StreamEvent.java new file mode 100755 index 0000000..4950f40 --- /dev/null +++ b/old code/tray/src/qz/ws/StreamEvent.java @@ -0,0 +1,65 @@ +package qz.ws; + +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class StreamEvent { + + public enum Stream { + SERIAL, USB, HID, PRINTER, FILE, SOCKET + } + + public enum Type { + RECEIVE, ERROR, ACTION + } + + private static final Logger log = LogManager.getLogger(StreamEvent.class); + + private Stream streamType; + private Type eventType; + + private JSONObject eventData; + + + public StreamEvent(Stream streamType, Type eventType) { + this.streamType = streamType; + this.eventType = eventType; + + eventData = new JSONObject(); + } + + public StreamEvent withException(Exception ex) { + String message = ex.getMessage(); + if (message == null) { message = ex.getClass().getSimpleName(); } + + return withData("exception", message); + } + + public StreamEvent withData(String key, Object data) { + try { + eventData.putOpt(key, data); + } + catch(JSONException e) { + log.warn("Failed to save {} as {}", data, key); + } + + return this; + } + + + public String getStreamType() { + return streamType.name(); + } + + public String getEventType() { + return eventType.name(); + } + + public String toJSON() throws JSONException { + eventData.put("type", getEventType()); + return eventData.toString(); + } + +} diff --git a/old code/tray/src/qz/ws/WebsocketPorts.java b/old code/tray/src/qz/ws/WebsocketPorts.java new file mode 100755 index 0000000..ec93fb9 --- /dev/null +++ b/old code/tray/src/qz/ws/WebsocketPorts.java @@ -0,0 +1,157 @@ +package qz.ws; + +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.build.provision.Step; +import qz.build.provision.params.Type; +import qz.common.Constants; +import qz.installer.provision.invoker.PropertyInvoker; +import qz.utils.ArgValue; +import qz.utils.PrefsSearch; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; + +import static qz.common.Constants.PROVISION_FILE; + +public class WebsocketPorts { + private static final Logger log = LogManager.getLogger(WebsocketPorts.class); + private List securePorts; + private List insecurePorts; + + public List getUnusedSecurePorts() { + List unused = new ArrayList<>(securePorts); + unused.remove(securePortIndex.get()); + return unused; + } + + public List getUnusedInsecurePorts() { + List unused = new ArrayList<>(insecurePorts); + unused.remove(insecurePortIndex.get()); + return unused; + } + + public void setHttpsOnly(boolean httpsOnly) { + if(httpsOnly) { + insecurePortIndex.set(-1); + } + } + + public void setHttpOnly(boolean httpOnly) { + if(httpOnly) { + securePortIndex.set(-1); + } + } + + private static final AtomicInteger securePortIndex = new AtomicInteger(0); + private static final AtomicInteger insecurePortIndex = new AtomicInteger(0); + + private WebsocketPorts(List securePorts, List insecurePorts) { + this.securePorts = securePorts; + this.insecurePorts = insecurePorts; + } + + public int getSecurePort() { + return securePorts.get(securePortIndex.get()); + } + + public int getInsecurePort() { + return insecurePorts.get(insecurePortIndex.get()); + } + + public int getSecureIndex() { + return securePortIndex.get(); + } + + public int getInsecureIndex() { + return insecurePortIndex.get(); + } + + public int nextSecureIndex() { + return securePortIndex.incrementAndGet(); + } + + public int nextInsecureIndex() { + return insecurePortIndex.incrementAndGet(); + } + + public void resetIndices() { + securePortIndex.set(0); + insecurePortIndex.set(0); + } + + public boolean secureBoundsCheck() { + return securePortIndex.get() < securePorts.size(); + } + + public boolean insecureBoundsCheck() { + return insecurePortIndex.get() < insecurePorts.size(); + } + + /** + * Parses WebSocket ports from preferences or fallback to defaults is a problem is found + */ + public static WebsocketPorts parseFromProperties() { + return fromList(PrefsSearch.getIntegerArray(ArgValue.WEBSOCKET_SECURE_PORTS), + PrefsSearch.getIntegerArray(ArgValue.WEBSOCKET_INSECURE_PORTS)); + } + + /** + * Loops through steps searching for a property which sets a custom websocket ports + */ + public static WebsocketPorts parseFromSteps(List steps) { + List secure = new ArrayList<>(); + List insecure = new ArrayList<>(); + for(Step step : steps) { + if(step.getType() == Type.PROPERTY) { + HashMap pairs = PropertyInvoker.parsePropertyPairs(step); + String foundPorts; + if((foundPorts = pairs.get(ArgValue.WEBSOCKET_SECURE_PORTS.getMatch())) != null) { + secure = PrefsSearch.parseIntegerArray(foundPorts); + if(!secure.isEmpty()) { + log.info("Picked up custom secure ports from {}: [{}]", PROVISION_FILE, StringUtils.join(secure, ",")); + } + } + if((foundPorts = pairs.get(ArgValue.WEBSOCKET_INSECURE_PORTS.getMatch())) != null) { + insecure = PrefsSearch.parseIntegerArray(foundPorts); + if(!insecure.isEmpty()) { + log.info("Picked up custom insecure ports from {}: [{}]", PROVISION_FILE, StringUtils.join(insecure, ",")); + } + } + } + } + return fromList(secure, insecure); + } + + /** + * Constructs a new instance of WebsocketPorts with the specified secure and + * insecure port ranges, falling back to Constants.DEFAULT_WSS_PORTS, + * Constants.DEFAULT_WS_PORTS if a problem occurred. + * + * @param secure Port listing with at least one element. Size must match insecure + * @param insecure Port listing with at least one element. Size must match secure + */ + public static WebsocketPorts fromList(List secure, List insecure) { + boolean fallback = false; + if(secure.isEmpty() || insecure.isEmpty()) { + log.warn("One or more WebSocket ports is empty, falling back to defaults"); + fallback = true; + } + if(secure.size() != insecure.size()) { + log.warn("Secure ({}) and insecure ({}) WebSocket port counts mismatch, falling back to defaults", secure, insecure); + fallback = true; + } + if(fallback) { + secure = Arrays.asList(Constants.DEFAULT_WSS_PORTS); + insecure = Arrays.asList(Constants.DEFAULT_WS_PORTS); + log.warn("Falling back to default WebSocket ports: ({}), ({})", secure, insecure); + } + + return new WebsocketPorts(Collections.unmodifiableList(secure), Collections.unmodifiableList(insecure)); + } + + public String allPortsAsString() { + return StringUtils.join(securePorts, ",") + "," + StringUtils.join(insecurePorts, ","); + } +} diff --git a/old code/tray/src/qz/ws/substitutions/SubstitutionException.java b/old code/tray/src/qz/ws/substitutions/SubstitutionException.java new file mode 100755 index 0000000..e2fa3fe --- /dev/null +++ b/old code/tray/src/qz/ws/substitutions/SubstitutionException.java @@ -0,0 +1,9 @@ +package qz.ws.substitutions; + +import org.codehaus.jettison.json.JSONException; + +public class SubstitutionException extends JSONException { + public SubstitutionException(String message) { + super(message); + } +} diff --git a/old code/tray/src/qz/ws/substitutions/Substitutions.java b/old code/tray/src/qz/ws/substitutions/Substitutions.java new file mode 100755 index 0000000..e119c48 --- /dev/null +++ b/old code/tray/src/qz/ws/substitutions/Substitutions.java @@ -0,0 +1,351 @@ +package qz.ws.substitutions; + +import org.apache.commons.io.IOUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.codehaus.jettison.json.JSONArray; +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; +import qz.utils.ArgValue; +import qz.utils.ByteUtilities; +import qz.utils.FileUtilities; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; + +public class Substitutions { + protected static final Logger log = LogManager.getLogger(Substitutions.class); + + public static final String FILE_NAME = "substitutions.json"; + + + private static final Path DEFAULT_SUBSTITUTIONS_PATH = FileUtilities.SHARED_DIR.resolve(FILE_NAME); + + // Global toggle (should match ArgValue.SECURITY_SUBSTITUTIONS_ENABLE) + private static boolean enabled = true; + + // Subkeys that are restricted for writing because they can materially impact the content + private static boolean strict = true; + private static final HashMap parlous = new HashMap<>(); + static { + parlous.put(Type.OPTIONS, new String[] {"copies"}); + parlous.put(Type.DATA, new String[] {"data"}); + } + private final ArrayList rules; + private static Substitutions INSTANCE; + + public Substitutions(Path path) throws IOException, JSONException { + this(Files.newInputStream(path.toFile().toPath())); + } + + public Substitutions(InputStream in) throws IOException, JSONException { + this(IOUtils.toString(in, StandardCharsets.UTF_8)); + } + + public Substitutions(String serialized) throws JSONException { + rules = new ArrayList<>(); + + JSONArray instructions = new JSONArray(serialized); + for(int i = 0; i < instructions.length(); i++) { + JSONObject step = instructions.optJSONObject(i); + if(step != null) { + rules.add(new Rule(step)); + } + } + } + + public JSONObject replace(InputStream in) throws IOException, JSONException { + return replace(new JSONObject(IOUtils.toString(in, StandardCharsets.UTF_8))); + } + + public JSONObject replace(JSONObject base) throws JSONException { + for(Rule rule : rules) { + if (find(base, rule.match, rule.caseSensitive)) { + log.debug("Matched {}JSON substitution rule: {}", rule.caseSensitive ? "case-sensitive " : "", rule); + replace(base, rule.replace); + } else { + log.debug("Unable to match {}JSON substitution rule: {}", rule.caseSensitive ? "case-sensitive " : "", rule); + } + } + return base; + } + + public static boolean isPrimitive(Object o) { + if(o instanceof JSONObject || o instanceof JSONArray) { + return false; + } + return true; + } + + public static void replace(JSONObject base, JSONObject replace) throws JSONException { + JSONObject jsonBase = base.optJSONObject("params"); + JSONObject jsonReplace = replace.optJSONObject("params"); + if(jsonBase == null) { + // skip, invalid base format for replacement + return; + } + if(jsonReplace == null) { + throw new SubstitutionException("Replacement JSON is missing \"params\": and is malformed"); + } + + if (strict) { + // Second pass of sanitization before we replace + for(Iterator it = jsonReplace.keys(); it.hasNext(); ) { + Type type = Type.parse(it.next()); + if(type == null || type.isReadOnly()) continue; + // Good, let's make sure there are no exceptions + switch(type) { + case DATA: + // Special handling for arrays + JSONArray jsonArray = jsonReplace.optJSONArray(type.getKey()); + removeRestrictedSubkeys(jsonArray, type); + break; + default: + removeRestrictedSubkeys(jsonReplace, type); + } + } + } + find(base, replace, false, true); + } + + private static void removeRestrictedSubkeys(JSONObject jsonObject, Type type) { + if(jsonObject == null) { + return; + } + + String[] parlousFieldNames = parlous.get(type); + if(parlousFieldNames == null) return; + + for (String parlousFieldName : parlousFieldNames) { + JSONObject toCheck = jsonObject.optJSONObject(type.getKey()); + if (toCheck != null && toCheck.has(parlousFieldName)) { + log.warn("Use of { \"{}\": { \"{}\": ... } } is restricted, removing", type.getKey(), parlousFieldName); + jsonObject.remove(parlousFieldName); + } + } + } + + private static void removeRestrictedSubkeys(JSONArray jsonArray, Type type) { + if(jsonArray == null) { + return; + } + + ArrayList toRemove = new ArrayList<>(); + for(int i = 0; i < jsonArray.length(); i++) { + JSONObject jsonObject; + if ((jsonObject = jsonArray.optJSONObject(i)) != null) { + String[] parlousFieldNames = parlous.get(type); + for (String parlousFieldName : parlousFieldNames) { + if (jsonObject.has(parlousFieldName)) { + log.warn("Use of { \"{}\": { \"{}\": ... } } is restricted, removing", type.getKey(), parlousFieldName); + toRemove.add(jsonObject); + } + } + } + } + for(Object o : toRemove) { + jsonArray.remove(o); + } + } + + public static void sanitize(JSONObject match) throws JSONException { + // "options" ~= "config" + Object cache; + + JSONObject nested = new JSONObject(); + for(Type key : Type.values()) { + // If any alts/aliases key are used, switch them out for the standard key + for(String alt : key.getAlts()) { + if ((cache = match.optJSONObject(alt)) != null) { + match.put(key.getKey(), cache); + match.remove(alt); + break; + } + } + + // Special handling for nesting of "printer", "options", "data" within "params" + if((cache = match.opt(key.getKey())) != null) { + switch(key) { + case PRINTER: + JSONObject name = new JSONObject(); + name.put("name", cache); + nested.put(key.getKey(), name); + break; + default: + nested.put(key.getKey(), cache); + } + match.remove(key.getKey()); + } + } + if(nested.length() > 0) { + match.put("params", nested); + } + + // Special handling for "data" being provided as an object instead of an array + if((cache = match.opt("params")) != null) { + if (cache instanceof JSONObject) { + JSONObject params = (JSONObject)cache; + if((cache = params.opt("data")) != null) { + if (cache instanceof JSONArray) { + // If "data" is already an array, skip + } else { + JSONArray wrapped = new JSONArray(); + wrapped.put(cache); + params.put("data", wrapped); + } + } + } + } + } + + private static boolean find(Object base, Object match, boolean caseSensitive) throws JSONException { + return find(base, match, caseSensitive, false); + } + private static boolean find(Object base, Object match, boolean caseSensitive, boolean replace) throws JSONException { + if(base instanceof JSONObject) { + if(match instanceof JSONObject) { + JSONObject jsonMatch = (JSONObject)match; + JSONObject jsonBase = (JSONObject)base; + for(Iterator it = jsonMatch.keys(); it.hasNext(); ) { + String nextKey = it.next().toString(); + Object newMatch = jsonMatch.get(nextKey); + + // Check if the key exists, recurse if needed + if(jsonBase.has(nextKey)) { + Object newBase = jsonBase.get(nextKey); + + if(replace && isPrimitive(newMatch)) { + // Overwrite value, don't recurse + jsonBase.put(nextKey, newMatch); + continue; + } else if(find(newBase, newMatch, caseSensitive, replace)) { + continue; + } + } else if(replace) { + // Key doesn't exist, so we'll merge it in + jsonBase.put(nextKey, newMatch); + } + return false; // wasn't found + } + return true; // assume found + } else { + return false; // mismatched types + } + } else if (base instanceof JSONArray) { + boolean found = false; + if(match instanceof JSONArray) { + JSONArray matchArray = (JSONArray)match; + JSONArray baseArray = (JSONArray)base; + for(int i = 0; i < matchArray.length(); i++) { + Object newMatch = matchArray.get(i); + for(int j = 0; j < baseArray.length(); j++) { + Object newBase = baseArray.get(j); + if(find(newBase, newMatch, caseSensitive, replace)) { + found = true; + if(!replace) { + return true; + } + } + } + } + } + return found; + } else { + // Treat as primitives + if(match instanceof Number || base instanceof Number) { + return ByteUtilities.numberEquals(match, base); + } + // Fallback: Cast both to String + if(caseSensitive) { + return match.toString().equals(base.toString()); + } + return match.toString().equalsIgnoreCase(base.toString()); + } + } + + public static void setEnabled(boolean enabled) { + Substitutions.enabled = enabled; + } + + public static void setStrict(boolean strict) { + Substitutions.strict = strict; + } + + /** + * Returns a new instance of the Substitutions object from the default + * substitutions.json file at DEFAULT_SUBSTITUTIONS_PATH, + * or null if an error occurred. + */ + public static Substitutions newInstance() { + return newInstance(DEFAULT_SUBSTITUTIONS_PATH); + } + + /** + * Returns a new instance of the Substitutions object from the provided + * json substitutions file, or null if an error occurred. + */ + public static Substitutions newInstance(Path path) { + Substitutions substitutions = null; + try { + substitutions = new Substitutions(path); + log.info("Successfully parsed new substitutions file."); + } catch(JSONException e) { + log.warn("Unable to parse substitutions file, skipping", e); + } catch(IOException e) { + log.info("Substitutions file missing, skipping: {}", e.getMessage()); + } + return substitutions; + } + + public static Substitutions getInstance() { + return getInstance(false); + } + + public static Substitutions getInstance(boolean forceRefresh) { + if(INSTANCE == null || forceRefresh) { + INSTANCE = Substitutions.newInstance(); + if(!enabled) { + log.warn("Substitution file was found, but substitutions are currently disabled via \"{}=false\"", ArgValue.SECURITY_SUBSTITUTIONS_ENABLE.getMatch()); + } + } + return INSTANCE; + } + + public static boolean areActive() { + return Substitutions.getInstance() != null && enabled; + } + + private class Rule { + private boolean caseSensitive; + private JSONObject match, replace; + + Rule(JSONObject json) throws JSONException { + JSONObject replaceJSON = json.optJSONObject("use"); + if(replaceJSON != null) { + sanitize(replaceJSON); + replace = replaceJSON; + } + + JSONObject matchJSON = json.optJSONObject("for"); + if(matchJSON != null) { + caseSensitive = matchJSON.optBoolean("caseSensitive", false); + matchJSON.remove("caseSensitive"); + sanitize(matchJSON); + match = matchJSON; + } + + if(match == null || replace == null) { + throw new SubstitutionException("Mismatched instructions; Each \"use\" must have a matching \"for\"."); + } + } + + @Override + public String toString() { + return "for: " + match + ", use: " + replace; + } + } +} diff --git a/old code/tray/src/qz/ws/substitutions/Type.java b/old code/tray/src/qz/ws/substitutions/Type.java new file mode 100755 index 0000000..4965664 --- /dev/null +++ b/old code/tray/src/qz/ws/substitutions/Type.java @@ -0,0 +1,42 @@ +package qz.ws.substitutions; + +public enum Type { + OPTIONS("options", "config"), + PRINTER("printer"), + DATA("data"), + QUERY("query"); + + private String key; + private boolean readOnly; + private String[] alts; + + Type(String key, String ... alts) { + this(key, false, alts); + } + Type(String key, boolean readOnly, String... alts) { + this.key = key; + this.readOnly = readOnly; + this.alts = alts; + } + + public boolean isReadOnly() { + return readOnly; + } + + public static Type parse(Object o) { + for(Type root : values()) { + if (root.key.equals(o)) { + return root; + } + } + return null; + } + + public String getKey() { + return key; + } + + public String[] getAlts() { + return alts; + } +} \ No newline at end of file diff --git a/old code/tray/test/log4j2.xml b/old code/tray/test/log4j2.xml new file mode 100755 index 0000000..8b7553a --- /dev/null +++ b/old code/tray/test/log4j2.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/old code/tray/test/qz/installer/InstallerTests.java b/old code/tray/test/qz/installer/InstallerTests.java new file mode 100755 index 0000000..51e2566 --- /dev/null +++ b/old code/tray/test/qz/installer/InstallerTests.java @@ -0,0 +1,230 @@ +package qz.installer; + +import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.bouncycastle.openssl.PEMParser; +import qz.installer.certificate.CertificateChainBuilder; +import qz.installer.certificate.ExpiryTask; +import qz.installer.certificate.CertificateManager; + +import java.io.IOException; +import java.io.StringReader; +import java.security.Security; +import java.security.cert.X509Certificate; +import java.util.HashMap; + +public class InstallerTests { + + public static void main(String ... args) throws Exception { + // runInstallerTests(); + runExpiryTests(); + } + + public static void runInstallerTests() throws Exception { + CertificateChainBuilder.SSL_CERT_AGE = 1; + Installer installer = Installer.getInstance(); + // installer.install(); + CertificateManager certificateManager = installer.certGen(true); + new ExpiryTask(certificateManager).schedule(1000, 1000); + Thread.sleep(5000); + installer.removeCerts(); + } + public static void runExpiryTests() throws Exception { + Security.addProvider(new BouncyCastleProvider()); + String[] testCerts = { QZ_INDUSTRIES_CERT, CA_CERT_ORG_CERT, LETS_ENCRYPT_CERT }; + + HashMap certmap = new HashMap<>(); + certmap.put(ExpiryTask.CertProvider.INTERNAL, QZ_INDUSTRIES_CERT); + certmap.put(ExpiryTask.CertProvider.CA_CERT_ORG, CA_CERT_ORG_CERT); + certmap.put(ExpiryTask.CertProvider.LETS_ENCRYPT, LETS_ENCRYPT_CERT); + + + for(String testCert : testCerts) { + X509Certificate cert = loadCert(testCert); + ExpiryTask.findCertProvider(cert); + ExpiryTask.getExpiry(cert); + ExpiryTask.parseHostNames(cert); + } + } + + public static X509Certificate loadCert(String cert) throws IOException { + PEMParser reader = new PEMParser(new StringReader(cert)); + return (X509Certificate)reader.readObject(); + } + + private static String QZ_INDUSTRIES_CERT = "-----BEGIN CERTIFICATE-----\n" + + "MIIFDjCCA/agAwIBAgIGAW3W19xeMA0GCSqGSIb3DQEBCwUAMIGaMQswCQYDVQQG\n" + + "EwJVUzELMAkGA1UECAwCTlkxEjAQBgNVBAcMCUNhbmFzdG90YTEbMBkGA1UECgwS\n" + + "UVogSW5kdXN0cmllcywgTExDMRswGQYDVQQLDBJRWiBJbmR1c3RyaWVzLCBMTEMx\n" + + "HDAaBgkqhkiG9w0BCQEWDXN1cHBvcnRAcXouaW8xEjAQBgNVBAMMCWxvY2FsaG9z\n" + + "dDAeFw0xOTEwMTUyMzEyMTNaFw0yMjAxMTgwMDEyMTNaMIGaMQswCQYDVQQGEwJV\n" + + "UzELMAkGA1UECAwCTlkxEjAQBgNVBAcMCUNhbmFzdG90YTEbMBkGA1UECgwSUVog\n" + + "SW5kdXN0cmllcywgTExDMRswGQYDVQQLDBJRWiBJbmR1c3RyaWVzLCBMTEMxHDAa\n" + + "BgkqhkiG9w0BCQEWDXN1cHBvcnRAcXouaW8xEjAQBgNVBAMMCWxvY2FsaG9zdDCC\n" + + "ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK8Hfp8Hujhr6OCTJYLPnluv\n" + + "XgDi92eX8nkW+HkpWjgDwjv59VqIiycSGTxp5GCozvDF7zHbrSICVOlHa1iFXv3w\n" + + "8EpWTIKxfqiNDZohnq38R1lVGwfPC97pzaqu5CWvjTmUD5T/Cl5RnZEvnKoXvxAA\n" + + "9/Eikzz7TGr2BL56rJFmwYRosEd2tvyxV4o/m1t/PSU9cAi1GzWpuwRbmFl34cvV\n" + + "tMPeWUz315zy8Qw9cz4ktb1O/H+5BWXdpb9DRUS9QG6sS1Esi9jIZ7rPjm+Gqj3P\n" + + "mcsev9jVlex7C0eMG3QVLpOiurPxKYkGHH9F9W6PXvKEk/jWjFFxbpy380iqTb8C\n" + + "AwEAAaOCAVYwggFSMIHMBgNVHSMEgcQwgcGAFCNVfcjxztjhZUuVHS5vsRDzVvhb\n" + + "oYGgpIGdMIGaMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTlkxEjAQBgNVBAcMCUNh\n" + + "bmFzdG90YTEbMBkGA1UECgwSUVogSW5kdXN0cmllcywgTExDMRswGQYDVQQLDBJR\n" + + "WiBJbmR1c3RyaWVzLCBMTEMxHDAaBgkqhkiG9w0BCQEWDXN1cHBvcnRAcXouaW8x\n" + + "EjAQBgNVBAMMCWxvY2FsaG9zdIIGAW3W19ucMAwGA1UdEwEB/wQCMAAwDgYDVR0P\n" + + "AQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAlBgNVHREE\n" + + "HjAcgglsb2NhbGhvc3SCD2xvY2FsaG9zdC5xei5pbzAdBgNVHQ4EFgQUf2fwQ8IJ\n" + + "pdlT4+ghS0BP/V91ix0wDQYJKoZIhvcNAQELBQADggEBAHFiDZ7jItbHjpxxOHYF\n" + + "g6O61+7ETEPy0JGIPWxiysNCDfKyxuaVQ0UZ3/r6g5uQs3GjiQRIFxTmBk0hFTYB\n" + + "ONS2P0ugyED+C5wJADDcILa8SAF0EwrFX/6f3TnG+Qvn3jBRUCnjKTMfpnSlgMTk\n" + + "/wm1Jg10gUEXGHWGagw4YPVwMvBaWWYEFPC/emlONcAkZv4gfPZJ61bZgstqF+bZ\n" + + "WQM1GF1TOO8x/2KgguTknxc1EI4SmWN3Zl58BY8sf95yribLmKFW2VwbOHqfs0/d\n" + + "lFDMhix3cTURGvpyt+ZM4KXD9VkFpLIqRe1Qj02BPXS4GDNPQ+3xPbFOpvIKeYhf\n" + + "cGk=\n" + + "-----END CERTIFICATE-----"; + + private static String CA_CERT_ORG_CERT = "-----BEGIN CERTIFICATE-----\n" + + "MIIHnjCCBYagAwIBAgIDE4H4MA0GCSqGSIb3DQEBDQUAMHkxEDAOBgNVBAoTB1Jv\n" + + "b3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEiMCAGA1UEAxMZ\n" + + "Q0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJARYSc3VwcG9y\n" + + "dEBjYWNlcnQub3JnMB4XDTE4MDMxNzExMTMxNloXDTIwMDMxNjExMTMxNlowYTEL\n" + + "MAkGA1UEBhMCQVUxDDAKBgNVBAgTA05TVzEPMA0GA1UEBxMGU3lkbmV5MRQwEgYD\n" + + "VQQKEwtDQWNlcnQgSW5jLjEdMBsGA1UEAxMUY29tbXVuaXR5LmNhY2VydC5vcmcw\n" + + "ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKY4Bz8s5f0AK56dGIl8y1\n" + + "qnLyNhJr2pxJF9PInO33meBiCqpoTWpPHyIO51NGeySrlW35ZXUzp6tBMptXQict\n" + + "J7PkQcSf+lEn1AmRtWHIFNf/uM5IlgoomKktbAkkK+PLOtDBuZ40sKnRY1ooJ9ZK\n" + + "UnOrb5puz1D+JHp8JYxkPfknCNAZLeNPXqn9QqnpFKk8/c2CrVF8hShk/k5t2Dpr\n" + + "Q0Et9FkPOYBru9p5LQXQBA5QKPg1ESAVKYxRLbR4tJ02we6rOKWgLCnETlMmdjky\n" + + "NgaDG6dg79wNKu/uuYyQSXaAnJU67RGXNxIpudOlZ0c2+467mWDFaUHY4yzGTquq\n" + + "OGhMDXJu2fe7kDcBP8qH9YeIhN1WSLSnN4cbIP9UVxZXNfZ0WnA2Drj8iGlpL48v\n" + + "vBzuUD6EZ+WTeOkoapb0CRGAB+wdMQ6Tg+87tx8vUkhilk3NZ3kKRzOoDKiDisK9\n" + + "/WFh8aU7Eq62V15TmzOOkCHmXME1KH2CuzG4MQzalFz8ahRQQnezEMt91uHvCZya\n" + + "t5lcGr9W57FnYcxG6KqUO4iV6HWmJYXYhl5PfpEKzKktceH1PnuDptnE8mtdJW1T\n" + + "8p43ubgcAGxEvsq6nbeY76b1xlIkq1/NEL3BPDSoz+Tnz5MwLKjHQcqA7Av/KRH3\n" + + "VBnw4YI0VtGxZnz4wjyA8wIDAQABo4ICRTCCAkEwDAYDVR0TAQH/BAIwADAOBgNV\n" + + "HQ8BAf8EBAMCA6gwNAYDVR0lBC0wKwYIKwYBBQUHAwIGCCsGAQUFBwMBBglghkgB\n" + + "hvhCBAEGCisGAQQBgjcKAwMwMwYIKwYBBQUHAQEEJzAlMCMGCCsGAQUFBzABhhdo\n" + + "dHRwOi8vb2NzcC5jYWNlcnQub3JnLzAxBgNVHR8EKjAoMCagJKAihiBodHRwOi8v\n" + + "Y3JsLmNhY2VydC5vcmcvcmV2b2tlLmNybDCCAYEGA1UdEQSCAXgwggF0ghRjb21t\n" + + "dW5pdHkuY2FjZXJ0Lm9yZ6AiBggrBgEFBQcIBaAWDBRjb21tdW5pdHkuY2FjZXJ0\n" + + "Lm9yZ4Ibbm9jZXJ0LmNvbW11bml0eS5jYWNlcnQub3JnoCkGCCsGAQUFBwgFoB0M\n" + + "G25vY2VydC5jb21tdW5pdHkuY2FjZXJ0Lm9yZ4IZY2VydC5jb21tdW5pdHkuY2Fj\n" + + "ZXJ0Lm9yZ6AnBggrBgEFBQcIBaAbDBljZXJ0LmNvbW11bml0eS5jYWNlcnQub3Jn\n" + + "ghBlbWFpbC5jYWNlcnQub3JnoB4GCCsGAQUFBwgFoBIMEGVtYWlsLmNhY2VydC5v\n" + + "cmeCF25vY2VydC5lbWFpbC5jYWNlcnQub3JnoCUGCCsGAQUFBwgFoBkMF25vY2Vy\n" + + "dC5lbWFpbC5jYWNlcnQub3JnghVjZXJ0LmVtYWlsLmNhY2VydC5vcmegIwYIKwYB\n" + + "BQUHCAWgFwwVY2VydC5lbWFpbC5jYWNlcnQub3JnMA0GCSqGSIb3DQEBDQUAA4IC\n" + + "AQBWaOcDYaF25eP9eJTBUItFKkK3ppq7eN0qT9qyrWVxhRMWtAYcjW8hfSOx5xPS\n" + + "4bYL8RJz+1NNyzZqbyhvHt9JnCn1g2HllSD1HTHSMxZZrdjWq/9XxnmG55u2CUfo\n" + + "hN1M0qmUJvvWv0T4YWMwhv94tKrThDXnvqa4S+JfnTZQTLPAVq+iTKr+bsdB7pkI\n" + + "D59SJdE9tRsrb1wfbBbEpYw2LBZo7Jje4E9FmtnMraGxZtFsHhpZvYAnEt80eFts\n" + + "ccSOlhqowW9Hqx0pg55Sq9Wrj9T+AxTx/6sAJL4qxm7CRjeIAqW5fksvA4yXgYaq\n" + + "g6M2uIcRMEeafN8bHy1LOXkZDAcbusPfAGenMdE/p5B0K45Rlx3+dfNUjHyF4+ob\n" + + "FOVNxgPcfCZ2lJrgvJbw9tBGqC13yPUlkywQ+7QSJgTPbWrnXLIu7fz5SmCxk5KD\n" + + "zsq4F4YsaeBIYeHOsJLbqeqftm3eNBESphOvXlZKMGRMiThVWIaX5PIZB5OKgyE3\n" + + "C5CvKcv5qv1CeI7qFtLkq28QKCqJJIfTDvArEq/O5P2d+yQetYkWN5mzCJqT/kB+\n" + + "y74nu6kCBoZNWBZHDKeM6NkZD1/wI47S2A4cmE7SiGx3AcNRhmrXhvnSD7u7cGVD\n" + + "b5yw6z+JqFRMqMm0SuSx5X2oKNKfnqY77fIx6dtY8F5Scg==\n" + + "-----END CERTIFICATE-----\n" + + " 1 s:/O=Root CA/OU=http://www.cacert.org/CN=CA Cert Signing Authority/emailAddress=support@cacert.org\n" + + " i:/O=Root CA/OU=http://www.cacert.org/CN=CA Cert Signing Authority/emailAddress=support@cacert.org\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290\n" + + "IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB\n" + + "IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA\n" + + "Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO\n" + + "BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi\n" + + "MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ\n" + + "ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC\n" + + "CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ\n" + + "8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6\n" + + "zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y\n" + + "fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7\n" + + "w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc\n" + + "G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k\n" + + "epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q\n" + + "laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ\n" + + "QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU\n" + + "fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826\n" + + "YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w\n" + + "ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY\n" + + "gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe\n" + + "MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0\n" + + "IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy\n" + + "dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw\n" + + "czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0\n" + + "dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl\n" + + "aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC\n" + + "AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg\n" + + "b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB\n" + + "ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc\n" + + "nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg\n" + + "18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c\n" + + "gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl\n" + + "Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY\n" + + "sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T\n" + + "SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF\n" + + "CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum\n" + + "GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk\n" + + "zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW\n" + + "omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD\n" + + "-----END CERTIFICATE-----"; + + private static String LETS_ENCRYPT_CERT = "-----BEGIN CERTIFICATE-----\n" + + "MIIFTTCCBDWgAwIBAgISA/Qu8kKrD8kLzdY+/WPM8whbMA0GCSqGSIb3DQEBCwUA\n" + + "MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD\n" + + "ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTA4MjgxMzQ0MzdaFw0x\n" + + "OTExMjYxMzQ0MzdaMBYxFDASBgNVBAMTC2J1aWxkLnF6LmlvMIIBIjANBgkqhkiG\n" + + "9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9Q/StADlpSnsShayw4SV4dIbiOiiEYwqBlB7\n" + + "FYFF7LfZdREXlYBaTH46hUJI1ooUfsfnNTnYHac6tCEwr9wQnnobO7ACtuYENrVN\n" + + "HiuzYtMGN90mqf2+PXhHb+xGpBrD36fmq4Ix3aIc5o4lKxFY4IstfbTbYDanF1Q4\n" + + "qUIRUSdAJdgJqmJB2hwlFvjzeBGV4h6vgmiEsATawGoSDMLdWsFpiEnYLTfyvvhY\n" + + "5L4e2O9roBOEQ/YJbWVrewh6LYs6s6SbbNkKttQNSGUFVeW6u8q5+yHi2chSXlwW\n" + + "+o1SdjE6yw9laHp/nog5gyg95O2xm36YA3mRgfoAEfimwFwf2wIDAQABo4ICXzCC\n" + + "AlswDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcD\n" + + "AjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBTRuEPSdvHr2SkCIpArJG34rUOPLzAf\n" + + "BgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBvBggrBgEFBQcBAQRjMGEw\n" + + "LgYIKwYBBQUHMAGGImh0dHA6Ly9vY3NwLmludC14My5sZXRzZW5jcnlwdC5vcmcw\n" + + "LwYIKwYBBQUHMAKGI2h0dHA6Ly9jZXJ0LmludC14My5sZXRzZW5jcnlwdC5vcmcv\n" + + "MBYGA1UdEQQPMA2CC2J1aWxkLnF6LmlvMEwGA1UdIARFMEMwCAYGZ4EMAQIBMDcG\n" + + "CysGAQQBgt8TAQEBMCgwJgYIKwYBBQUHAgEWGmh0dHA6Ly9jcHMubGV0c2VuY3J5\n" + + "cHQub3JnMIIBAwYKKwYBBAHWeQIEAgSB9ASB8QDvAHYAb1N2rDHwMRnYmQCkURX/\n" + + "dxUcEdkCwQApBo2yCJo32RMAAAFs2K+GMAAABAMARzBFAiAI6WH6tspPGgp6W3KI\n" + + "n3Ihkb5OqS4KjGFbWNxsJq+/FgIhAJ0zLvFPdlivXpJd/Vn/+xKIBeAs9Ens2uxS\n" + + "A34B35oyAHUAY/Lbzeg7zCzPC3KEJ1drM6SNYXePvXWmOLHHaFRL2I0AAAFs2K+F\n" + + "FwAABAMARjBEAiAEYpsT6YoIByfh2SHOjuvICRUejlAHVS6bbPN+hvV+4gIgS6pt\n" + + "7MtF6GA83AF3lVZPCSnUKp3VvqcEjchf493wHAowDQYJKoZIhvcNAQELBQADggEB\n" + + "AH1Nr3BfiCG6iRUtGpaxoIv1J2XDmxAfz5kEtoErwo/oPTz2xY8UyYa1WFlCyJU1\n" + + "JWvGrbpT3MQXbdrLsSyT2HQRwEKzXr/u8rRSj18cqggwi8T/f9HgZXjf4ly19uYU\n" + + "5GqLBsPwO8BVzawr/bnI0viH1uVpcIQA/rW63LkOL8bMv16zW27mnoEAo8NG1YZU\n" + + "IEuCfMH/wFfkbmcw549l2PqIidVqSvWPltLlGdkNJYobFvyg5ThWXNb57cNIMb1k\n" + + "Egy5O7RqmVycOdt6//M5KrluWDUS/qi+7oAllGJ9AnFVDttmKuklrhGmwRv/ezN7\n" + + "gUtpN5eb5M1XxvExz3fXxfM=\n" + + "-----END CERTIFICATE-----\n" + + "-----BEGIN CERTIFICATE-----\n" + + "MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/\n" + + "MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\n" + + "DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow\n" + + "SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT\n" + + "GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC\n" + + "AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF\n" + + "q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8\n" + + "SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0\n" + + "Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA\n" + + "a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj\n" + + "/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T\n" + + "AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG\n" + + "CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv\n" + + "bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k\n" + + "c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw\n" + + "VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC\n" + + "ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz\n" + + "MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu\n" + + "Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF\n" + + "AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo\n" + + "uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/\n" + + "wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu\n" + + "X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG\n" + + "PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6\n" + + "KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==\n" + + "-----END CERTIFICATE-----"; +} diff --git a/old code/tray/test/qz/installer/browser/AppFinderTests.java b/old code/tray/test/qz/installer/browser/AppFinderTests.java new file mode 100755 index 0000000..aff808c --- /dev/null +++ b/old code/tray/test/qz/installer/browser/AppFinderTests.java @@ -0,0 +1,44 @@ +package qz.installer.browser; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.installer.certificate.firefox.FirefoxCertificateInstaller; +import qz.installer.certificate.firefox.locator.AppAlias; +import qz.installer.certificate.firefox.locator.AppInfo; +import qz.installer.certificate.firefox.locator.AppLocator; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Date; + +public class AppFinderTests { + private static final Logger log = LogManager.getLogger(AppFinderTests.class); + + public static void main(String ... args) throws Exception { + runTest(AppAlias.FIREFOX); + } + + private static void runTest(AppAlias app) { + Date begin = new Date(); + ArrayList appList = AppLocator.getInstance().locate(app); + ArrayList runningPaths = AppLocator.getRunningPaths(appList); + + StringBuilder output = new StringBuilder("Found apps:\n"); + for (AppInfo appInfo : appList) { + output.append(String.format(" name: '%s', path: '%s', exePath: '%s', version: '%s'\n", + appInfo.getAlias().getName(), + appInfo.getPath(), + appInfo.getExePath(), + appInfo.getVersion() + )); + + if(runningPaths.contains(appInfo.getExePath())) { + FirefoxCertificateInstaller.issueRestartWarning(runningPaths, appInfo); + } + } + + Date end = new Date(); + log.debug(output.toString()); + log.debug("Time to find and execute {}: {}s", app.name(), (end.getTime() - begin.getTime())/1000.0f); + } +} diff --git a/old code/tray/test/qz/installer/provision/ProvisionerInstallerTests.java b/old code/tray/test/qz/installer/provision/ProvisionerInstallerTests.java new file mode 100755 index 0000000..69b1531 --- /dev/null +++ b/old code/tray/test/qz/installer/provision/ProvisionerInstallerTests.java @@ -0,0 +1,20 @@ +package qz.installer.provision; + +import org.codehaus.jettison.json.JSONException; +import qz.common.Constants; + +import java.io.IOException; +import java.io.InputStream; + +public class ProvisionerInstallerTests { + + public static void main(String ... args) throws JSONException, IOException { + InputStream in = ProvisionerInstallerTests.class.getResourceAsStream("resources/" + Constants.PROVISION_FILE); + + // Parse the JSON + ProvisionInstaller provisionInstaller = new ProvisionInstaller(ProvisionerInstallerTests.class, in); + + // Invoke all parsed steps + provisionInstaller.invoke(); + } +} diff --git a/old code/tray/test/qz/installer/provision/resources/cert1.crt b/old code/tray/test/qz/installer/provision/resources/cert1.crt new file mode 100755 index 0000000..4ec319a --- /dev/null +++ b/old code/tray/test/qz/installer/provision/resources/cert1.crt @@ -0,0 +1,60 @@ +-----BEGIN CERTIFICATE----- +MIIFAzCCAuugAwIBAgICEAIwDQYJKoZIhvcNAQEFBQAwgZgxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJOWTEbMBkGA1UECgwSUVogSW5kdXN0cmllcywgTExDMRswGQYD +VQQLDBJRWiBJbmR1c3RyaWVzLCBMTEMxGTAXBgNVBAMMEHF6aW5kdXN0cmllcy5j +b20xJzAlBgkqhkiG9w0BCQEWGHN1cHBvcnRAcXppbmR1c3RyaWVzLmNvbTAeFw0x +NTAzMTkwMjM4NDVaFw0yNTAzMTkwMjM4NDVaMHMxCzAJBgNVBAYTAkFBMRMwEQYD +VQQIDApTb21lIFN0YXRlMQ0wCwYDVQQKDAREZW1vMQ0wCwYDVQQLDAREZW1vMRIw +EAYDVQQDDAlsb2NhbGhvc3QxHTAbBgkqhkiG9w0BCQEWDnJvb3RAbG9jYWxob3N0 +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtFzbBDRTDHHmlSVQLqjY +aoGax7ql3XgRGdhZlNEJPZDs5482ty34J4sI2ZK2yC8YkZ/x+WCSveUgDQIVJ8oK +D4jtAPxqHnfSr9RAbvB1GQoiYLxhfxEp/+zfB9dBKDTRZR2nJm/mMsavY2DnSzLp +t7PJOjt3BdtISRtGMRsWmRHRfy882msBxsYug22odnT1OdaJQ54bWJT5iJnceBV2 +1oOqWSg5hU1MupZRxxHbzI61EpTLlxXJQ7YNSwwiDzjaxGrufxc4eZnzGQ1A8h1u +jTaG84S1MWvG7BfcPLW+sya+PkrQWMOCIgXrQnAsUgqQrgxQ8Ocq3G4X9UvBy5VR +CwIDAQABo3sweTAJBgNVHRMEAjAAMCwGCWCGSAGG+EIBDQQfFh1PcGVuU1NMIEdl +bmVyYXRlZCBDZXJ0aWZpY2F0ZTAdBgNVHQ4EFgQUpG420UhvfwAFMr+8vf3pJunQ +gH4wHwYDVR0jBBgwFoAUkKZQt4TUuepf8gWEE3hF6Kl1VFwwDQYJKoZIhvcNAQEF +BQADggIBAFXr6G1g7yYVHg6uGfh1nK2jhpKBAOA+OtZQLNHYlBgoAuRRNWdE9/v4 +J/3Jeid2DAyihm2j92qsQJXkyxBgdTLG+ncILlRElXvG7IrOh3tq/TttdzLcMjaR +8w/AkVDLNL0z35shNXih2F9JlbNRGqbVhC7qZl+V1BITfx6mGc4ayke7C9Hm57X0 +ak/NerAC/QXNs/bF17b+zsUt2ja5NVS8dDSC4JAkM1dD64Y26leYbPybB+FgOxFu +wou9gFxzwbdGLCGboi0lNLjEysHJBi90KjPUETbzMmoilHNJXw7egIo8yS5eq8RH +i2lS0GsQjYFMvplNVMATDXUPm9MKpCbZ7IlJ5eekhWqvErddcHbzCuUBkDZ7wX/j +unk/3DyXdTsSGuZk3/fLEsc4/YTujpAjVXiA1LCooQJ7SmNOpUa66TPz9O7Ufkng ++CoTSACmnlHdP7U9WLr5TYnmL9eoHwtb0hwENe1oFC5zClJoSX/7DRexSJfB7YBf +vn6JA2xy4C6PqximyCPisErNp85GUcZfo33Np1aywFv9H+a83rSUcV6kpE/jAZio +5qLpgIOisArj1HTM6goDWzKhLiR/AeG3IJvgbpr9Gr7uZmfFyQzUjvkJ9cybZRd+ +G8azmpBBotmKsbtbAU/I/LVk8saeXznshOVVpDRYtVnjZeAneso7 +-----END CERTIFICATE----- +--START INTERMEDIATE CERT-- +-----BEGIN CERTIFICATE----- +MIIFEjCCA/qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwgawxCzAJBgNVBAYTAlVT +MQswCQYDVQQIDAJOWTESMBAGA1UEBwwJQ2FuYXN0b3RhMRswGQYDVQQKDBJRWiBJ +bmR1c3RyaWVzLCBMTEMxGzAZBgNVBAsMElFaIEluZHVzdHJpZXMsIExMQzEZMBcG +A1UEAwwQcXppbmR1c3RyaWVzLmNvbTEnMCUGCSqGSIb3DQEJARYYc3VwcG9ydEBx +emluZHVzdHJpZXMuY29tMB4XDTE1MDMwMjAwNTAxOFoXDTM1MDMwMjAwNTAxOFow +gZgxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOWTEbMBkGA1UECgwSUVogSW5kdXN0 +cmllcywgTExDMRswGQYDVQQLDBJRWiBJbmR1c3RyaWVzLCBMTEMxGTAXBgNVBAMM +EHF6aW5kdXN0cmllcy5jb20xJzAlBgkqhkiG9w0BCQEWGHN1cHBvcnRAcXppbmR1 +c3RyaWVzLmNvbTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANTDgNLU +iohl/rQoZ2bTMHVEk1mA020LYhgfWjO0+GsLlbg5SvWVFWkv4ZgffuVRXLHrwz1H +YpMyo+Zh8ksJF9ssJWCwQGO5ciM6dmoryyB0VZHGY1blewdMuxieXP7Kr6XD3GRM +GAhEwTxjUzI3ksuRunX4IcnRXKYkg5pjs4nLEhXtIZWDLiXPUsyUAEq1U1qdL1AH +EtdK/L3zLATnhPB6ZiM+HzNG4aAPynSA38fpeeZ4R0tINMpFThwNgGUsxYKsP9kh +0gxGl8YHL6ZzC7BC8FXIB/0Wteng0+XLAVto56Pyxt7BdxtNVuVNNXgkCi9tMqVX +xOk3oIvODDt0UoQUZ/umUuoMuOLekYUpZVk4utCqXXlB4mVfS5/zWB6nVxFX8Io1 +9FOiDLTwZVtBmzmeikzb6o1QLp9F2TAvlf8+DIGDOo0DpPQUtOUyLPCh5hBaDGFE +ZhE56qPCBiQIc4T2klWX/80C5NZnd/tJNxjyUyk7bjdDzhzT10CGRAsqxAnsjvMD +2KcMf3oXN4PNgyfpbfq2ipxJ1u777Gpbzyf0xoKwH9FYigmqfRH2N2pEdiYawKrX +6pyXzGM4cvQ5X1Yxf2x/+xdTLdVaLnZgwrdqwFYmDejGAldXlYDl3jbBHVM1v+uY +5ItGTjk+3vLrxmvGy5XFVG+8fF/xaVfo5TW5AgMBAAGjUDBOMB0GA1UdDgQWBBSQ +plC3hNS56l/yBYQTeEXoqXVUXDAfBgNVHSMEGDAWgBQDRcZNwPqOqQvagw9BpW0S +BkOpXjAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAJIO8SiNr9jpLQ +eUsFUmbueoxyI5L+P5eV92ceVOJ2tAlBA13vzF1NWlpSlrMmQcVUE/K4D01qtr0k +gDs6LUHvj2XXLpyEogitbBgipkQpwCTJVfC9bWYBwEotC7Y8mVjjEV7uXAT71GKT +x8XlB9maf+BTZGgyoulA5pTYJ++7s/xX9gzSWCa+eXGcjguBtYYXaAjjAqFGRAvu +pz1yrDWcA6H94HeErJKUXBakS0Jm/V33JDuVXY+aZ8EQi2kV82aZbNdXll/R6iGw +2ur4rDErnHsiphBgZB71C5FD4cdfSONTsYxmPmyUb5T+KLUouxZ9B0Wh28ucc1Lp +rbO7BnjW +-----END CERTIFICATE----- \ No newline at end of file diff --git a/old code/tray/test/qz/installer/provision/resources/provision.json b/old code/tray/test/qz/installer/provision/resources/provision.json new file mode 100755 index 0000000..69c1684 --- /dev/null +++ b/old code/tray/test/qz/installer/provision/resources/provision.json @@ -0,0 +1,158 @@ + +[ + { + "description": "[ERROR EXPECTED] missing 'type' and 'phase'", + "os": "*", + "data": "foo.bar=1" + }, + { + "description": "[ERROR EXPECTED] invalid 'remover' id", + "type": "remover", + "os": "*", + "phase": "install", + "data": "bbb" + }, + { + "description": "[ERROR EXPECTED] missing 'type'", + "os": "*", + "phase": "install", + "data": "this_file_does_not_exist" + }, + { + "description": "[ERROR EXPECTED] 'data' file missing", + "type": "script", + "os": "*", + "phase": "install", + "data": "this_file_does_not_exist" + }, + { + "description": "[ERROR EXPECTED] 'arch' is invalid", + "type": "property", + "os": "*", + "arch": "sparc", + "data": "bar.foo=2" + }, + { + "description": "[ERROR EXPECTED] 'os' is invalid", + "type": "property", + "os": "quake", + "arch": "*", + "data": "bar.foo=2" + }, + { + "description": "[WINDOWS SCRIPT] powershell at 'install'", + "type": "script", + "os": "windows", + "phase": "install", + "data": "script1.ps1" + }, + { + "description": "[MAC SCRIPT] powershell at 'install'", + "type": "script", + "os": "mac", + "phase": "install", + "data": "script1.ps1" + }, + { + "description": "[LINUX SCRIPT] python at 'startup'", + "type": "script", + "os": "linux", + "phase": "startup", + "data": "script4.py" + }, + { + "description": "[LINUX & MAC SCRIPT] bash without extension at 'install'", + "type": "script", + "os": "linux|mac", + "phase": "install", + "data": "script2" + }, + { + "description": "[ALL OS SCRIPT] with '.sh' extension at 'install'", + "type": "script", + "os": "*", + "phase": "install", + "data": "script3.sh" + }, + { + "description": "[AARCH64 ONLY SCRIPT] with '.sh' extension at 'install'", + "type": "script", + "os": "*", + "arch": "aarch64", + "phase": "install", + "data": "script2" + }, + { + "description": "[CERTIFICATE] at 'startup' (allowed.dat)", + "type": "cert", + "os": "*", + "data": "cert1.crt" + }, + { + "description": "[PROPERTY] at wrong phase (qz-tray.properties)", + "type": "property", + "phase": "startup", + "os": "*", + "data": "foo=bar" + }, + { + "description": "[PROPERTY] at 'install' (qz-tray.properties)", + "type": "property", + "phase": "install", + "os": "*", + "data": "websocket.secure.ports=9191,9292,9393,9494" + }, + { + "description": "[PROPERTY] at 'certgen' (qz-tray.properties)", + "type": "property", + "phase": "install", + "os": "*", + "data": "websocket.insecure.ports=9192,9293,9394,9495" + }, + { + "description": "[PROPERTY] at 'certgen' (qz-tray.properties)", + "type": "property", + "os": "*", + "data": "log.size=2097152" + }, + { + "description": "[PREFERENCE] at 'startup' (prefs.properties)", + "type": "preference", + "os": "*", + "data": "tray.notifications=true" + }, + { + "description": "[REMOVER] at 'install' ('QZ Tray' rebranded 'Cherry Connect')", + "type": "remover", + "os": "*", + "phase": "install", + "data": "Cherry Connect,cc-util,cc" + }, + { + "description": "[REMOVER] at 'install' QZ-branded version", + "type": "remover", + "os": "*", + "phase": "install", + "data": "qz" + }, + { + "description": "[CA] at 'install'", + "type": "ca", + "os": "*", + "data": "selfsigned1.crt" + }, + { + "description": "[CONF] at 'install'", + "type": "conf", + "os": "*", + "data": "java.net.useSystemProxies=true", + "path": "net.properties" + }, + { + "description": "[SOFTWARE] at 'install'", + "type": "software", + "os": "windows", + "data": "DCDSetup1.5.0.17.exe", + "args": "/S /v/qn" + } +] \ No newline at end of file diff --git a/old code/tray/test/qz/installer/provision/resources/script1.ps1 b/old code/tray/test/qz/installer/provision/resources/script1.ps1 new file mode 100755 index 0000000..3a7c68e --- /dev/null +++ b/old code/tray/test/qz/installer/provision/resources/script1.ps1 @@ -0,0 +1,5 @@ +$shell="PowerShell" +$date="$(Get-Date -format "yyyy-MM-dd HH:mm:ss")" +$script="$($myInvocation.MyCommand.Name)" +# FIXME: ~/Desktop may try to write to /root/Desktop on Linux +echo "$date Successful provisioning test from '$shell': $script" >> ~/Desktop/provision.log \ No newline at end of file diff --git a/old code/tray/test/qz/installer/provision/resources/script2 b/old code/tray/test/qz/installer/provision/resources/script2 new file mode 100755 index 0000000..81165cb --- /dev/null +++ b/old code/tray/test/qz/installer/provision/resources/script2 @@ -0,0 +1,9 @@ +#!/bin/bash + +shell=$(ps -p $$ -oargs=|awk '{print $1}') +date=$(date "+%F %T") +script=$(basename "$0") +user="$(eval echo ~$(logname))" + +echo "$date Successful provisioning test from '$shell': $script" >> "$user/Desktop/provision.log" +chmod 555 "$user/Desktop/provision.log" \ No newline at end of file diff --git a/old code/tray/test/qz/installer/provision/resources/script3.sh b/old code/tray/test/qz/installer/provision/resources/script3.sh new file mode 100755 index 0000000..f05f99a --- /dev/null +++ b/old code/tray/test/qz/installer/provision/resources/script3.sh @@ -0,0 +1,7 @@ +shell=$(ps -p $$ -oargs=|awk '{print $1}') +date=$(date "+%F %T") +script=$(basename "$0") +user="$(eval echo ~$(logname))" + +echo "$date Successful provisioning test from '$shell': $script" >> "$user/Desktop/provision.log" +chmod 555 "$user/Desktop/provision.log" \ No newline at end of file diff --git a/old code/tray/test/qz/installer/provision/resources/script4.py b/old code/tray/test/qz/installer/provision/resources/script4.py new file mode 100755 index 0000000..e34d47b --- /dev/null +++ b/old code/tray/test/qz/installer/provision/resources/script4.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python3 + +import os + +def notify(title, message): + os.system(f"notify-send '{title}' '{message}'") + +title=os.getenv('APP_TITLE') +version=os.getenv('APP_VERSION') +printer="\U0001F5A8" +tada="\U0001F389" + +notify("{} {}".format(printer, title), """{} This is a sample message from {} {}. + +This message indicates that provisioning startup tasks are working.""".format(tada, title, version)) \ No newline at end of file diff --git a/old code/tray/test/qz/installer/provision/resources/selfsigned1.crt b/old code/tray/test/qz/installer/provision/resources/selfsigned1.crt new file mode 100755 index 0000000..7a05ab1 --- /dev/null +++ b/old code/tray/test/qz/installer/provision/resources/selfsigned1.crt @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIID+TCCAuGgAwIBAgIUf1NN9L3Tw6UVqPiHcFsnS+sdm1kwDQYJKoZIhvcNAQEL +BQAwgaQxCzAJBgNVBAYTAlVTMREwDwYDVQQIDAhOZXcgWW9yazESMBAGA1UEBwwJ +Q2FuYXN0b3RhMRswGQYDVQQKDBJRWiBJbmR1c3RyaWVzLCBMTEMxGzAZBgNVBAsM +ElFaIEluZHVzdHJpZXMsIExMQzEZMBcGA1UEAwwQVHJlcyBGaW5vY2NoaWFybzEZ +MBcGCSqGSIb3DQEJARYKdHJlc0Bxei5pbzAeFw0yNTAyMTAxODQxNDlaFw0yNTAz +MTIxODQxNDlaMIGkMQswCQYDVQQGEwJVUzERMA8GA1UECAwITmV3IFlvcmsxEjAQ +BgNVBAcMCUNhbmFzdG90YTEbMBkGA1UECgwSUVogSW5kdXN0cmllcywgTExDMRsw +GQYDVQQLDBJRWiBJbmR1c3RyaWVzLCBMTEMxGTAXBgNVBAMMEFRyZXMgRmlub2Nj +aGlhcm8xGTAXBgkqhkiG9w0BCQEWCnRyZXNAcXouaW8wggEiMA0GCSqGSIb3DQEB +AQUAA4IBDwAwggEKAoIBAQCyLio0KuXpKr7rmFRP9UpbKvmMZlUytKeYKfiffJub +7V4kcD2dAWnl2DL6IK8MNVM6SuTOGUIl8XDV1MTxKhtZoIF2tFtG3UpX2heL76Ai +Q7A3cqXiNzZAIxxfppgIZZSZlEjr1r+OCsJEpTJJA6Dm0xOKBcC7c4C3fi7xPvEM +hu4EOmzO1EczFF3/mpgO+RG4MFNDPa8zz8SFZN6e9LRkPiPupiapvklgHikkRm3L +V7ktP/jmA0zp7JVKDvfNBVR8iZQ3Fbhtk06Iv0t/a0bov4NtyItD1O1oftLS7jEK +Erop9NMAofk/MAduevQTHkKLwwAGXXEtHnpaXx7FDpnLAgMBAAGjITAfMB0GA1Ud +DgQWBBTcQETEM4iqTfFf6Tq5GvzbhnA7bzANBgkqhkiG9w0BAQsFAAOCAQEASY6v +ep8asCIbdDjj1knYEMUMaHsvPbXQYY60Zt1HeQxACrUHCBZ375A+sl/INrH7gRJj +KygYKbI/LGM2lDErLTyOzWu1CBTIUUbaDwY5Fc+xl39N+EDctU82Gg8ddUoJDb6f +8w/gy2FtcCiRUKdIR5rYxIDl1izkBHPoV3jgUe5ydhlfHUjBZdIWWnZs2+fETG5B +62sG9MBwOlPJkE7Wdsf6Q7lQwnpUpnj08IY9+T7c3SA3pGKi2/e7di3G/47riMOd +vnldRfCl5erR0qR6J3Ksk6oZCYngLj3zR8R9qjUgEaiC76DGZd9s9wT867SMJJLs +iu9NclpILl4aZRJg0w== +-----END CERTIFICATE----- diff --git a/old code/tray/test/qz/printer/action/WebAppTest.java b/old code/tray/test/qz/printer/action/WebAppTest.java new file mode 100755 index 0000000..61e09b5 --- /dev/null +++ b/old code/tray/test/qz/printer/action/WebAppTest.java @@ -0,0 +1,266 @@ +package qz.printer.action; + +import javafx.print.*; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.common.Constants; +import qz.printer.action.html.WebApp; +import qz.printer.action.html.WebAppModel; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; + +public class WebAppTest { + + private static final Logger log = LogManager.getLogger(WebAppTest.class); + private static final int SPOOLER_WAIT = 2000; // millis + private static final Path RASTER_OUTPUT_DIR = Paths.get("./out"); // see ant ${out.dir} + private static final String RASTER_OUTPUT_FORMAT = "png"; + + public static void main(String[] args) { + try { + WebApp.initialize(); + cleanup(); + + // RASTER// + + int rasterKnownHeightTests = 1000; + if (args.length > 1) { rasterKnownHeightTests = Integer.parseInt(args[1]); } + int rasterFittedHeightTests = 1000; + if (args.length > 2) { rasterFittedHeightTests = Integer.parseInt(args[2]); } + + if (!testRasterKnownSize(rasterKnownHeightTests)) { + log.error("Testing well defined sizes failed"); + } else if (!testRasterFittedSize(rasterFittedHeightTests)) { + log.error("Testing fit to height sizing failed"); + } else { + log.info("All raster tests passed"); + } + + + // VECTOR // + + int vectorKnownHeightPrints = 100; + if (args.length > 3) { vectorKnownHeightPrints = Integer.parseInt(args[3]); } + int vectorFittedHeightPrints = 100; + if (args.length > 4) { vectorFittedHeightPrints = Integer.parseInt(args[4]); } + + if (!testVectorKnownPrints(vectorKnownHeightPrints)) { + log.error("Failed vector prints with defined heights"); + } else if (!testVectorFittedPrints(vectorFittedHeightPrints)) { + log.error("Failed vector prints with fit to height sizing"); + } else { + log.info("All vector prints completed"); + } + } + catch(Throwable t) { + log.error("Tests failed due to an exception", t); + } + + System.exit(0); //explicit exit since jfx is running in background + } + + + public static boolean testRasterKnownSize(int trials) throws Throwable { + for(int i = 0; i < trials; i++) { + //new size every run + double printW = Math.max(2, (int)(Math.random() * 110) / 10d) * 72d; + double printH = Math.max(3, (int)(Math.random() * 110) / 10d) * 72d; + double zoom = Math.max(0.5d, (int)(Math.random() * 30) / 10d); + + String id = "known-" + i; + WebAppModel model = buildModel(id, printW, printH, zoom, true, (int)(Math.random() * 360)); + BufferedImage sample = WebApp.raster(model); + + if (sample == null) { + log.error("Failed to create capture"); + return false; + } + + //TODO - check bottom right matches expected color + //check capture for dimensional accuracy within 1 pixel of expected (due to int rounding) + int expectedWidth = (int)Math.round(printW * (96d / 72d) * zoom); + int expectedHeight = (int)Math.round(printH * (96d / 72d) * zoom); + boolean passed = true; + + if (!Arrays.asList(expectedWidth, expectedWidth + 1, expectedWidth - 1).contains(sample.getWidth())) { + log.error("Expected width to be {} but got {}", expectedWidth, sample.getWidth()); + passed = false; + } + if (!Arrays.asList(expectedHeight, expectedHeight + 1, expectedHeight - 1).contains(sample.getHeight())) { + log.error("Expected height to be {} but got {}", expectedHeight, sample.getHeight()); + passed = false; + } + + saveAudit(passed? id:"invalid", sample); + + if (!passed) { + return false; + } + } + + return true; + } + + public static boolean testRasterFittedSize(int trials) throws Throwable { + for(int i = 0; i < trials; i++) { + //new size every run (height always starts at 0) + double printW = Math.max(2, (int)(Math.random() * 110) / 10d) * 72d; + double zoom = Math.max(0.5d, (int)(Math.random() * 30) / 10d); + + String id = "fitted-" + i; + WebAppModel model = buildModel(id, printW, 0, zoom, true, (int)(Math.random() * 360)); + BufferedImage sample = WebApp.raster(model); + + if (sample == null) { + log.error("Failed to create capture"); + return false; + } + + //TODO - check bottom right matches expected color + //check capture for dimensional accuracy within 1 pixel of expected (due to int rounding) + //expected height is not known for these tests + int expectedWidth = (int)Math.round(printW * (96d / 72d) * zoom); + boolean passed = true; + + if (!Arrays.asList(expectedWidth, expectedWidth + 1, expectedWidth - 1).contains(sample.getWidth())) { + log.error("Expected width to be {} but got {}", expectedWidth, sample.getWidth()); + passed = false; + } + + saveAudit(passed? id:"invalid", sample); + + if (!passed) { + return false; + } + } + + return true; + } + + public static boolean testVectorKnownPrints(int trials) throws Throwable { + PrinterJob job = buildVectorJob("vector-test-known"); + for(int i = 0; i < trials; i++) { + //new size every run + double printW = Math.max(2, (int)(Math.random() * 85) / 10d) * 72d; + double printH = Math.max(3, (int)(Math.random() * 110) / 10d) * 72d; + + String id = "known-" + i; + WebAppModel model = buildModel(id, printW, printH, 1, false, (int)(Math.random() * 360)); + + WebApp.print(job, model); + } + job.endJob(); + + try { + log.info("Waiting {} seconds for the spooler to catch up.", SPOOLER_WAIT / 1000); + Thread.sleep(SPOOLER_WAIT); + } + catch(InterruptedException ignore) {} + + return job.getJobStatus() != PrinterJob.JobStatus.ERROR; + } + + public static boolean testVectorFittedPrints(int trials) throws Throwable { + PrinterJob job = buildVectorJob("vector-test-fitted"); + for(int i = 0; i < trials; i++) { + //new size every run + double printW = Math.max(2, (int)(Math.random() * 85) / 10d) * 72d; + + String id = "fitted-" + i; + WebAppModel model = buildModel(id, printW, 0, 1, false, (int)(Math.random() * 360)); + + WebApp.print(job, model); + } + job.endJob(); + + try { + log.info("Waiting {} seconds for the spooler to catch up.", SPOOLER_WAIT / 1000); + Thread.sleep(SPOOLER_WAIT); + } + catch(InterruptedException ignore) {} + + return job.getJobStatus() != PrinterJob.JobStatus.ERROR; + } + + private static WebAppModel buildModel(String index, double width, double height, double zoom, boolean scale, int hue) { + int level = (int)(Math.random() * 50) + 25; + WebAppModel model = new WebAppModel("" + + "" + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + " " + + "
Generated content:" + index + "
Content size:" + width + "x" + height + "
Physical size:" + (width / 72d) + "x" + (height / 72d) + "
Zoomed tox " + zoom + "
" + + "" + + "", + true, width, height, scale, zoom); + + log.trace("Generating #{} = [({},{}), x{}]", index, model.getWidth(), model.getHeight(), model.getZoom()); + + return model; + } + + private static PrinterJob buildVectorJob(String name) throws Throwable { + Printer defaultPrinter = Printer.getDefaultPrinter(); + PrinterJob job = PrinterJob.createPrinterJob(defaultPrinter); + + // All this to remove margins + Constructor plCon = PageLayout.class.getDeclaredConstructor(Paper.class, PageOrientation.class, double.class, double.class, double.class, double.class); + plCon.setAccessible(true); + + Paper paper = defaultPrinter.getDefaultPageLayout().getPaper(); + PageLayout layout = plCon.newInstance(paper, PageOrientation.PORTRAIT, 0, 0, 0, 0); + + Field field = defaultPrinter.getClass().getDeclaredField("defPageLayout"); + field.setAccessible(true); + field.set(defaultPrinter, layout); + + JobSettings settings = job.getJobSettings(); + settings.setPageLayout(layout); + settings.setJobName(name); + + return job; + } + + private static void cleanup() { + File[] files; + if ((files = RASTER_OUTPUT_DIR.toFile().listFiles()).length > 0) { + for(File file : files) { + if (file.getName().endsWith("." + RASTER_OUTPUT_FORMAT) + && file.getName().startsWith(String.format("%s-", Constants.DATA_DIR))) { + if (!file.delete()) { + log.warn("Could not delete {}", file); + } + } + } + } + } + + private static void saveAudit(String id, BufferedImage capture) throws IOException { + Path image = RASTER_OUTPUT_DIR.resolve(String.format("%s-%s.%s", Constants.DATA_DIR, id, RASTER_OUTPUT_FORMAT)); + ImageIO.write(capture, RASTER_OUTPUT_FORMAT, image.toFile()); + log.info("Wrote {}: {}", id, image); + } + +} diff --git a/old code/tray/test/qz/printer/info/NativePrinterTests.java b/old code/tray/test/qz/printer/info/NativePrinterTests.java new file mode 100755 index 0000000..f77085e --- /dev/null +++ b/old code/tray/test/qz/printer/info/NativePrinterTests.java @@ -0,0 +1,39 @@ +package tests.qz.printer.info; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import qz.printer.PrintServiceMatcher; +import qz.printer.info.NativePrinter; +import qz.printer.info.NativePrinterMap; + +import java.util.Date; + +public class NativePrinterTests { + private static final Logger log = LogManager.getLogger(NativePrinterTests.class); + + public static void main(String ... args) { + for (int i = 0; i < 10; i++) { + runTest(); + } + } + + private static void runTest() { + Date begin = new Date(); + NativePrinterMap printers = PrintServiceMatcher.getNativePrinterList(); + StringBuilder output = new StringBuilder("Found printers:\n"); + for (NativePrinter printer : printers.values()) { + output.append(String.format(" printerId: '%s', description: '%s', driverFile: '%s', " + + "resolution: '%s', driver: '%s'\n", + printer.getPrinterId(), + printer.getDescription(), + printer.getDriverFile(), + printer.getResolution(), + printer.getDriver() + )); + } + Date end = new Date(); + log.debug(output.toString()); + log.debug("Time to find printers: " + (end.getTime() - begin.getTime())); + } + +} diff --git a/old code/tray/test/qz/utils/JsonWriterTests.java b/old code/tray/test/qz/utils/JsonWriterTests.java new file mode 100755 index 0000000..21b8c5a --- /dev/null +++ b/old code/tray/test/qz/utils/JsonWriterTests.java @@ -0,0 +1,46 @@ +package qz.utils; + +import org.codehaus.jettison.json.JSONException; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.io.IOException; + +public class JsonWriterTests { + + private static final Logger log = LogManager.getLogger(JsonWriterTests.class); + + private static String DEFAULT_PATH = "/Applications/Firefox.app/Contents/Resources/distribution/policies.json"; + private static String DEFAULT_DATA = "{ \"policies\": { \"Certificates\": { \"ImportEnterpriseRoots\": true } } }"; + private static boolean DEFAULT_OVERWRITE = false; + private static boolean DEFAULT_DELETE = false; + + public static void main(String... args) { + String usingPath = DEFAULT_PATH; + if (args.length > 0) { + usingPath = args[0]; + } + String usingData = DEFAULT_DATA; + if (args.length > 1) { + usingData = args[1]; + } + boolean usingOverwrite = DEFAULT_OVERWRITE; + if (args.length > 2) { + usingOverwrite = Boolean.parseBoolean(args[2]); + } + boolean usingDeletion = DEFAULT_DELETE; + if (args.length > 3) { + usingDeletion = Boolean.parseBoolean(args[3]); + } + + try { + JsonWriter.write(usingPath, usingData, usingOverwrite, usingDeletion); + } + catch(JSONException jsone) { + log.error("Failed to read JSON", jsone); + } + catch(IOException ioe) { + log.error("Failed to access file", ioe); + } + } +} diff --git a/old code/tray/test/qz/ws/substitutions/SubstitutionsTests.java b/old code/tray/test/qz/ws/substitutions/SubstitutionsTests.java new file mode 100755 index 0000000..fca998d --- /dev/null +++ b/old code/tray/test/qz/ws/substitutions/SubstitutionsTests.java @@ -0,0 +1,19 @@ +package qz.ws.substitutions; + +import org.codehaus.jettison.json.JSONException; +import org.codehaus.jettison.json.JSONObject; + +import java.io.IOException; + +public class SubstitutionsTests { + public static void main(String ... args) throws JSONException, IOException { + Substitutions substitutions = new Substitutions( + SubstitutionsTests.class.getResourceAsStream("resources/substitutions.json") + ); + JSONObject base = substitutions.replace( + SubstitutionsTests.class.getResourceAsStream("resources/printRequest.json") + ); + + System.out.println(base); + } +} diff --git a/old code/tray/test/qz/ws/substitutions/resources/printRequest.json b/old code/tray/test/qz/ws/substitutions/resources/printRequest.json new file mode 100755 index 0000000..98612c2 --- /dev/null +++ b/old code/tray/test/qz/ws/substitutions/resources/printRequest.json @@ -0,0 +1,67 @@ +{ + "call": "print", + "params": { + "printer": { + "name": "PDFwriter" + }, + "options": { + "bounds": null, + "colorType": "color", + "copies": 1, + "density": 0, + "duplex": false, + "fallbackDensity": null, + "interpolation": "bicubic", + "jobName": null, + "legacy": false, + "margins": 0, + "orientation": null, + "paperThickness": null, + "printerTray": null, + "rasterize": false, + "rotation": 0, + "scaleContent": true, + "size": { + "width": "4", + "height": "6" + }, + "units": "in", + "forceRaw": false, + "encoding": null, + "spool": {} + }, + "data": [ + { + "type": "pixel", + "format": "pdf", + "flavor": "file", + "data": "https://demo.qz.io/assets/pdf_sample.pdf", + "options": { + "pageWidth": "8.5", + "pageHeight": "11", + "pageRanges": "", + "ignoreTransparency": false, + "altFontRendering": false + } + }, + { + "type": "pixel", + "format": "image", + "flavor": "file", + "data": "https://demo.qz.io/assets/img/image_sample.png", + }, + "^XA\n", + "^FO50,50^ADN,36,20^FDPRINTED WITH QZ 2.2.4-SNAPSHOT\n", + "^FS\n", + "^XZ\n" + ] + }, + "signature": "", + "timestamp": 1713895560783, + "uid": "64t63d", + "position": { + "x": 720, + "y": 462.5 + }, + "signAlgorithm": "SHA512" +} \ No newline at end of file diff --git a/old code/tray/test/qz/ws/substitutions/resources/substitutions.json b/old code/tray/test/qz/ws/substitutions/resources/substitutions.json new file mode 100755 index 0000000..274cfe5 --- /dev/null +++ b/old code/tray/test/qz/ws/substitutions/resources/substitutions.json @@ -0,0 +1,109 @@ +[ + { + "use":{ + "config": { + "size": { + "width": 100, + "height": 150 + }, + "units": "mm" + } + }, + "for": { + "config": { + "size": { + "width": 4, + "height": 6 + }, + "units": "in" + } + } + }, + { + "use": { + "printer": "PDF" + }, + "for": { + "printer": "PDFwriter" + } + }, + { + "use": { + "data": { + "options": { + "pageWidth": 8.5, + "pageHeight": 14 + } + } + }, + "for": { + "data": { + "options": { + "pageWidth": "8.5", + "pageHeight": "11" + } + } + } + }, + { + "use": { + "query": "pdf" + }, + "for": { + "query": "zzz" + } + }, + { + "use": { + "config": { + "copies": 3 + } + }, + "for": { + "config": { + "copies": 1 + } + } + }, + { + "use": { + "printer": "PDFwriter" + }, + "for": { + "caseSensitive": true, + "printer": "xps document writer" + } + }, + { + "use": { + "data": { + "data": "https://yahoo.com" + } + }, + "for": { + "data": { + "data": "https://demo.qz.io/assets/pdf_sample.pdf" + } + } + }, + { + "use": { + "printer": "ZDesigner" + }, + "for": { + "data": [ "^XA\n" ] + } + }, + { + "use": { + "data": { + "type": "PIXEL" + } + }, + "for": { + "data": { + "type": "pixel" + } + } + } +] \ No newline at end of file diff --git a/py_app/app/access_control.py b/py_app/app/access_control.py new file mode 100644 index 0000000..806a70c --- /dev/null +++ b/py_app/app/access_control.py @@ -0,0 +1,90 @@ +""" +Simple access control decorators for the 4-tier system +""" +from functools import wraps +from flask import session, redirect, url_for, flash, request +from .permissions_simple import check_access, ROLES + +def requires_role(min_role_level=None, required_modules=None, page=None): + """ + Simple role-based access decorator + + Args: + min_role_level (int): Minimum role level required (50, 70, 90, 100) + required_modules (list): Required modules for access + page (str): Page name for automatic access checking + """ + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + # Check if user is logged in + if 'user' not in session: + flash('Please log in to access this page.') + return redirect(url_for('main.login')) + + user_role = session.get('role') + user_modules = session.get('modules', []) + + # If page is specified, use automatic access checking + if page: + if not check_access(user_role, user_modules, page): + flash('Access denied: You do not have permission to access this page.') + return redirect(url_for('main.dashboard')) + return f(*args, **kwargs) + + # Manual role level checking + if min_role_level: + user_level = ROLES.get(user_role, {}).get('level', 0) + if user_level < min_role_level: + flash('Access denied: Insufficient privileges.') + return redirect(url_for('main.dashboard')) + + # Module requirement checking + if required_modules: + if user_role in ['superadmin', 'admin']: + # Superadmin and admin have access to all modules + pass + else: + if not any(module in user_modules for module in required_modules): + flash('Access denied: You do not have access to this module.') + return redirect(url_for('main.dashboard')) + + return f(*args, **kwargs) + return decorated_function + return decorator + +def superadmin_only(f): + """Decorator for superadmin-only pages""" + return requires_role(min_role_level=100)(f) + +def admin_plus(f): + """Decorator for admin and superadmin access""" + return requires_role(min_role_level=90)(f) + +def manager_plus(f): + """Decorator for manager, admin, and superadmin access""" + return requires_role(min_role_level=70)(f) + +def requires_quality_module(f): + """Decorator for quality module access""" + return requires_role(required_modules=['quality'])(f) + +def requires_warehouse_module(f): + """Decorator for warehouse module access""" + return requires_role(required_modules=['warehouse'])(f) + +def requires_labels_module(f): + """Decorator for labels module access""" + return requires_role(required_modules=['labels'])(f) + +def quality_manager_plus(f): + """Decorator for quality module manager+ access""" + return requires_role(min_role_level=70, required_modules=['quality'])(f) + +def warehouse_manager_plus(f): + """Decorator for warehouse module manager+ access""" + return requires_role(min_role_level=70, required_modules=['warehouse'])(f) + +def labels_manager_plus(f): + """Decorator for labels module manager+ access""" + return requires_role(min_role_level=70, required_modules=['labels'])(f) \ No newline at end of file diff --git a/py_app/app/permissions_simple.py b/py_app/app/permissions_simple.py new file mode 100644 index 0000000..9664b2a --- /dev/null +++ b/py_app/app/permissions_simple.py @@ -0,0 +1,226 @@ +""" +Simplified 4-Tier Role-Based Access Control System +Clear hierarchy: Superadmin → Admin → Manager → Worker +Module-based permissions: Quality, Labels, Warehouse +""" + +# APPLICATION MODULES +MODULES = { + 'quality': { + 'name': 'Quality Control', + 'scan_pages': ['quality', 'fg_quality'], + 'management_pages': ['quality_reports', 'quality_settings'], + 'worker_access': ['scan_only'] # Workers can only scan, no reports + }, + 'labels': { + 'name': 'Label Management', + 'scan_pages': ['label_scan'], + 'management_pages': ['label_creation', 'label_reports'], + 'worker_access': ['scan_only'] + }, + 'warehouse': { + 'name': 'Warehouse Management', + 'scan_pages': ['move_orders'], + 'management_pages': ['create_locations', 'warehouse_reports', 'inventory_management'], + 'worker_access': ['move_orders_only'] # Workers can move orders but not create locations + } +} + +# 4-TIER ROLE STRUCTURE +ROLES = { + 'superadmin': { + 'name': 'Super Administrator', + 'level': 100, + 'description': 'Full system access - complete control over all modules and system settings', + 'access': { + 'all_modules': True, + 'all_pages': True, + 'restricted_pages': [] # No restrictions + } + }, + 'admin': { + 'name': 'Administrator', + 'level': 90, + 'description': 'Full app access except role permissions and extension download', + 'access': { + 'all_modules': True, + 'all_pages': True, + 'restricted_pages': ['role_permissions', 'download_extension'] + } + }, + 'manager': { + 'name': 'Manager', + 'level': 70, + 'description': 'Complete module access - can manage one or more modules (quality/labels/warehouse)', + 'access': { + 'all_modules': False, # Only assigned modules + 'module_access': 'full', # Full access to assigned modules + 'can_cumulate': True, # Can have multiple modules + 'restricted_pages': ['role_permissions', 'download_extension', 'system_settings'] + } + }, + 'worker': { + 'name': 'Worker', + 'level': 50, + 'description': 'Limited module access - can perform basic operations in assigned modules', + 'access': { + 'all_modules': False, # Only assigned modules + 'module_access': 'limited', # Limited access (scan pages only) + 'can_cumulate': True, # Can have multiple modules + 'restricted_pages': ['role_permissions', 'download_extension', 'system_settings', 'reports'] + } + } +} + +# PAGE ACCESS RULES +PAGE_ACCESS = { + # System pages accessible by role level + 'dashboard': {'min_level': 50, 'modules': []}, + 'settings': {'min_level': 90, 'modules': []}, + 'role_permissions': {'min_level': 100, 'modules': []}, # Superadmin only + 'download_extension': {'min_level': 100, 'modules': []}, # Superadmin only + + # Quality module pages + 'quality': {'min_level': 50, 'modules': ['quality']}, + 'fg_quality': {'min_level': 50, 'modules': ['quality']}, + 'quality_reports': {'min_level': 70, 'modules': ['quality']}, # Manager+ only + 'reports': {'min_level': 70, 'modules': ['quality']}, # Manager+ only for quality reports + + # Warehouse module pages + 'warehouse': {'min_level': 50, 'modules': ['warehouse']}, + 'move_orders': {'min_level': 50, 'modules': ['warehouse']}, + 'create_locations': {'min_level': 70, 'modules': ['warehouse']}, # Manager+ only + 'warehouse_reports': {'min_level': 70, 'modules': ['warehouse']}, # Manager+ only + + # Labels module pages + 'labels': {'min_level': 50, 'modules': ['labels']}, + 'label_scan': {'min_level': 50, 'modules': ['labels']}, + 'label_creation': {'min_level': 70, 'modules': ['labels']}, # Manager+ only + 'label_reports': {'min_level': 70, 'modules': ['labels']} # Manager+ only +} + +def check_access(user_role, user_modules, page): + """ + Simple access check for the 4-tier system + + Args: + user_role (str): User's role (superadmin, admin, manager, worker) + user_modules (list): User's assigned modules ['quality', 'warehouse'] + page (str): Page being accessed + + Returns: + bool: True if access granted, False otherwise + """ + if user_role not in ROLES: + return False + + user_level = ROLES[user_role]['level'] + + # Check if page exists in our access rules + if page not in PAGE_ACCESS: + return False + + page_config = PAGE_ACCESS[page] + + # Check minimum level requirement + if user_level < page_config['min_level']: + return False + + # Check restricted pages for this role + if page in ROLES[user_role]['access']['restricted_pages']: + return False + + # Check module requirements + required_modules = page_config['modules'] + if required_modules: + # Page requires specific modules + # Superadmin and admin have access to all modules by default + if ROLES[user_role]['access']['all_modules']: + return True + # Other roles need to have the required module assigned + if not any(module in user_modules for module in required_modules): + return False + + return True + +def get_user_accessible_pages(user_role, user_modules): + """ + Get list of pages accessible to a user + + Args: + user_role (str): User's role + user_modules (list): User's assigned modules + + Returns: + list: List of accessible page names + """ + accessible_pages = [] + + for page in PAGE_ACCESS.keys(): + if check_access(user_role, user_modules, page): + accessible_pages.append(page) + + return accessible_pages + +def validate_user_modules(user_role, user_modules): + """ + Validate that user's module assignment is valid for their role + + Args: + user_role (str): User's role + user_modules (list): User's assigned modules + + Returns: + tuple: (is_valid, error_message) + """ + if user_role not in ROLES: + return False, "Invalid role" + + role_config = ROLES[user_role] + + # Superadmin and admin have access to all modules by default + if role_config['access']['all_modules']: + return True, "" + + # Manager can have multiple modules + if user_role == 'manager': + if not user_modules: + return False, "Managers must have at least one module assigned" + valid_modules = list(MODULES.keys()) + for module in user_modules: + if module not in valid_modules: + return False, f"Invalid module: {module}" + return True, "" + + # Worker can have multiple modules now + if user_role == 'worker': + if not user_modules: + return False, "Workers must have at least one module assigned" + valid_modules = list(MODULES.keys()) + for module in user_modules: + if module not in valid_modules: + return False, f"Invalid module: {module}" + return True, "" + + return True, "" + +def get_role_description(role): + """Get human-readable role description""" + return ROLES.get(role, {}).get('description', 'Unknown role') + +def get_available_modules(): + """Get list of available modules""" + return list(MODULES.keys()) + +def can_access_reports(user_role, user_modules, module): + """ + Check if user can access reports for a specific module + Worker level users cannot access reports + """ + if user_role == 'worker': + return False + + if module in user_modules or ROLES[user_role]['access']['all_modules']: + return True + + return False \ No newline at end of file diff --git a/py_app/app/routes.py b/py_app/app/routes.py index f48e860..f048527 100755 --- a/py_app/app/routes.py +++ b/py_app/app/routes.py @@ -32,10 +32,8 @@ bp = Blueprint('main', __name__) warehouse_bp = Blueprint('warehouse', __name__) @bp.route('/main_scan') +@requires_quality_module def main_scan(): - if 'role' not in session or session['role'] not in ['superadmin', 'admin', 'administrator', 'scan']: - flash('Access denied: Scan users only.') - return redirect(url_for('main.dashboard')) return render_template('main_page_scan.html') @bp.route('/', methods=['GET', 'POST']) @@ -386,6 +384,58 @@ def delete_user_simple(): flash('Error deleting user.') return redirect(url_for('main.user_management_simple')) +@bp.route('/quick_update_modules', methods=['POST']) +@admin_plus +def quick_update_modules(): + """Quick update of user modules without changing other details""" + try: + user_id = request.form.get('user_id') + modules = request.form.getlist('modules') + + if not user_id: + flash('User ID is required.') + return redirect(url_for('main.user_management_simple')) + + # Get current user to validate role + conn = get_db_connection() + cursor = conn.cursor() + cursor.execute("SELECT username, role FROM users WHERE id=%s", (user_id,)) + user_row = cursor.fetchone() + + if not user_row: + flash('User not found.') + conn.close() + return redirect(url_for('main.user_management_simple')) + + username, role = user_row + + # Validate modules for the role + from app.permissions_simple import validate_user_modules + is_valid, error_msg = validate_user_modules(role, modules) + if not is_valid: + flash(f'Invalid module assignment: {error_msg}') + conn.close() + return redirect(url_for('main.user_management_simple')) + + # Prepare modules JSON + modules_json = None + if modules and role in ['manager', 'worker']: + import json + modules_json = json.dumps(modules) + + # Update modules only + cursor.execute("UPDATE users SET modules=%s WHERE id=%s", (modules_json, user_id)) + conn.commit() + conn.close() + + flash(f'Modules updated successfully for user "{username}".') + return redirect(url_for('main.user_management_simple')) + + except Exception as e: + print(f"Error updating modules: {e}") + flash('Error updating modules.') + return redirect(url_for('main.user_management_simple')) + @bp.route('/reports') @requires_quality_module def reports(): @@ -499,10 +549,8 @@ def logout(): # Finish Goods Scan Route @bp.route('/fg_scan', methods=['GET', 'POST']) +@requires_quality_module def fg_scan(): - if 'role' not in session or session['role'] not in ['superadmin', 'administrator', 'admin', 'scan']: - flash('Access denied: Scan users only.') - return redirect(url_for('main.dashboard')) if request.method == 'POST': # Handle form submission diff --git a/py_app/app/static/fg_quality.js b/py_app/app/static/fg_quality.js new file mode 100644 index 0000000..7f3c88f --- /dev/null +++ b/py_app/app/static/fg_quality.js @@ -0,0 +1,539 @@ +// FG Quality specific JavaScript - Standalone version +document.addEventListener('DOMContentLoaded', function() { + // Prevent conflicts with main script.js by removing existing listeners + console.log('FG Quality JavaScript loaded'); + + const reportButtons = document.querySelectorAll('.report-btn'); + const reportTable = document.getElementById('report-table'); + const reportTitle = document.getElementById('report-title'); + const exportCsvButton = document.getElementById('export-csv'); + + // Calendar elements + const calendarModal = document.getElementById('calendar-modal'); + const dateRangeModal = document.getElementById('date-range-modal'); + const selectDayReport = document.getElementById('select-day-report'); + const selectDayDefectsReport = document.getElementById('select-day-defects-report'); + const dateRangeReport = document.getElementById('date-range-report'); + const dateRangeDefectsReport = document.getElementById('date-range-defects-report'); + + let currentReportType = null; + let currentDate = new Date(); + let selectedDate = null; + + // Clear any existing event listeners by cloning elements + function clearExistingListeners() { + if (selectDayReport) { + const newSelectDayReport = selectDayReport.cloneNode(true); + selectDayReport.parentNode.replaceChild(newSelectDayReport, selectDayReport); + } + if (selectDayDefectsReport) { + const newSelectDayDefectsReport = selectDayDefectsReport.cloneNode(true); + selectDayDefectsReport.parentNode.replaceChild(newSelectDayDefectsReport, selectDayDefectsReport); + } + if (dateRangeReport) { + const newDateRangeReport = dateRangeReport.cloneNode(true); + dateRangeReport.parentNode.replaceChild(newDateRangeReport, dateRangeReport); + } + if (dateRangeDefectsReport) { + const newDateRangeDefectsReport = dateRangeDefectsReport.cloneNode(true); + dateRangeDefectsReport.parentNode.replaceChild(newDateRangeDefectsReport, dateRangeDefectsReport); + } + } + + // Clear existing listeners first + clearExistingListeners(); + + // Re-get elements after cloning + const newSelectDayReport = document.getElementById('select-day-report'); + const newSelectDayDefectsReport = document.getElementById('select-day-defects-report'); + const newDateRangeReport = document.getElementById('date-range-report'); + const newDateRangeDefectsReport = document.getElementById('date-range-defects-report'); + + // Add event listeners to report buttons + reportButtons.forEach(button => { + const reportType = button.getAttribute('data-report'); + if (reportType) { + // Clone to remove existing listeners + const newButton = button.cloneNode(true); + button.parentNode.replaceChild(newButton, button); + + newButton.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + console.log('FG Report button clicked:', reportType); + fetchFGReportData(reportType); + }); + } + }); + + // Calendar-based report buttons with FG-specific handlers + if (newSelectDayReport) { + newSelectDayReport.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log('FG Select Day Report clicked'); + currentReportType = '6'; + showCalendarModal(); + }); + } + + if (newSelectDayDefectsReport) { + newSelectDayDefectsReport.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log('FG Select Day Defects Report clicked'); + currentReportType = '8'; + showCalendarModal(); + }); + } + + if (newDateRangeReport) { + newDateRangeReport.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log('FG Date Range Report clicked'); + currentReportType = '7'; + showDateRangeModal(); + }); + } + + if (newDateRangeDefectsReport) { + newDateRangeDefectsReport.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log('FG Date Range Defects Report clicked'); + currentReportType = '9'; + showDateRangeModal(); + }); + } + + // Function to fetch FG report data + function fetchFGReportData(reportType) { + const url = `/get_fg_report_data?report=${reportType}`; + console.log('Fetching FG data from:', url); + reportTitle.textContent = 'Loading FG data...'; + + fetch(url) + .then(response => response.json()) + .then(data => { + console.log('FG Report data received:', data); + if (data.error) { + reportTitle.textContent = data.error; + return; + } + + populateFGTable(data); + updateReportTitle(reportType); + }) + .catch(error => { + console.error('Error fetching FG report data:', error); + reportTitle.textContent = 'Error loading FG data.'; + }); + } + + // Function to fetch FG report data for specific dates + function fetchFGDateReportData(reportType, date, startDate = null, endDate = null) { + let url = `/generate_fg_report?report=${reportType}`; + if (date) { + url += `&date=${date}`; + } + if (startDate && endDate) { + url += `&start_date=${startDate}&end_date=${endDate}`; + } + + console.log('Fetching FG date report from:', url); + reportTitle.textContent = 'Loading FG data...'; + + fetch(url) + .then(response => response.json()) + .then(data => { + console.log('FG Date report data received:', data); + if (data.error) { + reportTitle.textContent = data.error; + return; + } + + populateFGTable(data); + updateDateReportTitle(reportType, date, startDate, endDate); + }) + .catch(error => { + console.error('Error fetching FG date report data:', error); + reportTitle.textContent = 'Error loading FG data.'; + }); + } + + // Function to populate the table with FG data + function populateFGTable(data) { + const thead = reportTable.querySelector('thead tr'); + const tbody = reportTable.querySelector('tbody'); + + // Clear existing content + thead.innerHTML = ''; + tbody.innerHTML = ''; + + // Add headers + if (data.headers && data.headers.length > 0) { + data.headers.forEach(header => { + const th = document.createElement('th'); + th.textContent = header; + thead.appendChild(th); + }); + } + + // Add rows + if (data.rows && data.rows.length > 0) { + data.rows.forEach(row => { + const tr = document.createElement('tr'); + row.forEach(cell => { + const td = document.createElement('td'); + td.textContent = cell || ''; + tr.appendChild(td); + }); + tbody.appendChild(tr); + }); + } else { + // Show no data message + const tr = document.createElement('tr'); + const td = document.createElement('td'); + td.colSpan = data.headers ? data.headers.length : 1; + td.textContent = data.message || 'No FG data found for the selected criteria.'; + td.style.textAlign = 'center'; + td.style.fontStyle = 'italic'; + td.style.padding = '20px'; + tr.appendChild(td); + tbody.appendChild(tr); + } + } + + // Function to update report title based on type + function updateReportTitle(reportType) { + const titles = { + '1': 'Daily Complete FG Orders Report', + '2': '5-Day Complete FG Orders Report', + '3': 'FG Items with Defects for Current Day', + '4': 'FG Items with Defects for Last 5 Days', + '5': 'Complete FG Database Report' + }; + + reportTitle.textContent = titles[reportType] || 'FG Quality Report'; + } + + // Function to update report title for date-based reports + function updateDateReportTitle(reportType, date, startDate, endDate) { + const titles = { + '6': `FG Daily Report for ${date}`, + '7': `FG Date Range Report (${startDate} to ${endDate})`, + '8': `FG Quality Defects Report for ${date}`, + '9': `FG Quality Defects Range Report (${startDate} to ${endDate})` + }; + + reportTitle.textContent = titles[reportType] || 'FG Quality Report'; + } + + // Calendar functionality + function showCalendarModal() { + if (calendarModal) { + calendarModal.style.display = 'block'; + generateCalendar(); + } + } + + function hideCalendarModal() { + if (calendarModal) { + calendarModal.style.display = 'none'; + selectedDate = null; + updateConfirmButton(); + } + } + + function showDateRangeModal() { + if (dateRangeModal) { + dateRangeModal.style.display = 'block'; + const today = new Date().toISOString().split('T')[0]; + document.getElementById('start-date').value = today; + document.getElementById('end-date').value = today; + } + } + + function hideDataRangeModal() { + if (dateRangeModal) { + dateRangeModal.style.display = 'none'; + } + } + + function generateCalendar() { + const calendarDays = document.getElementById('calendar-days'); + const monthYear = document.getElementById('calendar-month-year'); + + if (!calendarDays || !monthYear) return; + + const year = currentDate.getFullYear(); + const month = currentDate.getMonth(); + + monthYear.textContent = `${currentDate.toLocaleString('default', { month: 'long' })} ${year}`; + + // Clear previous days + calendarDays.innerHTML = ''; + + // Get first day of month and number of days + const firstDay = new Date(year, month, 1).getDay(); + const daysInMonth = new Date(year, month + 1, 0).getDate(); + + // Add empty cells for previous month + for (let i = 0; i < firstDay; i++) { + const emptyDay = document.createElement('div'); + emptyDay.className = 'calendar-day empty'; + calendarDays.appendChild(emptyDay); + } + + // Add days of current month + for (let day = 1; day <= daysInMonth; day++) { + const dayElement = document.createElement('div'); + dayElement.className = 'calendar-day'; + dayElement.textContent = day; + + // Check if it's today + const today = new Date(); + if (year === today.getFullYear() && month === today.getMonth() && day === today.getDate()) { + dayElement.classList.add('today'); + } + + dayElement.addEventListener('click', () => { + // Remove previous selection + document.querySelectorAll('.calendar-day.selected').forEach(el => { + el.classList.remove('selected'); + }); + + // Add selection to clicked day + dayElement.classList.add('selected'); + + // Set selected date + selectedDate = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; + console.log('FG Calendar date selected:', selectedDate); + updateConfirmButton(); + }); + + calendarDays.appendChild(dayElement); + } + } + + function updateConfirmButton() { + const confirmButton = document.getElementById('confirm-date'); + if (confirmButton) { + confirmButton.disabled = !selectedDate; + } + } + + // Calendar navigation + const prevMonthBtn = document.getElementById('prev-month'); + const nextMonthBtn = document.getElementById('next-month'); + + if (prevMonthBtn) { + // Clone to remove existing listeners + const newPrevBtn = prevMonthBtn.cloneNode(true); + prevMonthBtn.parentNode.replaceChild(newPrevBtn, prevMonthBtn); + + newPrevBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + currentDate.setMonth(currentDate.getMonth() - 1); + generateCalendar(); + }); + } + + if (nextMonthBtn) { + // Clone to remove existing listeners + const newNextBtn = nextMonthBtn.cloneNode(true); + nextMonthBtn.parentNode.replaceChild(newNextBtn, nextMonthBtn); + + newNextBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + currentDate.setMonth(currentDate.getMonth() + 1); + generateCalendar(); + }); + } + + // Calendar modal buttons + const cancelDateBtn = document.getElementById('cancel-date'); + const confirmDateBtn = document.getElementById('confirm-date'); + + if (cancelDateBtn) { + // Clone to remove existing listeners + const newCancelBtn = cancelDateBtn.cloneNode(true); + cancelDateBtn.parentNode.replaceChild(newCancelBtn, cancelDateBtn); + + newCancelBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + hideCalendarModal(); + }); + } + + if (confirmDateBtn) { + // Clone to remove existing listeners + const newConfirmBtn = confirmDateBtn.cloneNode(true); + confirmDateBtn.parentNode.replaceChild(newConfirmBtn, confirmDateBtn); + + newConfirmBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + console.log('FG Calendar confirm clicked with date:', selectedDate, 'report type:', currentReportType); + if (selectedDate && currentReportType) { + fetchFGDateReportData(currentReportType, selectedDate); + hideCalendarModal(); + } + }); + } + + // Date range modal buttons + const cancelDateRangeBtn = document.getElementById('cancel-date-range'); + const confirmDateRangeBtn = document.getElementById('confirm-date-range'); + + if (cancelDateRangeBtn) { + // Clone to remove existing listeners + const newCancelRangeBtn = cancelDateRangeBtn.cloneNode(true); + cancelDateRangeBtn.parentNode.replaceChild(newCancelRangeBtn, cancelDateRangeBtn); + + newCancelRangeBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + hideDataRangeModal(); + }); + } + + if (confirmDateRangeBtn) { + // Clone to remove existing listeners + const newConfirmRangeBtn = confirmDateRangeBtn.cloneNode(true); + confirmDateRangeBtn.parentNode.replaceChild(newConfirmRangeBtn, confirmDateRangeBtn); + + newConfirmRangeBtn.addEventListener('click', (e) => { + e.preventDefault(); + e.stopPropagation(); + const startDate = document.getElementById('start-date').value; + const endDate = document.getElementById('end-date').value; + + console.log('FG Date range confirm clicked:', startDate, 'to', endDate, 'report type:', currentReportType); + if (startDate && endDate && currentReportType) { + fetchFGDateReportData(currentReportType, null, startDate, endDate); + hideDataRangeModal(); + } + }); + } + + // Enable/disable date range confirm button + const startDateInput = document.getElementById('start-date'); + const endDateInput = document.getElementById('end-date'); + + function updateDateRangeConfirmButton() { + const confirmBtn = document.getElementById('confirm-date-range'); + if (confirmBtn && startDateInput && endDateInput) { + confirmBtn.disabled = !startDateInput.value || !endDateInput.value; + } + } + + if (startDateInput) { + startDateInput.addEventListener('change', updateDateRangeConfirmButton); + } + + if (endDateInput) { + endDateInput.addEventListener('change', updateDateRangeConfirmButton); + } + + // Close modals when clicking outside + window.addEventListener('click', (event) => { + if (event.target === calendarModal) { + hideCalendarModal(); + } + if (event.target === dateRangeModal) { + hideDataRangeModal(); + } + }); + + // Close modals with X button + document.querySelectorAll('.close-modal').forEach(closeBtn => { + // Clone to remove existing listeners + const newCloseBtn = closeBtn.cloneNode(true); + closeBtn.parentNode.replaceChild(newCloseBtn, closeBtn); + + newCloseBtn.addEventListener('click', (event) => { + event.preventDefault(); + event.stopPropagation(); + const modal = event.target.closest('.modal'); + if (modal) { + modal.style.display = 'none'; + } + }); + }); + + // Export functionality + if (exportCsvButton) { + exportCsvButton.addEventListener('click', () => { + const rows = reportTable.querySelectorAll('tr'); + if (rows.length === 0) { + alert('No FG data available to export.'); + return; + } + const reportTitleText = reportTitle.textContent.trim(); + const filename = `${reportTitleText.replace(/\s+/g, '_')}.csv`; + exportTableToCSV(filename); + }); + } + + // Export to CSV function + function exportTableToCSV(filename) { + const table = reportTable; + const rows = Array.from(table.querySelectorAll('tr')); + + const csvContent = rows.map(row => { + const cells = Array.from(row.querySelectorAll('th, td')); + return cells.map(cell => { + let text = cell.textContent.trim(); + // Escape quotes and wrap in quotes if necessary + if (text.includes(',') || text.includes('"') || text.includes('\n')) { + text = '"' + text.replace(/"/g, '""') + '"'; + } + return text; + }).join(','); + }).join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', filename); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + // Test Database Button + const testDatabaseBtn = document.getElementById('test-database'); + if (testDatabaseBtn) { + testDatabaseBtn.addEventListener('click', () => { + console.log('Testing FG database connection...'); + reportTitle.textContent = 'Testing FG Database Connection...'; + fetch('/test_fg_database') + .then(response => response.json()) + .then(data => { + console.log('FG Database test results:', data); + if (data.success) { + reportTitle.textContent = `FG Database Test Results - ${data.total_records} records found`; + // Show alert with summary + alert(`FG Database Test Complete!\n\nConnection: ${data.database_connection}\nTable exists: ${data.table_exists}\nTotal records: ${data.total_records}\nMessage: ${data.message}`); + } else { + reportTitle.textContent = 'FG Database Test Failed'; + alert(`FG Database test failed: ${data.message}`); + } + }) + .catch(error => { + console.error('FG Database test error:', error); + reportTitle.textContent = 'Error testing FG database.'; + alert('Error testing FG database connection.'); + }); + }); + } + + console.log('FG Quality JavaScript setup complete'); +}); \ No newline at end of file diff --git a/py_app/app/templates/base.html b/py_app/app/templates/base.html index f120e3c..414c53d 100755 --- a/py_app/app/templates/base.html +++ b/py_app/app/templates/base.html @@ -4,11 +4,16 @@ {% block title %}Flask App{% endblock %} + + + + + {% block extra_css %}{% endblock %} {% block head %}{% endblock %} @@ -56,5 +61,8 @@ {% if request.endpoint != 'main.fg_quality' %} {% endif %} + + + \ No newline at end of file diff --git a/py_app/app/templates/fg_quality.html b/py_app/app/templates/fg_quality.html new file mode 100644 index 0000000..f5ce008 --- /dev/null +++ b/py_app/app/templates/fg_quality.html @@ -0,0 +1,440 @@ +{% extends "base.html" %} +{% block title %}FG Quality Module{% endblock %} +{% block content %} +
+ +
+

FG Quality Reports

+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ +
+ + + {% if session.get('role') == 'superadmin' %} + + {% endif %} +
+
+
+
+ + +
+

No data to display, please select a report.

+
+ + + + + + + + + +
+
+
+
+ + + + + + + +{% endblock %} + +{% block head %} + + + +{% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/main_page_reports.html b/py_app/app/templates/main_page_reports.html new file mode 100644 index 0000000..3c17625 --- /dev/null +++ b/py_app/app/templates/main_page_reports.html @@ -0,0 +1,40 @@ +{% extends "base.html" %} + +{% block title %}Reports Module{% endblock %} + +{% block content %} +
+

Reports Module

+

Access different quality and production reporting modules for data analysis and verification.

+ + +
+ +
+

Quality Reports

+

Access quality scanning reports and analysis for production process verification.

+ +
+ + +
+

FG Quality Reports

+

Finished Goods quality reports and analysis for final product verification.

+ +
+ + +
+

Additional Reports

+

Access additional reporting modules and data analysis tools.

+
+ +
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/settings.html b/py_app/app/templates/settings.html index b340d35..b73f197 100755 --- a/py_app/app/templates/settings.html +++ b/py_app/app/templates/settings.html @@ -44,9 +44,7 @@ 🎯 Manage Users (Simplified) - - ⚙️ Advanced Role Permissions - + Recommended: Use the simplified user management for easier administration diff --git a/py_app/app/templates/user_management_simple.html b/py_app/app/templates/user_management_simple.html new file mode 100644 index 0000000..8c5aa78 --- /dev/null +++ b/py_app/app/templates/user_management_simple.html @@ -0,0 +1,890 @@ +{% extends "base.html" %} +{% block title %}User Management - Simplified{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+

Simplified User Management

+

Manage users with the new 4-tier permission system: Superadmin → Admin → Manager → Worker

+ +
+ +
+
+
+
👤 Create New User
+
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ + + + +
+
+
+
+ + +
+
+
+
⚙️ Edit User Rights
+
+
+
+
+ +

Select a user from the table below to edit their rights and module access.

+
+ + + +
+
+
+
+
+ + +
+
+
👥 Current Users
+
+
+ {% if users %} +
+ + + + + + + + + + + + {% for user in users %} + + + + + + + + {% endfor %} + +
UsernameRoleModulesAccess LevelActions
+ {{ user.username }} + + + {{ user.role.title() }} + + +
+ {% if user.role in ['superadmin', 'admin'] %} + All Modules + {% elif user.modules %} + {% for module in user.get_modules() %} + {{ module.title() }} + {% endfor %} + {% else %} + No modules assigned + {% endif %} +
+
+ + {% if user.role == 'superadmin' %} + Full system access + {% elif user.role == 'admin' %} + Full app access + {% elif user.role == 'manager' %} + Full module access + reports + {% elif user.role == 'worker' %} + Basic operations only (no reports) - Can have multiple modules + {% endif %} + + +
+ + + {% if user.username != session.get('user') %} + + {% endif %} +
+
+
+ {% else %} +
+ +

No users found. Create your first user above!

+
+ {% endif %} +
+
+
+ + + + + + +{% endblock %} \ No newline at end of file diff --git a/py_app/check_users.py b/py_app/check_users.py new file mode 100644 index 0000000..9b6fa15 --- /dev/null +++ b/py_app/check_users.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import pymysql + +try: + # Connect to the database + conn = pymysql.connect( + host='localhost', + database='trasabilitate', + user='trasabilitate', + password='Initial01!', + cursorclass=pymysql.cursors.DictCursor + ) + + with conn.cursor() as cursor: + cursor.execute("SELECT id, username, role, modules FROM users") + users = cursor.fetchall() + + print("Current users in database:") + print("-" * 50) + for user in users: + print(f"ID: {user['id']}") + print(f"Username: {user['username']}") + print(f"Role: {user['role']}") + print(f"Modules: {user['modules']}") + print("-" * 30) + +finally: + conn.close() \ No newline at end of file diff --git a/py_app/debug_modules.py b/py_app/debug_modules.py new file mode 100644 index 0000000..6ec839c --- /dev/null +++ b/py_app/debug_modules.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +import pymysql +import json + +try: + # Connect to the database + conn = pymysql.connect( + host='localhost', + database='trasabilitate', + user='trasabilitate', + password='Initial01!', + cursorclass=pymysql.cursors.DictCursor + ) + + with conn.cursor() as cursor: + cursor.execute("SELECT id, username, role, modules FROM users") + users = cursor.fetchall() + + print("Debug: User data and get_modules() output:") + print("=" * 60) + + for user_data in users: + print(f"Username: {user_data['username']}") + print(f"Role: {user_data['role']}") + print(f"Raw modules: {user_data['modules']} (type: {type(user_data['modules'])})") + + # Simulate the get_modules() method + modules = user_data['modules'] + if not modules: + parsed_modules = [] + else: + try: + parsed_modules = json.loads(modules) + except: + parsed_modules = [] + + print(f"Parsed modules: {parsed_modules} (type: {type(parsed_modules)})") + print(f"JSON output: {json.dumps(parsed_modules)}") + print("-" * 40) + +finally: + conn.close() \ No newline at end of file diff --git a/py_app/fix_user_data.py b/py_app/fix_user_data.py new file mode 100644 index 0000000..c5827de --- /dev/null +++ b/py_app/fix_user_data.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python3 + +import pymysql +import json + +try: + # Connect to the database + conn = pymysql.connect( + host='localhost', + database='trasabilitate', + user='trasabilitate', + password='Initial01!', + cursorclass=pymysql.cursors.DictCursor + ) + + with conn.cursor() as cursor: + # Update Ciprian's role from quality_manager to manager + print("Updating Ciprian's role from 'quality_manager' to 'manager'...") + cursor.execute("UPDATE users SET role = 'manager' WHERE username = 'Ciprian'") + + # Assign quality module to Ciprian since he was a quality manager + quality_modules = json.dumps(['quality']) + print(f"Assigning quality module to Ciprian: {quality_modules}") + cursor.execute("UPDATE users SET modules = %s WHERE username = 'Ciprian'", (quality_modules,)) + + # Commit the changes + conn.commit() + print("Database updated successfully!") + + # Show updated users + print("\nUpdated users:") + print("-" * 50) + cursor.execute("SELECT id, username, role, modules FROM users") + users = cursor.fetchall() + + for user in users: + print(f"ID: {user['id']}") + print(f"Username: {user['username']}") + print(f"Role: {user['role']}") + print(f"Modules: {user['modules']}") + print("-" * 30) + +finally: + conn.close() \ No newline at end of file diff --git a/py_app/test_ciprian_access.py b/py_app/test_ciprian_access.py new file mode 100644 index 0000000..0695742 --- /dev/null +++ b/py_app/test_ciprian_access.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 + +import pymysql +import json + +def test_login_data(): + try: + # Connect to the database + conn = pymysql.connect( + host='localhost', + database='trasabilitate', + user='trasabilitate', + password='Initial01!', + cursorclass=pymysql.cursors.DictCursor + ) + + with conn.cursor() as cursor: + # Simulate login for Ciprian + cursor.execute("SELECT username, password, role, modules FROM users WHERE username = 'Ciprian'") + user = cursor.fetchone() + + if user: + print("Ciprian's database record:") + print(f"Username: {user['username']}") + print(f"Role: {user['role']}") + print(f"Raw modules: {user['modules']}") + + # Simulate what happens in login + user_modules = [] + if user['modules']: + try: + user_modules = json.loads(user['modules']) + print(f"Parsed modules: {user_modules}") + except Exception as e: + print(f"Error parsing modules: {e}") + user_modules = [] + + # Check if user should have quality access + has_quality = 'quality' in user_modules + print(f"Has quality module access: {has_quality}") + + # Check role level + ROLES = { + 'superadmin': {'level': 100}, + 'admin': {'level': 90}, + 'manager': {'level': 70}, + 'worker': {'level': 50} + } + + user_level = ROLES.get(user['role'], {}).get('level', 0) + print(f"Role level: {user_level}") + + # Test access control logic + print("\nAccess Control Test:") + print(f"Required modules: ['quality']") + print(f"User role: {user['role']}") + print(f"User modules: {user_modules}") + + if user['role'] in ['superadmin', 'admin']: + print("✅ Access granted: Superadmin/Admin has access to all modules") + elif any(module in user_modules for module in ['quality']): + print("✅ Access granted: User has required quality module") + else: + print("❌ Access denied: User does not have quality module") + + else: + print("User 'Ciprian' not found!") + + finally: + conn.close() + +if __name__ == "__main__": + test_login_data() \ No newline at end of file diff --git a/py_app/test_worker_modules.py b/py_app/test_worker_modules.py new file mode 100644 index 0000000..ee94848 --- /dev/null +++ b/py_app/test_worker_modules.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 +""" +Quick test for updated worker permissions +""" + +import sys +import os +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app')) + +from permissions_simple import validate_user_modules + +def test_worker_multiple_modules(): + """Test that workers can now have multiple modules""" + print("Testing Updated Worker Module Permissions") + print("=" * 45) + + test_cases = [ + # (role, modules, expected_result, description) + ('worker', ['quality'], True, "Worker with quality module"), + ('worker', ['warehouse'], True, "Worker with warehouse module"), + ('worker', ['quality', 'warehouse'], True, "Worker with multiple modules (NEW)"), + ('worker', ['quality', 'warehouse', 'labels'], True, "Worker with all modules (NEW)"), + ('worker', [], False, "Worker with no modules"), + ('manager', ['quality', 'warehouse'], True, "Manager with multiple modules"), + ] + + passed = 0 + failed = 0 + + for role, modules, expected, description in test_cases: + is_valid, error_msg = validate_user_modules(role, modules) + status = "PASS" if is_valid == expected else "FAIL" + + print(f"{status}: {description}") + print(f" Role: {role}, Modules: {modules} -> {is_valid} (expected {expected})") + if error_msg: + print(f" Error: {error_msg}") + print() + + if is_valid == expected: + passed += 1 + else: + failed += 1 + + print(f"Results: {passed} passed, {failed} failed") + print("\n✅ Workers can now have multiple modules!" if failed == 0 else "❌ Some tests failed") + +if __name__ == "__main__": + test_worker_multiple_modules() \ No newline at end of file diff --git a/run/trasabilitate.pid b/run/trasabilitate.pid new file mode 100644 index 0000000..5a07609 --- /dev/null +++ b/run/trasabilitate.pid @@ -0,0 +1 @@ +299414