├── EvilKit ├── src │ ├── EVKFullURLPortion.m │ ├── EvilKit.h │ ├── EVKAction.h │ ├── NSURL+ComponentAdditions.h │ ├── EVKAppAlternative.h │ ├── EVKMapItemUnwrapperPortion.m │ ├── EVKStaticStringPortion.m │ ├── EVKQueryItemLexicon.h │ ├── EVKPercentEncodablePortion.m │ ├── NSURL+ComponentAdditions.m │ ├── EVKAppAlternative.m │ ├── EVKAction.m │ ├── EVKQueryItemLexicon.m │ ├── EVKTranslatedQueryPortion.m │ ├── EVKRegexSubstitutionPortion.m │ ├── EVKKeyValuePathPortion.m │ ├── EVKURLPortions.h │ └── EVKURLPortions.m ├── install_to_theos.sh ├── Makefile ├── Resources │ └── Info.plist └── Tests │ └── EvilKitTests.m ├── ZZ_EvilScheme.plist ├── Prefs ├── Resources │ ├── icon.png │ ├── icon@2x.png │ ├── icon@3x.png │ └── Info.plist ├── L0Prefs │ ├── L0LinkCell.h │ ├── L0ButtonCell.h │ ├── L0ProseCell.h │ ├── L0PureEditTextCell.h │ ├── L0ToggleCell.h │ ├── L0DataCell.h │ ├── L0PickerCell.h │ ├── L0Prefs.h │ ├── L0EditTextCell.h │ ├── L0ButtonCell.m │ ├── L0LinkCell.m │ ├── L0StepperCell.h │ ├── L0ToggleCell.m │ ├── L0DictionaryController.h │ ├── L0StepperCell.m │ ├── L0ProseCell.m │ ├── L0EditTextCell.m │ ├── L0PrefVC.h │ ├── L0DictionaryController.m │ ├── L0PickerCell.m │ ├── L0DataCell.m │ ├── L0PureEditTextCell.m │ └── L0PrefVC.m ├── src │ ├── EVSExperimentalPrefsVC.h │ ├── EVSLogVC.h │ ├── EVSRootVC.h │ ├── EVSPresetListVC.h │ ├── EVSKeyValuePairVC.h │ ├── EVSOutlineVC.h │ ├── EVSQueryTranslatorVC.h │ ├── EVSQueryLexiconVC.h │ ├── EVSAppAlternativeVC.h │ ├── EVSQueryLexiconWrapper.h │ ├── EVSPortionVC.h │ ├── EVSPortionVM.h │ ├── EVSAppAlternativeWrapper.h │ ├── EVSPreferenceManager.h │ ├── EVSQueryLexiconWrapper.m │ ├── EVSAppAlternativeWrapper.m │ ├── EVSPresetListVC.m │ ├── EVSKeyValuePairVC.m │ ├── EVSPortionVM.m │ ├── EVSQueryTranslatorVC.m │ ├── EVSLogVC.m │ ├── EVSPortionVC.m │ ├── EVSQueryLexiconVC.m │ ├── EVSOutlineVC.m │ ├── EVSExperimentalPrefsVC.m │ ├── EVSRootVC.m │ └── EVSAppAlternativeVC.m ├── entry.plist └── Makefile ├── packages └── net.pane.l.evilscheme_19.07.2002_iphoneos-arm64.deb ├── control ├── .github └── ISSUE_TEMPLATE │ ├── preset-request.md │ ├── feature_request.md │ └── bug_report.md ├── Makefile ├── DragonMake ├── LICENSE ├── README.md ├── PrivateFrameworks.h └── EvilScheme.x /EvilKit/src/EVKFullURLPortion.m: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ZZ_EvilScheme.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.apple.FrontBoardServices" ); }; } 2 | -------------------------------------------------------------------------------- /Prefs/Resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xkuj/EvilScheme/HEAD/Prefs/Resources/icon.png -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0LinkCell.h: -------------------------------------------------------------------------------- 1 | #import "L0DataCell.h" 2 | 3 | @interface L0LinkCell : L0DataCell 4 | @end 5 | -------------------------------------------------------------------------------- /Prefs/Resources/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xkuj/EvilScheme/HEAD/Prefs/Resources/icon@2x.png -------------------------------------------------------------------------------- /Prefs/Resources/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xkuj/EvilScheme/HEAD/Prefs/Resources/icon@3x.png -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0ButtonCell.h: -------------------------------------------------------------------------------- 1 | #import "L0DataCell.h" 2 | 3 | @interface L0ButtonCell : L0DataCell 4 | @end 5 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0ProseCell.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "L0DataCell.h" 3 | 4 | @interface L0ProseCell : L0DataCell 5 | @end 6 | -------------------------------------------------------------------------------- /packages/net.pane.l.evilscheme_19.07.2002_iphoneos-arm64.deb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xkuj/EvilScheme/HEAD/packages/net.pane.l.evilscheme_19.07.2002_iphoneos-arm64.deb -------------------------------------------------------------------------------- /EvilKit/src/EvilKit.h: -------------------------------------------------------------------------------- 1 | #import "EVKAction.h" 2 | #import "EVKAppAlternative.h" 3 | #import "EVKQueryItemLexicon.h" 4 | #import "EVKURLPortions.h" 5 | #import "NSURL+ComponentAdditions.h" 6 | -------------------------------------------------------------------------------- /Prefs/src/EVSExperimentalPrefsVC.h: -------------------------------------------------------------------------------- 1 | #import "../L0Prefs/L0Prefs.h" 2 | 3 | @interface EVSExperimentalPrefsVC : L0PrefVC 4 | @end 5 | -------------------------------------------------------------------------------- /Prefs/src/EVSLogVC.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "../L0Prefs/L0Prefs.h" 3 | 4 | @interface EVSLogVC : L0PrefVC 5 | @end 6 | -------------------------------------------------------------------------------- /Prefs/src/EVSRootVC.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "../L0Prefs/L0Prefs.h" 3 | #import "EVSAppAlternativeVC.h" 4 | 5 | @interface EVSRootVC : L0PrefVC 6 | - (void)saveSettings; 7 | @end 8 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0PureEditTextCell.h: -------------------------------------------------------------------------------- 1 | #import "L0EditTextCell.h" 2 | 3 | @interface L0PureEditTextCell : UITableViewCell 4 | 5 | @property (atomic, strong) UITextField *field; 6 | @property (nonatomic, weak) id delegate; 7 | 8 | @end 9 | -------------------------------------------------------------------------------- /Prefs/src/EVSPresetListVC.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "EVSAppAlternativeVC.h" 3 | #import "../L0Prefs/L0Prefs.h" 4 | 5 | @interface EVSPresetListVC : L0PrefVC 6 | @property (atomic, strong) L0DictionaryController *> *presets; 7 | @end 8 | -------------------------------------------------------------------------------- /Prefs/src/EVSKeyValuePairVC.h: -------------------------------------------------------------------------------- 1 | #import "../L0Prefs/L0Prefs.h" 2 | 3 | @interface EVSKeyValuePairVC : L0PrefVC 4 | 5 | @property (atomic, strong) NSString *key; 6 | @property (atomic, strong) NSString *value; 7 | 8 | - (instancetype)initWithKey:(NSString *)key value:(NSString *)value; 9 | 10 | @end 11 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0ToggleCell.h: -------------------------------------------------------------------------------- 1 | #import "L0DataCell.h" 2 | 3 | @protocol L0ToggleCellDelegate 4 | - (void)switchValueDidChange:(UISwitch *)toggle; 5 | @end 6 | 7 | @interface L0ToggleCell : L0DataCell 8 | @property (atomic, strong) UISwitch *toggle; 9 | @property (atomic, weak) id delegate; 10 | @end 11 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Name: EvilScheme 2 | Author: Lorenzo Pane 3 | Maintainer: 0xkuj 4 | Description: Manage default browser, navigation app, package manager, and more! 5 | Package: net.pane.l.evilscheme 6 | Replaces: com.lpane.browserdefault,com.lpane.googlemapsdefault 7 | Architecture: iphoneos-arm 8 | Section: Tweaks 9 | Version: 19.07.2002 10 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0DataCell.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface L0DataCell : UITableViewCell 4 | 5 | @property (atomic, strong) UIView *detailView; 6 | 7 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 8 | reuseIdentifier:(NSString *)reuseIdentifier 9 | detailView:(UIView *)detailView; 10 | @end 11 | -------------------------------------------------------------------------------- /EvilKit/install_to_theos.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | make clean 3 | make FINALPACKAGE=1 4 | cp -Rv "./.theos/obj/EvilKit.framework" "$THEOS/lib" 5 | 6 | make clean 7 | make FINALPACKAGE=1 THEOS_PACKAGE_SCHEME=rootless 8 | mkdir -p "$THEOS/lib/iphone/rootless/lib" 9 | cp -Rv "./.theos/obj/EvilKit.framework" "$THEOS/lib/iphone/rootless" 10 | 11 | echo "Successfully installed EvilKit" -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0PickerCell.h: -------------------------------------------------------------------------------- 1 | #import "L0EditTextCell.h" 2 | 3 | @interface L0PickerCell : L0EditTextCell 4 | 5 | @property (atomic, strong) UIPickerView *picker; 6 | @property (atomic, strong) NSArray *options; 7 | 8 | - (void)selectIndex:(NSUInteger)idx; 9 | - (void)selectObject:(NSString *)obj; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Prefs/src/EVSOutlineVC.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "../L0Prefs/L0Prefs.h" 4 | #import "EVSPortionVC.h" 5 | 6 | @interface EVSOutlineVC : L0PrefVC 7 | @property (atomic, strong) EVKAction *action; 8 | @property (atomic, assign) NSUInteger index; 9 | @end 10 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0Prefs.h: -------------------------------------------------------------------------------- 1 | #import "L0ButtonCell.h" 2 | #import "L0DataCell.h" 3 | #import "L0DictionaryController.h" 4 | #import "L0EditTextCell.h" 5 | #import "L0LinkCell.h" 6 | #import "L0PickerCell.h" 7 | #import "L0PrefVC.h" 8 | #import "L0ProseCell.h" 9 | #import "L0PureEditTextCell.h" 10 | #import "L0StepperCell.h" 11 | #import "L0ToggleCell.h" 12 | #import "L0ToggleCell.h" 13 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0EditTextCell.h: -------------------------------------------------------------------------------- 1 | #import "L0DataCell.h" 2 | 3 | @protocol L0TextCellDelegate 4 | @optional 5 | - (void)textFieldDidChange:(UITextField *)field; 6 | @end 7 | 8 | @interface L0EditTextCell : L0DataCell 9 | @property (atomic, strong) UITextField *field; 10 | @property (nonatomic, weak) id delegate; 11 | @end 12 | -------------------------------------------------------------------------------- /Prefs/src/EVSQueryTranslatorVC.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "../L0Prefs/L0Prefs.h" 3 | 4 | @interface EVSQueryTranslatorVC : L0PrefVC 5 | 6 | @property (atomic, strong) L0DictionaryController *dict; 7 | @property (atomic, assign) NSInteger tag; 8 | 9 | - (instancetype)initWithDictionary:(NSDictionary *)dict; 10 | 11 | @end 12 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0ButtonCell.m: -------------------------------------------------------------------------------- 1 | #import "L0Prefs.h" 2 | 3 | @implementation L0ButtonCell 4 | 5 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier { 7 | if((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { 8 | [[self textLabel] setTextColor:TINT_COLOR]; 9 | } 10 | 11 | return self; 12 | } 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Prefs/src/EVSQueryLexiconVC.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "../L0Prefs/L0Prefs.h" 3 | #import "EVSQueryLexiconWrapper.h" 4 | 5 | @interface EVSQueryLexiconVC : L0PrefVC 6 | 7 | @property (atomic, strong) EVSQueryLexiconWrapper *lex; 8 | @property (atomic, strong) NSString *key; 9 | 10 | - (instancetype)initWithKey:(NSString *)key lexicon:(EVSQueryLexiconWrapper *)lex; 11 | 12 | @end 13 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0LinkCell.m: -------------------------------------------------------------------------------- 1 | #import "L0Prefs.h" 2 | 3 | @implementation L0LinkCell 4 | 5 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier { 7 | if((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { 8 | [self setAccessoryType:UITableViewCellAccessoryDisclosureIndicator]; 9 | } 10 | 11 | return self; 12 | } 13 | 14 | @end 15 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0StepperCell.h: -------------------------------------------------------------------------------- 1 | #import "L0DataCell.h" 2 | 3 | @protocol L0StepperCellDelegate 4 | - (void)stepperValueDidChange:(UIStepper *)stepper; 5 | @end 6 | 7 | @interface L0StepperCell : L0DataCell 8 | @property (atomic, strong) UIStepper *stepper; 9 | @property (atomic, strong) NSString *prefix; 10 | @property (atomic, weak) id delegate; 11 | 12 | - (void)stepperValueDidChange:(UIStepper *)stepper; 13 | @end 14 | -------------------------------------------------------------------------------- /Prefs/src/EVSAppAlternativeVC.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "../L0Prefs/L0Prefs.h" 3 | #import "EVSAppAlternativeWrapper.h" 4 | #import "EVSOutlineVC.h" 5 | 6 | @interface EVSAppAlternativeVC : L0PrefVC 7 | 8 | @property (atomic, strong) EVSAppAlternativeWrapper *appAlternative; 9 | @property (atomic, assign) NSInteger index; 10 | 11 | - (void)showPresetView; 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /Prefs/src/EVSQueryLexiconWrapper.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "../L0Prefs/L0Prefs.h" 3 | 4 | @interface EVSQueryLexiconWrapper : L0DictionaryController 5 | 6 | @property (atomic, strong) EVKQueryItemLexicon *lex; 7 | 8 | - (instancetype)initWithLexicon:(EVKQueryItemLexicon *)lex; 9 | 10 | - (NSString *)defaultState; 11 | - (void)setDefaultState:(NSString *)state; 12 | 13 | - (NSString *)param; 14 | - (void)setParam:(NSString *)str; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /Prefs/src/EVSPortionVC.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "../L0Prefs/L0Prefs.h" 4 | #import "EVSPortionVM.h" 5 | 6 | @interface EVSPortionVC : L0PrefVC 7 | 8 | @property (atomic, assign) NSInteger index; 9 | @property (atomic, strong) EVSPortionVM *portion; 10 | 11 | - (instancetype)initWithPortion:(EVSPortionVM *)portion withIndex:(NSInteger)idx; 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/preset-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Preset Request 3 | about: Suggestion for a preset 4 | title: "[PRESET REQUEST]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Preset destination** 11 | The app you want links to open in. E.g. Google Maps 12 | 13 | **Preset target** 14 | That app's stock counterpart. E.g. Apple Maps 15 | 16 | **URL Scheme Info** 17 | Have you found any info about the URL scheme support for the destination? If it does not have URL scheme or universal link support: sorry, I can't add support for it. 18 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export ARCHS := arm64 arm64e 2 | export TARGET = iphone:clang:14.5:14.5 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | TWEAK_NAME = ZZ_EvilScheme 7 | ZZ_EvilScheme_FILES = EvilScheme.x 8 | ZZ_EvilScheme_CFLAGS = -fobjc-arc 9 | ZZ_EvilScheme_PRIVATE_FRAMEWORKS = UserActivity CoreServices 10 | ZZ_EvilScheme_EXTRA_FRAMEWORKS += EvilKit 11 | 12 | include $(THEOS_MAKE_PATH)/tweak.mk 13 | 14 | after-install:: 15 | install.exec "sbreload || killall -9 SpringBoard" 16 | 17 | SUBPROJECTS += EvilKit 18 | SUBPROJECTS += Prefs 19 | include $(THEOS_MAKE_PATH)/aggregate.mk 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE REQUEST]" 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /Prefs/entry.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | entry 6 | 7 | bundle 8 | EvilSchemePrefs 9 | cell 10 | PSLinkCell 11 | detail 12 | EVSRootVC 13 | icon 14 | icon.png 15 | isController 16 | 17 | label 18 | Evil Scheme 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /EvilKit/Makefile: -------------------------------------------------------------------------------- 1 | export ARCHS := arm64 arm64e 2 | export TARGET = iphone:clang:14.5:14.5 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | SAUCE = $(shell find src -name '*.m') 7 | HEADERS = $(shell find src -name '*.h') 8 | 9 | FRAMEWORK_NAME = EvilKit 10 | EvilKit_FILES = $(SAUCE) 11 | EvilKit_PUBLIC_HEADERS = $(HEADERS) 12 | EvilKit_INSTALL_PATH = /var/jb/Library/Frameworks 13 | EvilKit_PRIVATE_FRAMEWORKS = GeoServices 14 | EvilKit_LDFLAGS += -install_name @rpath/EvilKit.framework/EvilKit 15 | EvilKit_CFLAGS = -fobjc-arc 16 | 17 | include $(THEOS_MAKE_PATH)/framework.mk 18 | include $(THEOS_MAKE_PATH)/aggregate.mk 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Extra Info** 27 | Please include 28 | 1. Device model 29 | 2. iOS version 30 | 3. Evil Scheme version 31 | 4. Crash report (if applicable) 32 | -------------------------------------------------------------------------------- /EvilKit/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | EvilKit 9 | CFBundleIdentifier 10 | net.pane.l.evilkit 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | FMWK 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0ToggleCell.m: -------------------------------------------------------------------------------- 1 | #import "L0Prefs.h" 2 | 3 | @implementation L0ToggleCell 4 | 5 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier { 7 | UISwitch *toggle = [UISwitch new]; 8 | if((self = [super initWithStyle:style 9 | reuseIdentifier:reuseIdentifier 10 | detailView:toggle])) { 11 | _toggle = toggle; 12 | [_toggle addTarget:self 13 | action:@selector(switchValueDidChange:) 14 | forControlEvents:UIControlEventValueChanged]; 15 | } 16 | 17 | return self; 18 | } 19 | 20 | - (void)switchValueDidChange:(UISwitch *)toggle { 21 | [[self delegate] switchValueDidChange:toggle]; 22 | } 23 | 24 | @end 25 | 26 | -------------------------------------------------------------------------------- /DragonMake: -------------------------------------------------------------------------------- 1 | name: ZZ_EvilScheme 2 | icmd: sbreload || killall backboardd 3 | 4 | all: 5 | target: ios 6 | targetvers: 13.0 7 | archs: 8 | - arm64 9 | - arm64e 10 | 11 | EvilKit: 12 | dir: EvilKit 13 | type: framework 14 | install_location: '/Library/Frameworks/EvilKit.framework/' 15 | objc_files: 16 | - 'src/*.m' 17 | public_headers: 18 | - 'src/*.h' 19 | frameworks: 20 | - GeoServices 21 | 22 | ZZ_EvilScheme: 23 | type: tweak 24 | logos_files: 25 | - 'EvilScheme.x' 26 | frameworks: 27 | - UserActivity 28 | - EvilKit 29 | - CoreServices 30 | 31 | EvilSchemePrefs: 32 | dir: Prefs 33 | type: prefs 34 | objc_files: 35 | - 'L0Prefs/*.m' 36 | - 'src/*.m' 37 | header_includes: 38 | - 'L0Prefs/*.h' 39 | - 'src/*.h' 40 | frameworks: 41 | - Preferences 42 | - EvilKit 43 | -------------------------------------------------------------------------------- /Prefs/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | EvilSchemePrefs 9 | CFBundleIdentifier 10 | net.pane.l.evilschemeprefs 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1.0 21 | NSPrincipalClass 22 | EVSRootVC 23 | 24 | 25 | -------------------------------------------------------------------------------- /Prefs/src/EVSPortionVM.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface EVSPortionVM : NSObject 5 | 6 | @property (atomic, strong) NSObject *portion; 7 | 8 | + (NSDictionary *)propertyNameMappings; 9 | + (NSDictionary *)classNameMappings; 10 | 11 | - (instancetype)initWithPortion:(NSObject *)portion; 12 | - (NSString *)stringRepresentation; 13 | - (int)propertyCount; 14 | - (NSString *)propertyNameForIndex:(NSInteger)index; 15 | - (id)objectForPropertyIndex:(NSInteger)index; 16 | - (void)setObject:(NSObject *)obj forPropertyIndex:(NSInteger)index; 17 | - (NSString *)valueStringForIndex:(NSInteger)index; 18 | - (NSString *)valueStringForKey:(NSString *)key; 19 | - (Class)cellTypeForIndex:(NSInteger)index; 20 | - (Class)cellTypeForKey:(NSString *)index; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0DictionaryController.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface L0DictionaryController : NSObject 4 | 5 | @property (atomic, strong) NSDictionary *dict; 6 | 7 | - (instancetype)initWithDict:(NSDictionary *)dict; 8 | 9 | - (NSInteger)count; 10 | - (BOOL)containsKey:(NSString *)key; 11 | 12 | - (NSArray *)keys; 13 | - (NSArray *)objects; 14 | 15 | - (NSString *)keyAtIndex:(NSInteger)idx; 16 | 17 | - (T)objectForKey:(NSString *)key; 18 | - (T)objectAtIndex:(NSInteger)idx; 19 | 20 | - (void)removeObjectForKey:(NSString *)key; 21 | - (void)removeObjectAtIndex:(NSInteger)idx; 22 | 23 | - (void)setObject:(T)obj forKey:(NSString *)key; 24 | - (void)setObject:(T)obj atIndex:(NSInteger)idx; 25 | 26 | - (void)renameKey:(NSString *)key toString:(NSString *)str; 27 | - (void)renameKeyAtIndex:(NSInteger)idx toString:(NSString *)str; 28 | 29 | @end 30 | -------------------------------------------------------------------------------- /Prefs/src/EVSAppAlternativeWrapper.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "../L0Prefs/L0Prefs.h" 4 | 5 | @interface EVSAppAlternativeWrapper : L0DictionaryController 6 | 7 | @property (atomic, strong) EVKAppAlternative *orig; 8 | @property (atomic, strong) NSString *name; 9 | @property (atomic, strong) NSMutableArray *targetBundleIDs; 10 | 11 | - (instancetype)initWithAppAlternative:(EVKAppAlternative *)app name:(NSString *)name; 12 | - (instancetype)initWithAppAlternative:(EVKAppAlternative *)app 13 | name:(NSString *)name 14 | targetBundleIDs:(NSArray *)targets; 15 | - (NSString *)substituteBundleID; 16 | - (void)setSubstituteBundleID:(NSString *)bundleID; 17 | - (NSArray *)urlOutlines; 18 | - (void)setUrlOutlines:(NSArray *)outlines; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /Prefs/src/EVSPreferenceManager.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import "EVSAppAlternativeWrapper.h" 5 | 6 | @interface EVSPreferenceManager : NSObject 7 | 8 | + (void)ensureDirExists:(NSString *)dirString; 9 | + (NSArray *)activeAlternatives; 10 | + (void)setActiveAlternatives:(NSArray *)alternatives; 11 | + (void)applyActiveAlternatives:(NSArray *)alternatives; 12 | + (L0DictionaryController *> *)presets; 13 | + (void)setSearchEngine:(NSString *)engine; 14 | + (NSString *)searchEngine; 15 | + (void)setBlacklistedApps:(NSArray *)apps; 16 | + (NSArray *)blacklistedApps; 17 | + (void)setLogging:(BOOL)logging; 18 | + (BOOL)isLogging; 19 | + (NSDictionary *)logDict; 20 | + (void)setLogDict:(NSDictionary *)dict; 21 | 22 | @end 23 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0StepperCell.m: -------------------------------------------------------------------------------- 1 | #import "L0Prefs.h" 2 | 3 | @implementation L0StepperCell 4 | 5 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier { 7 | UIStepper *stepper = [UIStepper new]; 8 | if((self = [super initWithStyle:style 9 | reuseIdentifier:reuseIdentifier 10 | detailView:stepper])) { 11 | _prefix = @""; 12 | _stepper = stepper; 13 | [_stepper addTarget:self 14 | action:@selector(stepperValueDidChange:) 15 | forControlEvents:UIControlEventValueChanged]; 16 | } 17 | 18 | return self; 19 | } 20 | 21 | - (void)stepperValueDidChange:(UIStepper *)stepper { 22 | [[self textLabel] setText:[NSString stringWithFormat:@"%@: %d", [self prefix], (int)[stepper value]]]; 23 | [[self delegate] stepperValueDidChange:stepper]; 24 | } 25 | 26 | @end 27 | 28 | -------------------------------------------------------------------------------- /Prefs/Makefile: -------------------------------------------------------------------------------- 1 | export ARCHS := arm64 arm64e 2 | export TARGET = iphone:clang:14.5:14.5 3 | 4 | include $(THEOS)/makefiles/common.mk 5 | 6 | BUNDLE_NAME = EvilSchemePrefs 7 | 8 | SAUCE = $(shell find src -name "*.m") $(shell find L0Prefs -name "*.m") 9 | HEADERS = $(shell find src -name "*.h") $(shell find L0Prefs -name "*.h") 10 | 11 | EvilSchemePrefs_FILES = $(SAUCE) 12 | EvilSchemePrefs_HEADERS = $(HEADERS) 13 | EvilSchemePrefs_INSTALL_PATH = /Library/PreferenceBundles 14 | EvilSchemePrefs_FRAMEWORKS = UIKit 15 | EvilSchemePrefs_PRIVATE_FRAMEWORKS = Preferences 16 | EvilSchemePrefs_EXTRA_FRAMEWORKS += EvilKit 17 | EvilSchemePrefs_CFLAGS = -fobjc-arc 18 | 19 | include $(THEOS_MAKE_PATH)/bundle.mk 20 | 21 | internal-stage:: 22 | $(ECHO_NOTHING)mkdir -p $(THEOS_STAGING_DIR)/Library/PreferenceLoader/Preferences$(ECHO_END) 23 | $(ECHO_NOTHING)cp entry.plist $(THEOS_STAGING_DIR)/Library/PreferenceLoader/Preferences/EvilSchemePrefs.plist$(ECHO_END) 24 | -------------------------------------------------------------------------------- /Prefs/src/EVSQueryLexiconWrapper.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "EVSQueryLexiconWrapper.h" 3 | 4 | @implementation EVSQueryLexiconWrapper 5 | 6 | - (instancetype)initWithLexicon:(EVKQueryItemLexicon *)lex { 7 | if((self = [super init])) { 8 | _lex = lex; 9 | } 10 | 11 | return self; 12 | } 13 | 14 | - (NSString *)defaultState { 15 | return [[self lex] defaultState] ? @"Keep original value" : @"Exclude argument"; 16 | } 17 | 18 | - (void)setDefaultState:(NSString *)state { 19 | [[self lex] setDefaultState:[state isEqualToString:@"Keep original value"]]; 20 | } 21 | 22 | - (NSString *)param { 23 | return [[self lex] param]; 24 | } 25 | 26 | - (void)setParam:(NSString *)str { 27 | [[self lex] setParam:str]; 28 | } 29 | 30 | - (NSDictionary *)dict { 31 | return [[self lex] substitutions]; 32 | } 33 | 34 | - (void)setDict:(NSDictionary *)dict { 35 | [[self lex] setSubstitutions:dict]; 36 | } 37 | 38 | @end 39 | -------------------------------------------------------------------------------- /EvilKit/src/EVKAction.h: -------------------------------------------------------------------------------- 1 | #import "EVKURLPortions.h" 2 | 3 | /// Object to represent a regex pattern and its corrosponding blueprint 4 | @interface EVKAction : NSObject 5 | 6 | @property (atomic, strong) NSString *regexPattern; 7 | @property (atomic, strong) NSArray *> *outline; 8 | 9 | /// Initializes a newly allocated action with all properties 10 | /// @param pattern Regular expression pattern 11 | /// @param outline Array of URL fragments 12 | - (instancetype)initWithRegexPattern:(NSString *)pattern 13 | URLOutline:(NSArray *> *)outline; 14 | + (instancetype)actionWithPattern:(NSString *)pattern 15 | outline:(NSArray *> *)outline; 16 | 17 | /// Returns concatenation of all fragments evaulted with a URL if it contains a match for the regex, nil otherwise 18 | - (NSURL *)transformURL:(NSURL *)url; 19 | 20 | @end 21 | -------------------------------------------------------------------------------- /EvilKit/src/NSURL+ComponentAdditions.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @interface NSURL (ComponentAdditions) 4 | 5 | /// Returns query as an array of name/value pairs 6 | - (NSArray *)queryItems; 7 | 8 | /// Returns percent encoded query string 9 | - (NSString *)queryString; 10 | 11 | /// Returns resource specifier excluding leading and trailing slashes 12 | - (NSString *)trimmedResourceSpecifier; 13 | 14 | /// Returns path parsed by NSURLComponents 15 | - (NSString *)trimmedPathComponent; 16 | 17 | /// Returns the host parsed by NSURLComponents 18 | - (NSString *)hostComponent; 19 | 20 | /// Returns the fragment parsed by NSURLComponents 21 | - (NSString *)fragmentString; 22 | 23 | /// Returns YES if the absolute string contains a match for a given regular expression 24 | /// @param regex regular expression to use 25 | - (BOOL)matchesRegularExpression:(NSRegularExpression *)regex; 26 | 27 | /// Returns string value for a given query parameter, nil if key is not found 28 | - (NSString *)queryValueForParameter:(NSString *)param; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /EvilKit/src/EVKAppAlternative.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "EVKURLPortions.h" 3 | #import "EVKAction.h" 4 | 5 | /// Object to represent an altenative app to a default scheme endpoint and the modifications that must be made to it's request 6 | @interface EVKAppAlternative : NSObject 7 | 8 | /// Bundle ID of the default scheme endpoint 9 | @property (copy) NSString *targetBundleID; 10 | 11 | /// Bundle ID of the altenrative app 12 | @property (copy) NSString *substituteBundleID; 13 | 14 | /// Array of EVKActions in order of priority 15 | @property (copy) NSArray *urlOutlines; 16 | 17 | /// Initializes a newly allocated alternative with all properties 18 | /// @param targetBundleID Bundle ID of the default scheme endpoint 19 | /// @param substituteBundleID Bundle ID of the alternative app 20 | /// @param outlines Array of EVKActions in order of priority 21 | - (instancetype)initWithTargetBundleID:(NSString *)targetBundleID 22 | substituteBundleID:(NSString *)substituteBundleID 23 | urlOutlines:(NSArray *)outlines; 24 | 25 | /// Transforms given URL with the proper outline 26 | /// @param url URL to transform 27 | /// @return Transformed url, returns nil if no matches are found 28 | - (NSURL *)transformURL:(NSURL *)url; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0ProseCell.m: -------------------------------------------------------------------------------- 1 | #import "L0ProseCell.h" 2 | 3 | @implementation L0ProseCell 4 | 5 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier { 7 | if((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { 8 | [[self textLabel] setLineBreakMode:NSLineBreakByWordWrapping]; 9 | [[self textLabel] setNumberOfLines:0]; 10 | } 11 | 12 | return self; 13 | } 14 | 15 | 16 | - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event { 17 | [self becomeFirstResponder]; 18 | 19 | CGPoint point = [[touches anyObject] locationInView:[self contentView]]; 20 | UIMenuController *shared = [UIMenuController sharedMenuController]; 21 | [shared setMenuItems:@[ 22 | [[UIMenuItem alloc] initWithTitle:@"Copy" action:@selector(copyText)] 23 | ]]; 24 | [shared showMenuFromView:[self textLabel] rect:CGRectMake(point.x, point.y, 0, 0)]; 25 | 26 | [super touchesEnded:touches withEvent:event]; 27 | } 28 | 29 | - (BOOL)canBecomeFirstResponder { return YES; } 30 | 31 | - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { 32 | return action == @selector(copyText); 33 | } 34 | 35 | - (void)copyText { 36 | [[UIPasteboard generalPasteboard] setString:[[self textLabel] text]]; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /EvilKit/src/EVKMapItemUnwrapperPortion.m: -------------------------------------------------------------------------------- 1 | #import "EVKURLPortions.h" 2 | #import "NSURL+ComponentAdditions.h" 3 | 4 | @interface GEOMapItemStorage : NSObject 5 | -(instancetype)initWithData:(NSData *)data; 6 | -(NSDictionary *)addressDictionary; 7 | @end 8 | 9 | @implementation EVKMapItemUnwrapperPortion 10 | 11 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { 12 | NSData *b64 = [[NSData alloc] initWithBase64EncodedString:[url trimmedResourceSpecifier] 13 | options:0]; 14 | NSDictionary *plist = [NSPropertyListSerialization propertyListWithData:b64 15 | options:0 16 | format:nil 17 | error:nil]; 18 | @try { 19 | NSData *d = plist[@"MKMapItemLaunchAdditionsMapItems"][0][@"MKMapItemGEOMapItem"]; 20 | NSDictionary *addr = [[[GEOMapItemStorage alloc] initWithData:d] addressDictionary]; 21 | 22 | return [NSString stringWithFormat:@"%@ %@ %@, %@ %@", 23 | addr[@"Name"] ? : @"", 24 | addr[@"Street"] ? : @"", 25 | addr[@"City"] ? : @"", 26 | addr[@"State"] ? : @"", 27 | addr[@"ZIP"] ? : @"" 28 | ]; 29 | } @catch (NSException *ex) { 30 | return nil; 31 | } 32 | } 33 | 34 | - (NSString *)stringRepresentation { return @"Address unwrapper"; } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0EditTextCell.m: -------------------------------------------------------------------------------- 1 | #import "L0Prefs.h" 2 | 3 | @implementation L0EditTextCell 4 | 5 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier { 7 | UITextField *textField = [UITextField new]; 8 | [textField setDelegate:self]; 9 | [textField setReturnKeyType:UIReturnKeyDone]; 10 | [textField setTextAlignment:NSTextAlignmentRight]; 11 | [textField setAutocorrectionType:UITextAutocorrectionTypeNo]; 12 | [textField setAutocapitalizationType:UITextAutocapitalizationTypeNone]; 13 | if((self = [super initWithStyle:UITableViewCellStyleValue1 14 | reuseIdentifier:reuseIdentifier 15 | detailView:textField])) { 16 | _field = textField; 17 | [[self contentView] addGestureRecognizer:[[UITapGestureRecognizer alloc] 18 | initWithTarget:_field 19 | action:@selector(becomeFirstResponder)]]; 20 | } 21 | 22 | return self; 23 | } 24 | 25 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { 26 | [textField endEditing:YES]; 27 | return NO; 28 | } 29 | 30 | - (void)setDelegate:(id)delegate { 31 | _delegate = delegate; 32 | if([delegate respondsToSelector:@selector(textFieldDidChange:)]) { 33 | [[self field] addTarget:delegate 34 | action:@selector(textFieldDidChange:) 35 | forControlEvents:UIControlEventEditingChanged]; 36 | } 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2020, Lorenzo Pane 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /EvilKit/src/EVKStaticStringPortion.m: -------------------------------------------------------------------------------- 1 | #import "EVKURLPortions.h" 2 | 3 | #define set(...) [NSOrderedSet orderedSetWithObjects:__VA_ARGS__, nil] 4 | 5 | @implementation EVKStaticStringPortion 6 | 7 | - (instancetype)initWithString:(NSString *)str percentEncodingIterations:(int)iterations { 8 | if((self = [super initWithPercentEncodingIterations:iterations])) { 9 | _string = str; 10 | } 11 | 12 | return self; 13 | } 14 | 15 | - (instancetype)init { 16 | return [self initWithString:@"" percentEncodingIterations:0]; 17 | } 18 | 19 | + (instancetype)portionWithString:(NSString *)str percentEncodingIterations:(int)iterations { 20 | return [[EVKStaticStringPortion alloc] initWithString:str percentEncodingIterations:iterations]; 21 | } 22 | 23 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { return [self string]; } 24 | 25 | - (NSString *)stringRepresentation { 26 | return [NSString stringWithFormat:@"Text: %@", [self string]]; 27 | } 28 | 29 | // Coding {{{ 30 | - (NSOrderedSet *)endUserAccessibleKeys { 31 | return set(@"string", @"percentEncodingIterations"); 32 | } 33 | 34 | + (BOOL)supportsSecureCoding { return YES; } 35 | 36 | - (void)encodeWithCoder:(NSCoder *)coder { 37 | [super encodeWithCoder:coder]; 38 | [coder encodeObject:[self string] forKey:@"string"]; 39 | } 40 | 41 | - (instancetype)initWithCoder:(NSCoder *)coder { 42 | return [self initWithString:[coder decodeObjectOfClass:[NSString class] forKey:@"string"] 43 | percentEncodingIterations:[coder decodeIntForKey:@"percentEncodingIterations"]]; 44 | } 45 | 46 | - (instancetype)copyWithZone:(NSZone *)zone { 47 | return [[self class] portionWithString:[self string] 48 | percentEncodingIterations:[[self percentEncodingIterations] intValue]]; 49 | } 50 | // }}} 51 | 52 | @end 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Dedicated to the memory of Lorenzo Pane 2 | === 3 | 4 | EvilScheme 5 | === 6 | 7 | Extensible iOS tweak for modifying URL actions. 8 | 9 | Manual 10 | --- 11 | A full manual for operation can be found [here](https://l.pane.net/evil.html) 12 | 13 | Installation 14 | --- 15 | You can install EvilScheme for free by adding the [Dynastic Repo](http://repo.dynastic.co) to your favorite package manager. It can also be compiled from source using [Theos](https://theos.dev) or [DragonBuild](https://github.com/DragonBuild/DragonBuild). Makefiles for both are provided in this repo. You may need to build EvilKit separately first and add it to your build system's framework include directory. Debs are also avalible directly from Github under the `Releases` tab. 16 | 17 | Contribution 18 | --- 19 | I love pull requests! To contribute, just fork, branch, commit, push, and send me a PR. Please ensure that your code fits with the style of the project and is well documented with comments. If you have and questions, shoot me a DM. If you want to add a preset, modify the `EVSPreferenceManager.m` file with your preset from source. Each item is structured similarly to its counterpart in the settings pane. If you don't quite remember the behavior of each portion, check out the well documented headers [here](https://github.com/LorenzoPane/EvilScheme/blob/master/EvilKit/src/EVKURLPortions.h). 20 | 21 | Tips 22 | --- 23 | I don't accept donations currently, but if you want to help support my projects, check out various charitable causes, software projects, and awesome developers listed [here](https://l.pane.net/causes.html). :) 24 | 25 | License 26 | --- 27 | Evil Scheme is free and open source software licensed under the [BSD 3-Clause 'New' or 'Revised' license](https://opensource.org/licenses/BSD-3-Clause) provided "As is" with absolutely no warranty. 28 | -------------------------------------------------------------------------------- /EvilKit/src/EVKQueryItemLexicon.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | /// @enum URLQueryState 4 | /// @brief Item state if no substitution is found 5 | /// @constant URLQueryStateNull Exclude item 6 | /// @constant URLQueryStatePassThrough Include item and keep value 7 | typedef NS_ENUM(NSInteger, URLQueryState) { 8 | URLQueryStateNull, 9 | URLQueryStatePassThrough, 10 | }; 11 | 12 | /// An object to model translations of query items from one application's context to another's 13 | @interface EVKQueryItemLexicon : NSObject 14 | 15 | /// The name with which to substitute 16 | @property (nonatomic, copy) NSString *param; 17 | 18 | /// Dictionary of substitutions for values 19 | @property (nonatomic, copy) NSDictionary *substitutions; 20 | 21 | /// The default state for values without a substitute 22 | @property URLQueryState defaultState; 23 | 24 | /// Creates a new item lexicon with no value modification with a given name 25 | /// @param key The name with which to substitute 26 | + (instancetype)identityLexiconWithName:(NSString *)key; 27 | 28 | /// Initializes a newly created lexicon with the specified key name, dictionary, and default state 29 | /// @param key The name with which to substitute 30 | /// @param dictionary Dictionary of substitutions for values 31 | /// @param state The default state for values without a substitute 32 | - (instancetype)initWithKeyName:(NSString *)key 33 | dictionary:(NSDictionary *)dictionary 34 | defaultState:(URLQueryState)state; 35 | 36 | /// Returns a translated query item, or nil if translated item should not be included 37 | /// @param item Item to translate 38 | /// @returns Translated item, or nil if item should not be included 39 | - (NSURLQueryItem *)translateItem:(NSURLQueryItem *)item; 40 | 41 | @end 42 | -------------------------------------------------------------------------------- /EvilKit/src/EVKPercentEncodablePortion.m: -------------------------------------------------------------------------------- 1 | #import "EVKURLPortions.h" 2 | 3 | #define set(...) [NSOrderedSet orderedSetWithObjects:__VA_ARGS__, nil] 4 | 5 | @implementation EVKPercentEncodablePortion 6 | 7 | + (instancetype)portionWithPercentEncodingIterations:(int)iterations { 8 | return [[[self class] alloc] initWithPercentEncodingIterations:iterations]; 9 | } 10 | 11 | - (instancetype)initWithPercentEncodingIterations:(int)iterations { 12 | if((self = [super init])) { 13 | _percentEncodingIterations = @(iterations); 14 | } 15 | 16 | return self; 17 | } 18 | 19 | - (instancetype)init { 20 | return [self initWithPercentEncodingIterations:0]; 21 | } 22 | 23 | - (NSString *)stringRepresentation { return @""; } 24 | 25 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { return nil; } 26 | 27 | - (NSString *)evaluateWithURL:(NSURL *)url { 28 | NSString *ret = [self evaluateUnencodedWithURL:url]; 29 | int iters = [[self percentEncodingIterations] intValue]; 30 | while(iters != 0) { 31 | if(iters < 0) { 32 | iters += 1; 33 | ret = percentDecode(ret); 34 | } else { 35 | iters -= 1; 36 | ret = percentEncode(ret); 37 | } 38 | } 39 | return ret ? : @""; 40 | } 41 | 42 | // Coding {{{ 43 | - (NSOrderedSet *)endUserAccessibleKeys { 44 | return set(@"percentEncodingIterations"); 45 | } 46 | 47 | + (BOOL)supportsSecureCoding { return YES; } 48 | 49 | - (void)encodeWithCoder:(NSCoder *)coder { 50 | [coder encodeInt:[[self percentEncodingIterations] intValue] forKey:@"percentEncodingIterations"]; 51 | } 52 | 53 | - (instancetype)initWithCoder:(NSCoder *)coder { 54 | return [self initWithPercentEncodingIterations:[coder decodeIntForKey:@"percentEncodingIterations"]]; 55 | } 56 | 57 | - (instancetype)copyWithZone:(NSZone *)zone { 58 | return [[self class] portionWithPercentEncodingIterations:[[self percentEncodingIterations] intValue]]; 59 | } 60 | // }}} 61 | 62 | @end 63 | -------------------------------------------------------------------------------- /EvilKit/src/NSURL+ComponentAdditions.m: -------------------------------------------------------------------------------- 1 | #import "NSURL+ComponentAdditions.h" 2 | 3 | @implementation NSURL (ComponentAdditions) 4 | 5 | - (NSArray *)queryItems { 6 | return [[NSURLComponents componentsWithURL:self 7 | resolvingAgainstBaseURL:NO] queryItems]; 8 | } 9 | 10 | - (NSString *)queryString { 11 | return [[NSURLComponents componentsWithURL:self 12 | resolvingAgainstBaseURL:NO] percentEncodedQuery]; 13 | } 14 | 15 | - (NSString *)fragmentString { 16 | return [[NSURLComponents componentsWithURL:self 17 | resolvingAgainstBaseURL:NO] percentEncodedFragment]; 18 | } 19 | 20 | - (NSString *)trimmedPathComponent { 21 | NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:@"/"]; 22 | return [[[NSURLComponents componentsWithURL:self 23 | resolvingAgainstBaseURL:NO] path] stringByTrimmingCharactersInSet:set]; 24 | } 25 | 26 | - (NSString *)trimmedResourceSpecifier { 27 | NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:@"/"]; 28 | return [[self resourceSpecifier] stringByTrimmingCharactersInSet:set]; 29 | } 30 | 31 | - (NSString *)hostComponent { 32 | return [[NSURLComponents componentsWithURL:self 33 | resolvingAgainstBaseURL:NO] host]; 34 | } 35 | 36 | - (BOOL)matchesRegularExpression:(NSRegularExpression *)regex { 37 | return [regex numberOfMatchesInString:[self absoluteString] 38 | options:0 39 | range:NSMakeRange(0, [[self absoluteString] length])] ? YES : NO; 40 | } 41 | 42 | - (NSString *)queryValueForParameter:(NSString *)param { 43 | for(NSURLQueryItem *item in [[NSURLComponents componentsWithURL:self 44 | resolvingAgainstBaseURL:NO] queryItems]) { 45 | if([[item name] isEqualToString:param]) return [item value]; 46 | } 47 | 48 | return nil; 49 | } 50 | @end 51 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0PrefVC.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | /// Default tint color 5 | #define TINT_COLOR [UIColor colorWithRed:0.776 green:0.471 blue:0.867 alpha:1] 6 | 7 | /// Default reuse identifiers 8 | #define LINK_CELL_ID @"L0LinkCell" 9 | #define BASIC_CELL_ID @"L0BasicCell" 10 | #define PROSE_CELL_ID @"L0ProseCell" 11 | #define PICKER_CELL_ID @"L0PickerCell" 12 | #define BUTTON_CELL_ID @"L0ButtonCell" 13 | #define TOGGLE_CELL_ID @"L0ToggleCell" 14 | #define STEPPER_CELL_ID @"L0StepperCell" 15 | #define EDIT_TEXT_CELL_ID @"L0EditTextCell" 16 | #define PURE_EDIT_TEXT_CELL_ID @"L0PureEditTextCell" 17 | 18 | @class L0PrefVC; 19 | 20 | @protocol L0PrefVCDelegate 21 | 22 | /// Method to be called when a member of the implementing delegate controller has changed it's model 23 | /// @param controller View controller that has been changed 24 | @required - (void)controllerDidChangeModel:(L0PrefVC *)controller; 25 | 26 | @optional - (BOOL)controller:(L0PrefVC *)controller canMoveFromKey:(NSString *)from toKey:(NSString *)to; 27 | @optional - (void)controller:(L0PrefVC *)controller willMoveFromKey:(NSString *)from toKey:(NSString *)to; 28 | 29 | @end 30 | 31 | /// Abstract class for all view table view controllers 32 | @interface L0PrefVC : PSViewController 33 | 34 | /// Delegate to implement model changes 35 | @property (atomic, weak) id delegate; 36 | 37 | /// Primary view 38 | @property (atomic, strong) UITableView *tableView; 39 | 40 | /// Dictionary of reuse identifiers and corrosponding classes 41 | @property (atomic, strong) NSDictionary *cells; 42 | 43 | /// Convenience method for finding the apps key window 44 | + (UIWindow *)keyWindow; 45 | 46 | /// Method to be overridden if the subclass is the root view controller 47 | - (BOOL)isRootVC; 48 | 49 | - (void)setupTable; 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /EvilKit/src/EVKAppAlternative.m: -------------------------------------------------------------------------------- 1 | #import "EVKAppAlternative.h" 2 | 3 | @implementation EVKAppAlternative 4 | 5 | - (instancetype)initWithTargetBundleID:(NSString *)targetBundleID 6 | substituteBundleID:(NSString *)substituteBundleID 7 | urlOutlines:(NSArray *)outlines { 8 | if((self = [super init])) { 9 | _targetBundleID = targetBundleID; 10 | _substituteBundleID = substituteBundleID; 11 | _urlOutlines = outlines; 12 | } 13 | 14 | return self; 15 | } 16 | 17 | - (instancetype)init { 18 | return [self initWithTargetBundleID:@"" 19 | substituteBundleID:@"" 20 | urlOutlines:@[]]; 21 | } 22 | 23 | - (NSURL *)transformURL:(NSURL *)url { 24 | NSURL *ret; 25 | for(EVKAction *action in [self urlOutlines]) { 26 | if((ret = [action transformURL:url])) { 27 | return ret; 28 | } 29 | } 30 | 31 | return nil; 32 | } 33 | 34 | // Coding {{{ 35 | + (BOOL)supportsSecureCoding { return YES; } 36 | 37 | - (void)encodeWithCoder:(NSCoder *)coder { 38 | [coder encodeObject:[self targetBundleID] forKey:@"targetBundleID"]; 39 | [coder encodeObject:[self substituteBundleID] forKey:@"substituteBundleID"]; 40 | [coder encodeObject:[self urlOutlines] forKey:@"urlOutlines"]; 41 | } 42 | 43 | - (instancetype)initWithCoder:(NSCoder *)coder { 44 | return [self initWithTargetBundleID:[coder decodeObjectOfClass:[NSString class] forKey:@"targetBundleID"] 45 | substituteBundleID:[coder decodeObjectOfClass:[NSString class] forKey:@"substituteBundleID"] 46 | urlOutlines:[coder decodeObjectOfClass:[NSDictionary class] forKey:@"urlOutlines"]]; 47 | } 48 | 49 | - (instancetype)copyWithZone:(NSZone *)zone { 50 | return [[[self class] alloc] initWithTargetBundleID:[self targetBundleID] 51 | substituteBundleID:[self substituteBundleID] 52 | urlOutlines:[self urlOutlines]]; 53 | } 54 | // }}} 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /EvilKit/src/EVKAction.m: -------------------------------------------------------------------------------- 1 | #import "EVKAction.h" 2 | #import "NSURL+ComponentAdditions.h" 3 | 4 | #define regex(pattern) [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil] 5 | 6 | @implementation EVKAction 7 | 8 | + (instancetype)actionWithPattern:(NSString *)pattern 9 | outline:(NSArray *> *)outline { 10 | return [[self alloc] initWithRegexPattern:pattern 11 | URLOutline:outline]; 12 | } 13 | 14 | - (instancetype)initWithRegexPattern:(NSString *)pattern 15 | URLOutline:(NSArray *> *)outline { 16 | if((self = [super init])) { 17 | _regexPattern = pattern; 18 | _outline = outline; 19 | } 20 | 21 | return self; 22 | } 23 | 24 | - (instancetype)init { 25 | return [self initWithRegexPattern:@"" URLOutline:@[]]; 26 | } 27 | 28 | - (NSURL *)transformURL:(NSURL *)url { 29 | if(regex([self regexPattern]) && [url matchesRegularExpression:regex([self regexPattern])]) { 30 | NSMutableString *ret = [NSMutableString new]; 31 | for(NSObject *portion in [self outline]) { 32 | [ret appendString:[portion evaluateWithURL:url]]; 33 | } 34 | return [NSURL URLWithString:ret]; 35 | } 36 | 37 | return nil; 38 | } 39 | 40 | // Coding {{{ 41 | + (BOOL)supportsSecureCoding { return YES; } 42 | 43 | - (void)encodeWithCoder:(NSCoder *)coder { 44 | [coder encodeObject:[self regexPattern] forKey:@"regexPattern"]; 45 | [coder encodeObject:[self outline] forKey:@"outline"]; 46 | } 47 | 48 | - (instancetype)initWithCoder:(NSCoder *)coder { 49 | return [self initWithRegexPattern:[coder decodeObjectForKey:@"regexPattern"] 50 | URLOutline:[coder decodeObjectForKey:@"outline"]]; 51 | } 52 | 53 | - (instancetype)copyWithZone:(NSZone *)zone { 54 | return [[self class] actionWithPattern:[self regexPattern] 55 | outline:[self outline]]; 56 | } 57 | // }}} 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /Prefs/src/EVSAppAlternativeWrapper.m: -------------------------------------------------------------------------------- 1 | #import "EVSAppAlternativeWrapper.h" 2 | 3 | @implementation EVSAppAlternativeWrapper 4 | 5 | - (instancetype)init { 6 | return [self initWithAppAlternative:[EVKAppAlternative new] name:@""]; 7 | } 8 | 9 | - (instancetype)initWithAppAlternative:(EVKAppAlternative *)app 10 | name:(NSString *)name 11 | targetBundleIDs:(NSArray *)targets { 12 | if((self = [super init])) { 13 | _orig = app; 14 | _name = name; 15 | _targetBundleIDs = [(targets ? : @[]) mutableCopy]; 16 | if(![_targetBundleIDs containsObject:[app targetBundleID]]) { 17 | [_targetBundleIDs addObject:[app targetBundleID]]; 18 | } 19 | } 20 | 21 | return self; 22 | } 23 | 24 | - (instancetype)initWithAppAlternative:(EVKAppAlternative *)app name:(NSString *)name { 25 | return [self initWithAppAlternative:app 26 | name:name 27 | targetBundleIDs:@[]]; 28 | } 29 | 30 | - (NSString *)substituteBundleID { 31 | return [[self orig] substituteBundleID]; 32 | } 33 | 34 | - (void)setSubstituteBundleID:(NSString *)bundleID { 35 | [[self orig] setSubstituteBundleID:bundleID]; 36 | } 37 | 38 | - (NSArray *)urlOutlines { 39 | return [[self orig] urlOutlines]; 40 | } 41 | 42 | - (void)setUrlOutlines:(NSArray *)outlines { 43 | [[self orig] setUrlOutlines:outlines]; 44 | } 45 | 46 | // Coding {{{ 47 | 48 | + (BOOL)supportsSecureCoding { return YES; } 49 | 50 | - (void)encodeWithCoder:(NSCoder *)coder { 51 | [coder encodeObject:[self orig] forKey:@"orig"]; 52 | [coder encodeObject:[self name] forKey:@"name"]; 53 | [coder encodeObject:[self targetBundleIDs] forKey:@"targetBundleIDs"]; 54 | } 55 | 56 | - (instancetype)initWithCoder:(NSCoder *)coder { 57 | return [self initWithAppAlternative:[coder decodeObjectForKey:@"orig"] 58 | name:[coder decodeObjectForKey:@"name"] 59 | targetBundleIDs:[coder decodeObjectForKey:@"targetBundleIDs"]]; 60 | } 61 | 62 | // }}} 63 | 64 | @end 65 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0DictionaryController.m: -------------------------------------------------------------------------------- 1 | #import "L0DictionaryController.h" 2 | 3 | @implementation L0DictionaryController 4 | 5 | - (instancetype)initWithDict:(NSDictionary *)dict { 6 | if((self = [super init])) { 7 | _dict = dict; 8 | } 9 | 10 | return self; 11 | } 12 | 13 | - (instancetype)init { 14 | return [self initWithDict:@{}]; 15 | } 16 | 17 | - (NSInteger)count { 18 | return [[self dict] count]; 19 | } 20 | 21 | - (BOOL)containsKey:(NSString *)key { 22 | return (BOOL)[self dict][key]; 23 | } 24 | 25 | - (NSArray *)keys { 26 | return [[[self dict] allKeys] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]; 27 | } 28 | 29 | - (NSArray *)objects { 30 | NSMutableArray *ret = [NSMutableArray new]; 31 | for(NSString *key in [self keys]) [ret addObject:[self dict][key]]; 32 | return ret; 33 | } 34 | 35 | - (NSString *)keyAtIndex:(NSInteger)idx { 36 | return [self keys][idx]; 37 | } 38 | 39 | - (id)objectForKey:(NSString *)key { 40 | return [self dict][key]; 41 | } 42 | 43 | - (id)objectAtIndex:(NSInteger)idx { 44 | return [self dict][[self keys][idx]]; 45 | } 46 | 47 | - (void)removeObjectForKey:(NSString *)key { 48 | NSMutableDictionary *dict = [[self dict] mutableCopy]; 49 | [dict removeObjectForKey:key]; 50 | [self setDict:dict]; 51 | } 52 | 53 | - (void)removeObjectAtIndex:(NSInteger)idx { 54 | [self removeObjectForKey:[self keyAtIndex:idx]]; 55 | } 56 | 57 | - (void)setObject:(id)obj forKey:(NSString *)key { 58 | NSMutableDictionary *dict = [[self dict] mutableCopy]; 59 | dict[key] = obj; 60 | [self setDict:dict]; 61 | } 62 | 63 | - (void)setObject:(id)obj atIndex:(NSInteger)idx { 64 | [self setObject:obj forKey:[self keys][idx]]; 65 | } 66 | 67 | - (void)renameKey:(NSString *)key toString:(NSString *)str { 68 | NSMutableDictionary *dict = [[self dict] mutableCopy]; 69 | id value = dict[key]; 70 | [dict removeObjectForKey:key]; 71 | dict[str] = value; 72 | [self setDict:dict]; 73 | } 74 | 75 | - (void)renameKeyAtIndex:(NSInteger)idx toString:(NSString *)str { 76 | [self renameKey:[self keys][idx] toString:str]; 77 | } 78 | 79 | @end 80 | -------------------------------------------------------------------------------- /Prefs/src/EVSPresetListVC.m: -------------------------------------------------------------------------------- 1 | #import "EVSPresetListVC.h" 2 | #import "EVSPreferenceManager.h" 3 | #import "EVSAppAlternativeVC.h" 4 | 5 | @implementation EVSPresetListVC 6 | 7 | #pragma mark - lifecycle 8 | 9 | - (void)viewDidLoad { 10 | [super viewDidLoad]; 11 | [self setupNav]; 12 | [self setPresets:[EVSPreferenceManager presets]]; 13 | [[self tableView] setSectionFooterHeight:0]; 14 | } 15 | 16 | - (void)setupNav { 17 | [self setTitle:@"Presets"]; 18 | [[self navigationItem] setLeftBarButtonItem:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone 19 | target:self 20 | action:@selector(dismiss)]]; 21 | } 22 | 23 | - (void)dismiss { 24 | [[self navigationController] dismissViewControllerAnimated:YES completion:nil]; 25 | } 26 | 27 | #pragma mark - table 28 | 29 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 30 | return [[self presets] keyAtIndex:section]; 31 | } 32 | 33 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 34 | return [[self presets] count]; 35 | } 36 | 37 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 38 | return [[[self presets] objectAtIndex:section] count]; 39 | } 40 | 41 | #pragma mark - cells 42 | 43 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 44 | L0LinkCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:LINK_CELL_ID forIndexPath:indexPath]; 45 | [[cell textLabel] setText:[[[[self presets] objectAtIndex:[indexPath section]] objectAtIndex:[indexPath row]] name]]; 46 | 47 | return cell; 48 | } 49 | 50 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 51 | [(EVSAppAlternativeVC *)[self delegate] setAppAlternative:[[[self presets] objectAtIndex:[indexPath section]] objectAtIndex:[indexPath row]]]; 52 | [[self delegate] controllerDidChangeModel:nil]; 53 | [super tableView:tableView didSelectRowAtIndexPath:indexPath]; 54 | [self dismiss]; 55 | } 56 | 57 | @end 58 | -------------------------------------------------------------------------------- /EvilKit/src/EVKQueryItemLexicon.m: -------------------------------------------------------------------------------- 1 | #import "EVKQueryItemLexicon.h" 2 | 3 | @implementation EVKQueryItemLexicon 4 | 5 | + (instancetype)identityLexiconWithName:(NSString *)key { 6 | return [[self alloc] initWithKeyName:key 7 | dictionary:@{} 8 | defaultState:URLQueryStatePassThrough]; 9 | } 10 | 11 | - (instancetype)initWithKeyName:(NSString *)key 12 | dictionary:(NSDictionary *)dictionary 13 | defaultState:(URLQueryState)state { 14 | if((self = [super init])) { 15 | _param = key; 16 | _substitutions = dictionary; 17 | _defaultState = state; 18 | } 19 | 20 | return self; 21 | } 22 | 23 | - (instancetype)init { 24 | return [self initWithKeyName:@"" dictionary:@{} defaultState:URLQueryStateNull]; 25 | } 26 | 27 | - (NSURLQueryItem *)translateItem:(NSURLQueryItem *)item { 28 | NSString *value; 29 | 30 | if(!(value = [self substitutions][[item value]])) { 31 | switch([self defaultState]) { 32 | case URLQueryStatePassThrough: 33 | value = [item value]; 34 | break; 35 | default: 36 | return nil; 37 | } 38 | } 39 | 40 | return [NSURLQueryItem queryItemWithName:[self param] value:value]; 41 | } 42 | 43 | // Coding {{{ 44 | + (BOOL)supportsSecureCoding { return YES; } 45 | 46 | - (void)encodeWithCoder:(NSCoder *)coder { 47 | [coder encodeObject:[self param] forKey:@"key"]; 48 | [coder encodeObject:[self substitutions] forKey:@"substitutions"]; 49 | [coder encodeInteger:[self defaultState] forKey:@"defaultState"]; 50 | } 51 | 52 | - (instancetype)initWithCoder:(NSCoder *)coder { 53 | return [self initWithKeyName:[coder decodeObjectOfClass:[NSString class] forKey:@"key"] 54 | dictionary:[coder decodeObjectOfClass:[NSDictionary class] forKey:@"substitutions"] 55 | defaultState:[coder decodeIntegerForKey:@"defaultState"]]; 56 | } 57 | 58 | - (instancetype)copyWithZone:(NSZone *)zone { 59 | return [[[self class] alloc] initWithKeyName:[self param] 60 | dictionary:[self substitutions] 61 | defaultState:[self defaultState]]; 62 | } 63 | // }}} 64 | 65 | @end 66 | -------------------------------------------------------------------------------- /PrivateFrameworks.h: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | @interface FBSOpenApplicationOptions : NSObject 4 | @property (nonatomic,copy) NSDictionary *dictionary; 5 | @end 6 | 7 | @interface BSSettings : NSObject 8 | -(id)objectForSetting:(NSUInteger)index; 9 | -(NSIndexSet *)allSettings; 10 | -(void)_setObject:(id)obj forSetting:(NSUInteger)setting ; 11 | @end 12 | 13 | @interface BSAction : NSObject 14 | @property (nonatomic,copy,readonly) BSSettings *info; 15 | @end 16 | 17 | @interface UAUserActivityInfo : NSObject 18 | @property (copy) NSURL * webpageURL; 19 | @end 20 | 21 | @interface BSProcessHandle 22 | @property NSString *bundleIdentifier; 23 | @end 24 | 25 | @interface LSApplicationWorkspace : NSObject 26 | + (instancetype)defaultWorkspace; 27 | - (BOOL)applicationIsInstalled:(NSString *)bundleID; 28 | @end 29 | 30 | @interface FBSystemService : NSObject 31 | - (void)openApplication:(NSString *)bundleID 32 | withOptions:(FBSOpenApplicationOptions *)options 33 | originator:(BSProcessHandle *)source 34 | requestID:(NSUInteger)req 35 | completion:(id)completion; 36 | - (void)activateApplication:(NSString *)bundleID 37 | requestID:(NSUInteger)req 38 | options:(FBSOpenApplicationOptions *)options 39 | source:(BSProcessHandle *)source 40 | originalSource:(BSProcessHandle *)origSource 41 | withResult:(id)completion; 42 | - (void)_activateApplication:(NSString *)bundleID 43 | requestID:(NSUInteger)req 44 | options:(FBSOpenApplicationOptions *)options 45 | source:(BSProcessHandle *)source 46 | originalSource:(BSProcessHandle *)origSource 47 | withResult:(id)completion; 48 | - (void)_reallyActivateApplication:(NSString *)bundleID 49 | requestID:(NSUInteger)req 50 | options:(FBSOpenApplicationOptions *)options 51 | source:(BSProcessHandle *)source 52 | originalSource:(BSProcessHandle *)origSource 53 | isTrusted:(BOOL)sourceTrusted 54 | sequenceNumber:(unsigned long long)sourceSeq 55 | cacheGUID:(id)sourceGUID 56 | ourSequenceNumber:(unsigned long long)ourSeq 57 | ourCacheGUID:(id)ourGUID 58 | withResult:(id)completion; 59 | @end 60 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0PickerCell.m: -------------------------------------------------------------------------------- 1 | #import "L0PickerCell.h" 2 | 3 | @implementation L0PickerCell 4 | 5 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier { 7 | if((self = [super initWithStyle:style reuseIdentifier:reuseIdentifier])) { 8 | _picker = [[UIPickerView alloc] initWithFrame:CGRectZero]; 9 | 10 | [_picker setDelegate:self]; 11 | [_picker setDataSource:self]; 12 | 13 | UIToolbar *bar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, 100, 40)]; 14 | 15 | [bar setItems:@[ 16 | [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone 17 | target:self 18 | action:@selector(dismissPicker)], 19 | ]]; 20 | 21 | [[self field] setInputView:_picker]; 22 | [[self field] setInputAccessoryView:bar]; 23 | [[self field] setTintColor:[UIColor clearColor]]; 24 | } 25 | 26 | return self; 27 | } 28 | 29 | - (BOOL)textField:(UITextField *)textField 30 | shouldChangeCharactersInRange:(NSRange)range 31 | replacementString:(NSString *)string { 32 | return NO; 33 | } 34 | 35 | - (void)dismissPicker { 36 | [[self field] setText:[self options][[[self picker] selectedRowInComponent:0]]]; 37 | [[self field] endEditing:YES]; 38 | [[self delegate] textFieldDidChange:[self field]]; 39 | } 40 | 41 | - (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView { return 1; } 42 | 43 | - (NSInteger)pickerView:(UIPickerView *)pickerView 44 | numberOfRowsInComponent:(NSInteger)component { 45 | return [[self options] count]; 46 | } 47 | 48 | - (NSString *)pickerView:(UIPickerView *)pickerView 49 | titleForRow:(NSInteger)row 50 | forComponent:(NSInteger)component { 51 | return [self options][row]; 52 | } 53 | 54 | - (void)pickerView:(UIPickerView *)pickerView 55 | didSelectRow:(NSInteger)row 56 | inComponent:(NSInteger)component { 57 | [[self field] setText:[self options][row]]; 58 | [[self delegate] textFieldDidChange:[self field]]; 59 | } 60 | 61 | - (void)selectIndex:(NSUInteger)idx { 62 | [[self picker] selectRow:idx inComponent:0 animated:YES]; 63 | } 64 | 65 | - (void)selectObject:(NSString *)obj { 66 | if([[self options] containsObject:obj]) { 67 | [self selectIndex:[[self options] indexOfObject:obj]]; 68 | } 69 | } 70 | 71 | @end 72 | -------------------------------------------------------------------------------- /EvilKit/src/EVKTranslatedQueryPortion.m: -------------------------------------------------------------------------------- 1 | #import "EVKURLPortions.h" 2 | #import "NSURL+ComponentAdditions.h" 3 | 4 | #define set(...) [NSOrderedSet orderedSetWithObjects:__VA_ARGS__, nil] 5 | 6 | @implementation EVKTranslatedQueryPortion 7 | 8 | - (instancetype)initWithDictionary:(NSDictionary *)dict 9 | percentEncodingIterations:(int)iterations { 10 | if((self = [super initWithPercentEncodingIterations:iterations])) { 11 | _paramTranslations = dict; 12 | } 13 | 14 | return self; 15 | } 16 | 17 | - (instancetype)init { 18 | return [self initWithDictionary:@{} percentEncodingIterations:NO]; 19 | } 20 | 21 | + (instancetype)portionWithDictionary:(NSDictionary *)dict 22 | percentEncodingIterations:(int)iterations { 23 | return [[[self class] alloc] initWithDictionary:dict percentEncodingIterations:iterations]; 24 | } 25 | 26 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { 27 | NSMutableArray *items = [NSMutableArray new]; 28 | EVKQueryItemLexicon *t; 29 | NSURLQueryItem *translatedItem; 30 | 31 | for(NSURLQueryItem *item in [url queryItems]) { 32 | if((t = _paramTranslations[[item name]]) && 33 | (translatedItem = [t translateItem:item])) { 34 | [items addObject:translatedItem]; 35 | } 36 | } 37 | 38 | NSURLComponents *c = [[NSURLComponents alloc] init]; 39 | [c setQueryItems:items]; 40 | 41 | return [c percentEncodedQuery]; 42 | } 43 | 44 | - (NSString *)stringRepresentation { return @"Translated query"; } 45 | 46 | // Coding {{{ 47 | - (NSOrderedSet *)endUserAccessibleKeys { 48 | return set(@"paramTranslations", @"percentEncodingIterations"); 49 | } 50 | 51 | + (BOOL)supportsSecureCoding { return YES; } 52 | 53 | - (void)encodeWithCoder:(NSCoder *)coder { 54 | [super encodeWithCoder:coder]; 55 | [coder encodeObject:_paramTranslations forKey:@"paramTranslations"]; 56 | } 57 | 58 | - (instancetype)initWithCoder:(NSCoder *)coder { 59 | return [self initWithDictionary:[coder decodeObjectOfClass:[NSDictionary class] 60 | forKey:@"paramTranslations"] 61 | percentEncodingIterations:[coder decodeIntForKey:@"percentEncodingIterations"]]; 62 | } 63 | 64 | - (instancetype)copyWithZone:(NSZone *)zone { 65 | return [[self class] portionWithDictionary:[self paramTranslations] 66 | percentEncodingIterations:[[self percentEncodingIterations] intValue]]; 67 | } 68 | // }}} 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0DataCell.m: -------------------------------------------------------------------------------- 1 | #import "L0Prefs.h" 2 | 3 | @implementation L0DataCell 4 | 5 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier 7 | detailView:(UIView *)detailView { 8 | if((self = [super initWithStyle:UITableViewCellStyleValue1 9 | reuseIdentifier:reuseIdentifier])) { 10 | [[self detailTextLabel] setHidden:YES]; 11 | 12 | _detailView = detailView; 13 | 14 | [_detailView setTranslatesAutoresizingMaskIntoConstraints:NO]; 15 | [[self contentView] addSubview:[self detailView]]; 16 | [[self contentView] addConstraint:[NSLayoutConstraint constraintWithItem:_detailView 17 | attribute:NSLayoutAttributeCenterY 18 | relatedBy:NSLayoutRelationEqual 19 | toItem:[self contentView] 20 | attribute:NSLayoutAttributeCenterY 21 | multiplier:1 22 | constant:0]]; 23 | [[self contentView] addConstraint:[NSLayoutConstraint constraintWithItem:_detailView 24 | attribute:NSLayoutAttributeTrailing 25 | relatedBy:NSLayoutRelationEqual 26 | toItem:[self contentView] 27 | attribute:NSLayoutAttributeTrailingMargin 28 | multiplier:1 29 | constant:0]]; 30 | [[self contentView] addConstraint:[NSLayoutConstraint constraintWithItem:_detailView 31 | attribute:NSLayoutAttributeLeading 32 | relatedBy:NSLayoutRelationGreaterThanOrEqual 33 | toItem:[self textLabel] 34 | attribute:NSLayoutAttributeTrailing 35 | multiplier:1 36 | constant:16]]; 37 | 38 | [self setSelectionStyle:UITableViewCellSelectionStyleNone]; 39 | } 40 | 41 | return self; 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /EvilKit/src/EVKRegexSubstitutionPortion.m: -------------------------------------------------------------------------------- 1 | #import "EVKURLPortions.h" 2 | 3 | #define set(...) [NSOrderedSet orderedSetWithObjects:__VA_ARGS__, nil] 4 | 5 | @implementation EVKRegexSubstitutionPortion 6 | 7 | - (instancetype)initWithRegex:(NSString *)regex 8 | template:(NSString *)templet 9 | percentEncodingIterations:(int)iterations { 10 | if((self = [super initWithPercentEncodingIterations:iterations])) { 11 | _regex = regex; 12 | _templet = templet; 13 | } 14 | 15 | return self; 16 | } 17 | 18 | + (instancetype)portionWithRegex:(NSString *)regex 19 | template:(NSString *)templet 20 | percentEncodingIterations:(int)iterations { 21 | return [[[self class] alloc] initWithRegex:regex 22 | template:templet 23 | percentEncodingIterations:iterations]; 24 | } 25 | 26 | - (instancetype)init { 27 | return [self initWithRegex:@"" template:@""percentEncodingIterations:0]; 28 | } 29 | 30 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { 31 | NSMatchingOptions opts = NSMatchingWithTransparentBounds | NSMatchingWithoutAnchoringBounds; 32 | NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:_regex 33 | options:0 34 | error:nil]; 35 | return [regex stringByReplacingMatchesInString:[url absoluteString] 36 | options:opts 37 | range:NSMakeRange(0, [[url absoluteString] length]) 38 | withTemplate:[self templet]]; 39 | } 40 | 41 | - (NSString *)stringRepresentation { return @"Regex substitution"; } 42 | 43 | // Coding {{{ 44 | - (NSOrderedSet *)endUserAccessibleKeys { 45 | return set(@"regex", @"templet", @"percentEncodingIterations"); 46 | } 47 | 48 | + (BOOL)supportsSecureCoding { return YES; } 49 | 50 | - (void)encodeWithCoder:(NSCoder *)coder { 51 | [super encodeWithCoder:coder]; 52 | [coder encodeObject:_regex forKey:@"regex"]; 53 | [coder encodeObject:_templet forKey:@"templet"]; 54 | } 55 | 56 | - (instancetype)initWithCoder:(NSCoder *)coder { 57 | return [self initWithRegex:[coder decodeObjectOfClass:[NSRegularExpression class] forKey:@"regex"] 58 | template:[coder decodeObjectOfClass:[NSString class] forKey:@"templet"] 59 | percentEncodingIterations:[coder decodeIntForKey:@"percentEncodingIterations"] ]; 60 | } 61 | 62 | - (instancetype)copyWithZone:(NSZone *)zone { 63 | return [[self class] portionWithRegex:[self regex] 64 | template:[self templet] 65 | percentEncodingIterations:[[self percentEncodingIterations] intValue]]; 66 | } 67 | // }}} 68 | 69 | @end 70 | -------------------------------------------------------------------------------- /Prefs/src/EVSKeyValuePairVC.m: -------------------------------------------------------------------------------- 1 | #import "EVSKeyValuePairVC.h" 2 | 3 | @implementation EVSKeyValuePairVC 4 | 5 | #pragma mark - lifecycle 6 | 7 | - (instancetype)initWithKey:(NSString *)key value:(NSString *)value { 8 | if((self = [super init])) { 9 | _key = key; 10 | _value = value; 11 | } 12 | 13 | return self; 14 | } 15 | 16 | - (void)viewDidLoad { 17 | [super viewDidLoad]; 18 | [self setupNav]; 19 | [self setTitle:[self key]]; 20 | } 21 | 22 | - (void)setupNav { 23 | [[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone 24 | target:self 25 | action:@selector(dismiss)]]; 26 | } 27 | 28 | - (void)dismiss { 29 | [[self navigationController] dismissViewControllerAnimated:YES completion:nil]; 30 | } 31 | 32 | #pragma mark - table 33 | 34 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 35 | return 1; 36 | } 37 | 38 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 39 | return 2; 40 | } 41 | 42 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 43 | return @"Translate argument"; 44 | } 45 | 46 | #pragma mark - cells 47 | 48 | NS_ENUM(NSUInteger, KeyValuePairCells) { 49 | KeyTag, 50 | ValueTag, 51 | }; 52 | 53 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 54 | L0EditTextCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:EDIT_TEXT_CELL_ID forIndexPath:indexPath]; 55 | [[cell field] setTag:[indexPath row]]; 56 | [cell setDelegate:self]; 57 | switch([indexPath row]) { 58 | case KeyTag: { 59 | [[cell textLabel] setText:@"To"]; 60 | [[cell field] setText:[self value]]; 61 | } 62 | case ValueTag: { 63 | [[cell textLabel] setText:@"From"]; 64 | [[cell field] setText:[self key]]; 65 | } 66 | } 67 | 68 | return cell; 69 | } 70 | 71 | #pragma mark - model 72 | 73 | - (void)textFieldDidChange:(UITextField *)field { 74 | NSString *txt = [field text]; 75 | switch([field tag]) { 76 | case KeyTag: { 77 | if([[self delegate] controller:self canMoveFromKey:[self key] toKey:txt]) { 78 | [[self delegate] controller:self willMoveFromKey:[self key] toKey:txt]; 79 | [self setKey:[field text]]; 80 | [self setTitle:[field text]]; 81 | } 82 | break; 83 | } 84 | case ValueTag: { 85 | [self setValue:txt]; 86 | [[self delegate] controllerDidChangeModel:self]; 87 | break; 88 | } 89 | } 90 | } 91 | 92 | @end 93 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0PureEditTextCell.m: -------------------------------------------------------------------------------- 1 | #import "L0PureEditTextCell.h" 2 | 3 | @implementation L0PureEditTextCell 4 | 5 | - (instancetype)initWithStyle:(UITableViewCellStyle)style 6 | reuseIdentifier:(NSString *)reuseIdentifier { 7 | UITextField *textField = [UITextField new]; 8 | [textField setDelegate:self]; 9 | [textField setReturnKeyType:UIReturnKeyDone]; 10 | [textField setTextAlignment:NSTextAlignmentLeft]; 11 | [textField setAutocorrectionType:UITextAutocorrectionTypeNo]; 12 | [textField setAutocapitalizationType:UITextAutocapitalizationTypeNone]; 13 | [textField setTranslatesAutoresizingMaskIntoConstraints:NO]; 14 | if((self = [super initWithStyle:UITableViewCellStyleDefault 15 | reuseIdentifier:reuseIdentifier])) { 16 | _field = textField; 17 | [[self contentView] addSubview:_field]; 18 | [[self contentView] addConstraint:[NSLayoutConstraint constraintWithItem:_field 19 | attribute:NSLayoutAttributeCenterY 20 | relatedBy:NSLayoutRelationEqual 21 | toItem:[self contentView] 22 | attribute:NSLayoutAttributeCenterY 23 | multiplier:1 24 | constant:0]]; 25 | [[self contentView] addConstraint:[NSLayoutConstraint constraintWithItem:_field 26 | attribute:NSLayoutAttributeLeading 27 | relatedBy:NSLayoutRelationEqual 28 | toItem:[self contentView] 29 | attribute:NSLayoutAttributeLeading 30 | multiplier:1 31 | constant:16]]; 32 | 33 | 34 | [[self contentView] addGestureRecognizer:[[UITapGestureRecognizer alloc] 35 | initWithTarget:_field 36 | action:@selector(becomeFirstResponder)]]; 37 | } 38 | 39 | return self; 40 | } 41 | 42 | - (BOOL)textFieldShouldReturn:(UITextField *)textField { 43 | [textField endEditing:YES]; 44 | return NO; 45 | } 46 | 47 | - (void)setDelegate:(id)delegate { 48 | _delegate = delegate; 49 | if([delegate respondsToSelector:@selector(textFieldDidChange:)]) { 50 | [[self field] addTarget:delegate 51 | action:@selector(textFieldDidChange:) 52 | forControlEvents:UIControlEventEditingChanged]; 53 | } 54 | } 55 | 56 | @end 57 | -------------------------------------------------------------------------------- /Prefs/L0Prefs/L0PrefVC.m: -------------------------------------------------------------------------------- 1 | #import "L0Prefs.h" 2 | 3 | @implementation L0PrefVC 4 | 5 | - (void)viewDidLoad { 6 | [super viewDidLoad]; 7 | [self setupTable]; 8 | } 9 | 10 | + (UIWindow *)keyWindow { 11 | NSArray *windows = [[UIApplication sharedApplication] windows]; 12 | UIWindow *ret = nil; 13 | for (UIWindow *window in windows) { 14 | if (window.isKeyWindow) { 15 | ret = window; 16 | break; 17 | } 18 | } 19 | return ret; 20 | } 21 | 22 | - (void)setupTable { 23 | [self setCells:@{ 24 | LINK_CELL_ID : [L0LinkCell class], 25 | BASIC_CELL_ID : [L0DataCell class], 26 | PICKER_CELL_ID : [L0PickerCell class], 27 | PROSE_CELL_ID : [L0ProseCell class], 28 | BUTTON_CELL_ID : [L0ButtonCell class], 29 | TOGGLE_CELL_ID : [L0ToggleCell class], 30 | STEPPER_CELL_ID : [L0StepperCell class], 31 | EDIT_TEXT_CELL_ID : [L0EditTextCell class], 32 | PURE_EDIT_TEXT_CELL_ID : [L0PureEditTextCell class], 33 | }]; 34 | 35 | [self setTableView:[[UITableView alloc] initWithFrame:CGRectZero 36 | style:UITableViewStyleGrouped]]; 37 | 38 | [[self tableView] setDataSource:self]; 39 | [[self tableView] setDelegate:self]; 40 | [[self tableView] setRowHeight:44]; 41 | [[self tableView] setAllowsMultipleSelectionDuringEditing:NO]; 42 | 43 | for(NSString *reuseID in [self cells]) { 44 | [[self tableView] registerClass:[self cells][reuseID] 45 | forCellReuseIdentifier:reuseID]; 46 | } 47 | 48 | [self setView:[self tableView]]; 49 | } 50 | 51 | - (void)viewWillAppear:(BOOL)animated { 52 | [super viewWillAppear:animated]; 53 | UIWindow *window = [L0PrefVC keyWindow]; 54 | if (!window) { 55 | window = [[[UIApplication sharedApplication] windows] firstObject]; 56 | } 57 | if ([window respondsToSelector:@selector(setTintColor:)]) { 58 | [window setTintColor:TINT_COLOR]; 59 | } 60 | } 61 | 62 | - (void)viewWillDisappear:(BOOL)animated { 63 | [super viewWillDisappear:animated]; 64 | if([self isRootVC]) { 65 | UIWindow *window = [L0PrefVC keyWindow]; 66 | if (!window) { 67 | window = [[[UIApplication sharedApplication] windows] firstObject]; 68 | } 69 | if ([window respondsToSelector:@selector(setTintColor:)]) { 70 | [window setTintColor:nil]; 71 | } 72 | } 73 | } 74 | 75 | - (void)controllerDidChangeModel:(L0PrefVC *)controller { 76 | [[self tableView] reloadData]; 77 | [[self delegate] controllerDidChangeModel:self]; 78 | } 79 | 80 | - (NSInteger)tableView:(UITableView *)tableView 81 | numberOfRowsInSection:(NSInteger)section { 82 | return 1; 83 | } 84 | 85 | - (UITableViewCell *)tableView:(UITableView *)tableView 86 | cellForRowAtIndexPath:(NSIndexPath *)indexPath { 87 | return nil; 88 | } 89 | 90 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 91 | return 1; 92 | } 93 | 94 | - (BOOL)isRootVC { return NO; } 95 | 96 | - (void)tableView:(UITableView *)tableView 97 | didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 98 | [[self tableView] deselectRowAtIndexPath:indexPath animated:YES]; 99 | } 100 | 101 | @end 102 | -------------------------------------------------------------------------------- /EvilKit/src/EVKKeyValuePathPortion.m: -------------------------------------------------------------------------------- 1 | #import "EVKURLPortions.h" 2 | 3 | #define set(...) [NSOrderedSet orderedSetWithObjects:__VA_ARGS__, nil] 4 | 5 | @implementation EVKKeyValuePathPortion 6 | 7 | + (instancetype)portionWithRegex:(NSString *)regex 8 | template:(NSString *)templet 9 | path:(NSString *)path 10 | percentEncodingIterations:(int)iterations { 11 | return [[self alloc] initWithRegex:regex 12 | template:templet 13 | path:path 14 | percentEncodingIterations:iterations]; 15 | } 16 | 17 | - (instancetype)initWithRegex:(NSString *)regex 18 | template:(NSString *)templet 19 | path:(NSString *)path 20 | percentEncodingIterations:(int)iterations { 21 | if((self = [super initWithRegex:regex template:templet percentEncodingIterations:iterations])) { 22 | _path = path; 23 | } 24 | 25 | return self; 26 | } 27 | 28 | - (instancetype)init { 29 | return [self initWithRegex:@"" template:@"" path:@"" percentEncodingIterations:0]; 30 | } 31 | 32 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { 33 | // I want to vomit 34 | @try { 35 | // Data from template 36 | NSData *data = [[NSData alloc] initWithBase64EncodedString:[super evaluateUnencodedWithURL:url] 37 | options:0]; 38 | // Resulting object 39 | id obj = [[NSPropertyListSerialization propertyListWithData:data 40 | options:0 41 | format:nil 42 | error:nil] valueForKeyPath:[self path]]; 43 | 44 | if([obj respondsToSelector:@selector(countByEnumeratingWithState:objects:count:)]) { 45 | NSMutableString *ret = [NSMutableString new]; 46 | for(NSObject *item in obj) { 47 | [ret appendString:[item description]]; 48 | } 49 | return ret; 50 | } 51 | else { 52 | return [obj description]; 53 | } 54 | } @catch (NSException *ex) { 55 | return @""; 56 | } 57 | 58 | return @""; 59 | } 60 | 61 | - (NSString *)stringRepresentation { return @"Key-value path"; } 62 | 63 | // Coding {{{ 64 | - (NSOrderedSet *)endUserAccessibleKeys { 65 | return set(@"regex", @"templet", @"path", @"percentEncodingIterations"); 66 | } 67 | 68 | + (BOOL)supportsSecureCoding { return YES; } 69 | 70 | - (void)encodeWithCoder:(NSCoder *)coder { 71 | [super encodeWithCoder:coder]; 72 | [coder encodeObject:[self regex] forKey:@"regex"]; 73 | [coder encodeObject:[self templet] forKey:@"templet"]; 74 | [coder encodeObject:[self path] forKey:@"path"]; 75 | } 76 | 77 | - (instancetype)initWithCoder:(NSCoder *)coder { 78 | return [self initWithRegex:[coder decodeObjectForKey:@"regex"] 79 | template:[coder decodeObjectForKey:@"templet"] 80 | path:[coder decodeObjectForKey:@"path"] 81 | percentEncodingIterations:[coder decodeIntForKey:@"percentEncodingIterations"]]; 82 | } 83 | 84 | - (instancetype)copyWithZone:(NSZone *)zone { 85 | return [[self class] portionWithRegex:[self regex] 86 | template:[self templet] 87 | path:[self path] 88 | percentEncodingIterations:[[self percentEncodingIterations] intValue]]; 89 | } 90 | // }}} 91 | 92 | @end 93 | -------------------------------------------------------------------------------- /Prefs/src/EVSPortionVM.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import "../L0Prefs/L0Prefs.h" 3 | #import "EVSPortionVM.h" 4 | 5 | @implementation EVSPortionVM 6 | 7 | + (NSDictionary *)propertyNameMappings { 8 | return @{ 9 | @"paramTranslations" : @"Query Translator", 10 | @"parameter" : @"Parameter Name", 11 | @"percentEncodingIterations" : @"Percent Encoding", 12 | @"regex" : @"Regular Expression", 13 | @"string" : @"Text", 14 | @"templet" : @"Template", 15 | @"path" : @"Key-value path" 16 | }; 17 | } 18 | 19 | + (NSDictionary *)classNameMappings { 20 | return @{ 21 | @"Address unwrapper" : [EVKMapItemUnwrapperPortion class], 22 | @"Constant Text" : [EVKStaticStringPortion class], 23 | @"Domain" : [EVKHostPortion class], 24 | @"Full URL" : [EVKFullURLPortion class], 25 | @"Key-value path" : [EVKKeyValuePathPortion class], 26 | @"Path" : [EVKTrimmedPathPortion class], 27 | @"Query Parameter" : [EVKQueryParameterValuePortion class], 28 | @"Query" : [EVKQueryPortion class], 29 | @"Regex Substitution" : [EVKRegexSubstitutionPortion class], 30 | @"Resource Specifier" : [EVKTrimmedResourceSpecifierPortion class], 31 | @"Scheme" : [EVKSchemePortion class], 32 | @"Translated Query" : [EVKTranslatedQueryPortion class], 33 | }; 34 | } 35 | 36 | - (instancetype)initWithPortion:(NSObject *)portion { 37 | if((self = [super init])) { 38 | _portion = portion; 39 | } 40 | 41 | return self; 42 | } 43 | 44 | - (NSString *)stringRepresentation { 45 | return [[self portion] stringRepresentation]; 46 | } 47 | 48 | - (int)propertyCount { 49 | return (int)[[[self portion] endUserAccessibleKeys] count]; 50 | } 51 | 52 | - (id)objectForPropertyIndex:(NSInteger)index { 53 | return [[self portion] valueForKey:[[self portion] endUserAccessibleKeys][index]]; 54 | } 55 | 56 | - (void)setObject:(NSObject *)obj forPropertyIndex:(NSInteger)index { 57 | return [[self portion] setValue:obj forKey:[[self portion] endUserAccessibleKeys][index]]; 58 | } 59 | 60 | - (NSString *)propertyKeyForIndex:(NSInteger)index { 61 | return [[self portion] endUserAccessibleKeys][index]; 62 | } 63 | 64 | - (NSString *)propertyNameForIndex:(NSInteger)index { 65 | return [[self class] propertyNameMappings][[self propertyKeyForIndex:index]]; 66 | } 67 | 68 | - (NSString *)valueStringForKey:(NSString *)key { 69 | NSObject *obj = [[self portion] valueForKey:key]; 70 | NSString *str = [NSString stringWithFormat:@"%@", obj]; 71 | if([obj isKindOfClass:objc_getClass("__NSCFBoolean")]) 72 | str = [(NSNumber *)obj boolValue] ? @"True" : @"False"; 73 | else if([obj isKindOfClass:[NSNumber class]]) 74 | str = [NSString stringWithFormat:@"%d", [(NSNumber *)obj intValue]]; 75 | else if([obj isKindOfClass:[NSDictionary class]]) 76 | str = @"Query Tranlator"; 77 | 78 | return str; 79 | } 80 | 81 | - (NSString *)valueStringForIndex:(NSInteger)index { 82 | return [self valueStringForKey:[self propertyKeyForIndex:index]]; 83 | } 84 | 85 | - (Class)cellTypeForIndex:(NSInteger)index { 86 | return [self cellTypeForKey:[self propertyKeyForIndex:index]]; 87 | } 88 | 89 | - (Class)cellTypeForKey:(NSString *)key { 90 | NSObject *obj = [[self portion] valueForKey:key]; 91 | if([obj isKindOfClass:objc_getClass("__NSCFBoolean")]) 92 | return [L0ToggleCell class]; 93 | else if([obj isKindOfClass:[NSNumber class]]) 94 | return [L0StepperCell class]; 95 | else if([obj isKindOfClass:[NSDictionary class]]) 96 | return [L0LinkCell class]; 97 | else if([obj isKindOfClass:[NSString class]]) 98 | return [L0EditTextCell class]; 99 | return [L0EditTextCell class]; 100 | } 101 | 102 | @end 103 | -------------------------------------------------------------------------------- /Prefs/src/EVSQueryTranslatorVC.m: -------------------------------------------------------------------------------- 1 | #import "../L0Prefs/L0Prefs.h" 2 | #import "EVSQueryTranslatorVC.h" 3 | #import "EVSQueryLexiconVC.h" 4 | 5 | @implementation EVSQueryTranslatorVC 6 | 7 | #pragma mark - lifecycle 8 | 9 | - (instancetype)initWithDictionary:(NSDictionary *)dict { 10 | if((self = [super init])) { 11 | _dict = [[L0DictionaryController alloc] initWithDict:dict]; 12 | } 13 | 14 | return self; 15 | } 16 | 17 | - (void)viewDidLoad { 18 | [super viewDidLoad]; 19 | [self setupNav]; 20 | } 21 | 22 | - (void)setupNav { 23 | [self setTitle:@"Configure query translator"]; 24 | [[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemEdit target:self action:@selector(toggleEditing)]]; 25 | } 26 | 27 | #pragma mark - table 28 | 29 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 30 | return [[self dict] count] + 1; 31 | } 32 | 33 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 34 | return 1; 35 | } 36 | 37 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 38 | return @"Query fields"; 39 | } 40 | 41 | #pragma mark - cells 42 | 43 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 44 | if([indexPath row] < [[self dict] count]) { 45 | L0LinkCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:LINK_CELL_ID forIndexPath:indexPath]; 46 | [[cell textLabel] setText:[[self dict] keyAtIndex:[indexPath row]]]; 47 | return cell; 48 | } else { 49 | L0ButtonCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:BUTTON_CELL_ID forIndexPath:indexPath]; 50 | [[cell textLabel] setText:@"Add new"]; 51 | return cell; 52 | } 53 | } 54 | 55 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 56 | EVSQueryLexiconVC *ctrl; 57 | if([indexPath row] < [[self dict] count]) { 58 | ctrl = [[EVSQueryLexiconVC alloc] initWithKey:[[self dict] keyAtIndex:[indexPath row]] 59 | lexicon:[[EVSQueryLexiconWrapper alloc] initWithLexicon:[[self dict] objectAtIndex:[indexPath row]]]]; 60 | 61 | } else { 62 | ctrl = [[EVSQueryLexiconVC alloc] initWithKey:@"" lexicon:[[EVSQueryLexiconWrapper alloc] initWithLexicon:[EVKQueryItemLexicon new]]]; 63 | } 64 | 65 | [ctrl setDelegate:self]; 66 | [[self navigationController] pushViewController:ctrl animated:YES]; 67 | [super tableView:tableView didSelectRowAtIndexPath:indexPath]; 68 | } 69 | 70 | #pragma mark - editing 71 | 72 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 73 | if(editingStyle == UITableViewCellEditingStyleDelete) { 74 | [[self dict] removeObjectAtIndex:[indexPath row]]; 75 | [[self tableView] deleteRowsAtIndexPaths:@[indexPath] 76 | withRowAnimation:UITableViewRowAnimationMiddle]; 77 | [[self delegate] controllerDidChangeModel:self]; 78 | } 79 | } 80 | 81 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 82 | return [indexPath row] < [[self dict] count]; 83 | } 84 | 85 | - (void)toggleEditing { 86 | [[self tableView] setEditing:![[self tableView] isEditing] animated:YES]; 87 | [[[self navigationItem] rightBarButtonItem] setTitle:([[self tableView] isEditing] ? @"Done" : @"Edit")]; 88 | } 89 | 90 | #pragma mark - model 91 | 92 | - (BOOL)controller:(L0PrefVC *)controller canMoveFromKey:(NSString *)from toKey:(NSString *)to { 93 | return !([to isEqualToString:@""] || [[self dict] containsKey:to]); 94 | } 95 | 96 | - (void)controller:(L0PrefVC *)controller willMoveFromKey:(NSString *)from toKey:(NSString *)to { 97 | [[self dict] renameKey:from toString:to]; 98 | [super controllerDidChangeModel:controller]; 99 | } 100 | 101 | - (void)controllerDidChangeModel:(EVSQueryLexiconVC *)controller { 102 | [[self dict] setObject:[[controller lex] lex] forKey:[controller key]]; 103 | [super controllerDidChangeModel:controller]; 104 | } 105 | 106 | @end 107 | -------------------------------------------------------------------------------- /Prefs/src/EVSLogVC.m: -------------------------------------------------------------------------------- 1 | #import "EVSLogVC.h" 2 | #import "EVSPreferenceManager.h" 3 | 4 | @implementation EVSLogVC { 5 | NSMutableArray *log; 6 | } 7 | 8 | #pragma mark - lifecycle 9 | 10 | - (void)viewDidLoad { 11 | [super viewDidLoad]; 12 | [self setupNav]; 13 | [self refresh]; 14 | } 15 | 16 | - (void)setupNav { 17 | [self setTitle:@"Log"]; 18 | [[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:@"Refresh" 19 | style:UIBarButtonItemStylePlain 20 | target:self 21 | action:@selector(refresh)]]; 22 | } 23 | 24 | #pragma mark - table 25 | 26 | NS_ENUM(NSInteger, LogSections) { 27 | TopSection, 28 | LogSection, 29 | }; 30 | 31 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 32 | return 2; 33 | } 34 | 35 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 36 | switch(section) { 37 | case TopSection: 38 | return 2; 39 | case LogSection: 40 | return [log count]; 41 | default: 42 | return 0; 43 | } 44 | } 45 | 46 | #pragma mark - cells 47 | 48 | NS_ENUM(NSInteger, TopCells) { 49 | ToggleCell, 50 | ResetCell, 51 | }; 52 | 53 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 54 | return UITableViewAutomaticDimension; 55 | } 56 | 57 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 58 | switch([indexPath section]) { 59 | case TopSection: { 60 | switch([indexPath row]) { 61 | case ToggleCell: { 62 | L0ToggleCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:TOGGLE_CELL_ID]; 63 | [[cell textLabel] setText:@"Enable Logging"]; 64 | [[cell toggle] setOn:[EVSPreferenceManager isLogging]]; 65 | [cell setDelegate:self]; 66 | return cell; 67 | } 68 | case ResetCell: { 69 | L0ButtonCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:BUTTON_CELL_ID]; 70 | [[cell textLabel] setText:@"Clear log"]; 71 | return cell; 72 | } 73 | } 74 | } 75 | case LogSection: { 76 | L0ProseCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:PROSE_CELL_ID]; 77 | [[cell textLabel] setText:log[[indexPath row]]]; 78 | return cell; 79 | } 80 | default: 81 | return nil; 82 | } 83 | } 84 | 85 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 86 | if([indexPath isEqual:[NSIndexPath indexPathForRow:ResetCell inSection:TopSection]]) { 87 | NSMutableDictionary *dict = [[EVSPreferenceManager logDict] mutableCopy]; 88 | dict[@"data"] = @[]; 89 | [EVSPreferenceManager setLogDict:dict]; 90 | [self refresh]; 91 | } 92 | 93 | [super tableView:tableView didSelectRowAtIndexPath:indexPath]; 94 | } 95 | 96 | 97 | #pragma mark - editing 98 | 99 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 100 | return [indexPath section] == LogSection; 101 | } 102 | 103 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 104 | if(editingStyle == UITableViewCellEditingStyleDelete && [indexPath section] == LogSection) { 105 | NSMutableDictionary *dict = [[EVSPreferenceManager logDict] mutableCopy]; 106 | 107 | [log removeObjectAtIndex:[indexPath row]]; 108 | [dict setObject:log forKey:@"data"]; 109 | [EVSPreferenceManager setLogDict:dict]; 110 | 111 | [[self tableView] deleteRowsAtIndexPaths:@[indexPath] 112 | withRowAnimation:UITableViewRowAnimationMiddle]; 113 | } 114 | } 115 | 116 | #pragma mark - model 117 | 118 | - (void)switchValueDidChange:(UISwitch *)toggle { 119 | [EVSPreferenceManager setLogging:[toggle isOn]]; 120 | } 121 | 122 | - (void)refresh { 123 | log = [[[[EVSPreferenceManager logDict][@"data"] reverseObjectEnumerator] allObjects] mutableCopy]; 124 | [[self tableView] reloadSections:[NSIndexSet indexSetWithIndex:LogSection] withRowAnimation:UITableViewRowAnimationAutomatic]; 125 | } 126 | 127 | @end 128 | 129 | -------------------------------------------------------------------------------- /EvilKit/src/EVKURLPortions.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import "EVKQueryItemLexicon.h" 3 | 4 | /// Percent encode a given string 5 | /// @param str NSString to encode 6 | #define percentEncode(str) [str stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet characterSetWithCharactersInString:@""]] 7 | 8 | /// Percent decode a given string 9 | /// @param str NSString to decode 10 | #define percentDecode(str) [str stringByRemovingPercentEncoding] 11 | 12 | /// Protocol representing functions of URLs that return strings 13 | @protocol EVKURLPortion 14 | /// Returns the evaluated portion given the original URL or an empty string 15 | /// @param url Original URL 16 | - (NSString *)evaluateWithURL:(NSURL *)url; 17 | /// Returns a human-readable string representation of the un-evaluated portion 18 | - (NSString *)stringRepresentation; 19 | /// Returns an array of string to be used to access KVC-compliant properties 20 | - (NSOrderedSet *)endUserAccessibleKeys; 21 | @end 22 | 23 | /// Protocol to represent URL portions with an arbitrary number of percent encoding iterations 24 | @protocol EVKPercentEncodable 25 | /// Method to be overridden with the intended unencoded output of evaluateWithURL: 26 | /// @param url Original URL 27 | /// @seealso evaluateWithURL: 28 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url; 29 | @end 30 | 31 | /// Mostly abstract class to implement EVKPercentEncodable, also encapsulates default 32 | /// return value logic: `(ret ? : @"")` 33 | /// @seealso EVKPercentEncodable 34 | /// @discussion When subclassed, evaluateUnencodedWithURL: should be overridden rather than evaluateWithURL: 35 | @interface EVKPercentEncodablePortion : NSObject 36 | @property (copy) NSNumber *percentEncodingIterations; 37 | - (instancetype)initWithPercentEncodingIterations:(int)iterations; 38 | + (instancetype)portionWithPercentEncodingIterations:(int)iterations; 39 | @end 40 | 41 | /// Portion which always returns a fixed string, optionally percent encoded for convenience 42 | @interface EVKStaticStringPortion : EVKPercentEncodablePortion 43 | @property (copy) NSString *string; 44 | - (instancetype)initWithString:(NSString *)str percentEncodingIterations:(int)iterations; 45 | + (instancetype)portionWithString:(NSString *)str percentEncodingIterations:(int)iterations; 46 | @end 47 | 48 | /// Portion which returns the entire URL 49 | @interface EVKFullURLPortion : EVKPercentEncodablePortion 50 | @end 51 | 52 | /// Portion which returns the URL's path 53 | @interface EVKTrimmedPathPortion : EVKPercentEncodablePortion 54 | @end 55 | 56 | /// Portion which returns the URL's fragment 57 | @interface EVKFragmentPortion : EVKPercentEncodablePortion 58 | @end 59 | 60 | /// Portion which returns the URL's resource specifier, excluding leading and trailing slashes 61 | @interface EVKTrimmedResourceSpecifierPortion : EVKPercentEncodablePortion 62 | @end 63 | 64 | /// Portion which returns the URL's host 65 | @interface EVKHostPortion : EVKPercentEncodablePortion 66 | @end 67 | 68 | /// Portion which returns the URL's scheme 69 | @interface EVKSchemePortion : EVKPercentEncodablePortion 70 | @end 71 | 72 | /// Portion which returns the URL's query string unmodified 73 | @interface EVKQueryPortion : EVKPercentEncodablePortion 74 | @end 75 | 76 | /// Portion which returns the value associated with a parameter in the URL's query 77 | @interface EVKQueryParameterValuePortion : EVKPercentEncodablePortion 78 | @property (copy) NSString *parameter; 79 | - (instancetype)initWithParameter:(NSString *)param 80 | percentEncodingIterations:(int)iterations; 81 | + (instancetype)portionWithParameter:(NSString *)param 82 | percentEncodingIterations:(int)iterations; 83 | @end 84 | 85 | /// Portion which returns the URL's query string translated with a given dictionary 86 | @interface EVKTranslatedQueryPortion : EVKPercentEncodablePortion 87 | @property (copy) NSDictionary *paramTranslations; 88 | - (instancetype)initWithDictionary:(NSDictionary *)dict 89 | percentEncodingIterations:(int)iterations; 90 | + (instancetype)portionWithDictionary:(NSDictionary *)dict 91 | percentEncodingIterations:(int)iterations; 92 | @end 93 | 94 | /// Portion which returns the result of a substitution being performed on the url with a given regex and template 95 | @interface EVKRegexSubstitutionPortion : EVKPercentEncodablePortion 96 | @property (copy) NSString *regex; 97 | @property (copy) NSString *templet; 98 | - (instancetype)initWithRegex:(NSString *)regex 99 | template:(NSString *)templet 100 | percentEncodingIterations:(int)iterations; 101 | + (instancetype)portionWithRegex:(NSString *)regex 102 | template:(NSString *)templet 103 | percentEncodingIterations:(int)iterations; 104 | @end 105 | 106 | /// Niche portion which returns the joined value for a key path for of a base64 encoded plist 107 | @interface EVKKeyValuePathPortion : EVKRegexSubstitutionPortion 108 | @property (copy) NSString *path; 109 | - (instancetype)initWithRegex:(NSString *)regex 110 | template:(NSString *)templet 111 | path:(NSString *)path 112 | percentEncodingIterations:(int)iterations; 113 | + (instancetype)portionWithRegex:(NSString *)regex 114 | template:(NSString *)templet 115 | path:(NSString *)path 116 | percentEncodingIterations:(int)iterations; 117 | @end 118 | 119 | /// Niche portion which returns an address from a base64 encoded MapItem 120 | @interface EVKMapItemUnwrapperPortion : EVKPercentEncodablePortion 121 | @end 122 | -------------------------------------------------------------------------------- /EvilKit/src/EVKURLPortions.m: -------------------------------------------------------------------------------- 1 | #import "EVKURLPortions.h" 2 | #import "NSURL+ComponentAdditions.h" 3 | 4 | #define set(...) [NSOrderedSet orderedSetWithObjects:__VA_ARGS__, nil] 5 | 6 | @implementation EVKFullURLPortion 7 | 8 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { return [url absoluteString]; } 9 | 10 | - (NSString *)stringRepresentation { return @"Full URL"; } 11 | 12 | // Coding {{{ 13 | + (BOOL)supportsSecureCoding { return YES; } 14 | 15 | - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; } 16 | 17 | - (instancetype)initWithCoder:(NSCoder *)coder { return [super initWithCoder:coder]; } 18 | 19 | - (instancetype)copyWithZone:(NSZone *)zone { 20 | return [[self class] portionWithPercentEncodingIterations:[[self percentEncodingIterations] intValue]]; 21 | } 22 | // }}} 23 | 24 | @end 25 | 26 | @implementation EVKTrimmedPathPortion 27 | 28 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { return [url trimmedPathComponent]; } 29 | 30 | - (NSString *)stringRepresentation { return @"Path"; } 31 | 32 | // Coding {{{ 33 | + (BOOL)supportsSecureCoding { return YES; } 34 | 35 | - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; } 36 | 37 | - (instancetype)initWithCoder:(NSCoder *)coder { return [super initWithCoder:coder]; }; 38 | 39 | - (instancetype)copyWithZone:(NSZone *)zone { 40 | return [[self class] portionWithPercentEncodingIterations:[[self percentEncodingIterations] intValue]]; 41 | } 42 | // }}} 43 | 44 | @end 45 | 46 | @implementation EVKTrimmedResourceSpecifierPortion 47 | 48 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { return [url trimmedResourceSpecifier]; } 49 | 50 | - (NSString *)stringRepresentation { return @"Resource specifier"; } 51 | 52 | // Coding {{{ 53 | + (BOOL)supportsSecureCoding { return YES; } 54 | 55 | - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; } 56 | 57 | - (instancetype)initWithCoder:(NSCoder *)coder { return [super initWithCoder:coder]; }; 58 | 59 | - (instancetype)copyWithZone:(NSZone *)zone { 60 | return [[self class] portionWithPercentEncodingIterations:[[self percentEncodingIterations] intValue]]; 61 | } 62 | // }}} 63 | 64 | @end 65 | 66 | @implementation EVKHostPortion 67 | 68 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { return [url hostComponent]; } 69 | 70 | - (NSString *)stringRepresentation { return @"Host domain"; } 71 | 72 | // Coding {{{ 73 | + (BOOL)supportsSecureCoding { return YES; } 74 | 75 | - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; } 76 | 77 | - (instancetype)initWithCoder:(NSCoder *)coder { return [super initWithCoder:coder]; }; 78 | 79 | - (instancetype)copyWithZone:(NSZone *)zone { 80 | return [[self class] portionWithPercentEncodingIterations:[[self percentEncodingIterations] intValue]]; 81 | } 82 | // }}} 83 | 84 | @end 85 | 86 | @implementation EVKSchemePortion 87 | 88 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { 89 | return [url scheme]; 90 | } 91 | 92 | - (NSString *)stringRepresentation { return @"Original scheme"; } 93 | 94 | // Coding {{{ 95 | + (BOOL)supportsSecureCoding { return YES; } 96 | 97 | - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; } 98 | 99 | - (instancetype)initWithCoder:(NSCoder *)coder { return [super initWithCoder:coder]; }; 100 | 101 | - (instancetype)copyWithZone:(NSZone *)zone { 102 | return [[self class] portionWithPercentEncodingIterations:[[self percentEncodingIterations] intValue]]; 103 | } 104 | // }}} 105 | 106 | @end 107 | 108 | @implementation EVKQueryPortion 109 | 110 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { return [url queryString]; } 111 | 112 | - (NSString *)stringRepresentation { return @"Original query"; } 113 | 114 | // Coding {{{ 115 | + (BOOL)supportsSecureCoding { return YES; } 116 | 117 | - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; } 118 | 119 | - (instancetype)initWithCoder:(NSCoder *)coder { return [super initWithCoder:coder]; }; 120 | 121 | - (instancetype)copyWithZone:(NSZone *)zone { 122 | return [[self class] portionWithPercentEncodingIterations:[[self percentEncodingIterations] intValue]]; 123 | } 124 | // }}} 125 | 126 | @end 127 | 128 | @implementation EVKFragmentPortion 129 | 130 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { 131 | return [url fragmentString]; 132 | } 133 | 134 | // Coding {{{ 135 | + (BOOL)supportsSecureCoding { return YES; } 136 | 137 | - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; } 138 | 139 | - (instancetype)initWithCoder:(NSCoder *)coder { return [super initWithCoder:coder]; }; 140 | 141 | - (instancetype)copyWithZone:(NSZone *)zone { 142 | return [[self class] portionWithPercentEncodingIterations:[[self percentEncodingIterations] intValue]]; 143 | } 144 | // }}} 145 | @end 146 | 147 | @implementation EVKQueryParameterValuePortion : EVKPercentEncodablePortion 148 | 149 | - (instancetype)initWithParameter:(NSString *)param 150 | percentEncodingIterations:(int)iterations { 151 | if((self = [super initWithPercentEncodingIterations:iterations])) { 152 | _parameter = param; 153 | } 154 | 155 | return self; 156 | } 157 | 158 | - (instancetype)init { 159 | return [self initWithParameter:@"" percentEncodingIterations:0]; 160 | } 161 | 162 | + (instancetype)portionWithParameter:(NSString *)param 163 | percentEncodingIterations:(int)iterations { 164 | return [[self alloc] initWithParameter:param percentEncodingIterations:iterations]; 165 | } 166 | 167 | - (NSString *)stringRepresentation { return @"Query parameter"; } 168 | 169 | - (NSString *)evaluateUnencodedWithURL:(NSURL *)url { return [url queryValueForParameter:[self parameter]]; } 170 | 171 | // Coding {{{ 172 | + (BOOL)supportsSecureCoding { return YES; } 173 | 174 | - (void)encodeWithCoder:(NSCoder *)coder { 175 | [super encodeWithCoder:coder]; 176 | [coder encodeObject:[self parameter] forKey:@"parameter"]; 177 | } 178 | 179 | - (instancetype)initWithCoder:(NSCoder *)coder { 180 | return [self initWithParameter:[coder decodeObjectForKey:@"parameter"] 181 | percentEncodingIterations:[coder decodeIntForKey:@"percentEncodingIterations"]]; 182 | } 183 | 184 | - (NSOrderedSet *)endUserAccessibleKeys { 185 | return set(@"parameter", @"percentEncodingIterations"); 186 | } 187 | 188 | - (instancetype)copyWithZone:(NSZone *)zone { 189 | return [[self class] portionWithPercentEncodingIterations:[[self percentEncodingIterations] intValue]]; 190 | } 191 | // }}} 192 | 193 | @end 194 | -------------------------------------------------------------------------------- /EvilScheme.x: -------------------------------------------------------------------------------- 1 | #import 2 | #import "PrivateFrameworks.h" 3 | #import 4 | #define appInstalled(app) [[LSApplicationWorkspace defaultWorkspace] applicationIsInstalled:app] 5 | 6 | // Preference retrieval {{{ 7 | static NSDictionary *prefs() { 8 | NSError *err; 9 | 10 | NSString *path = ROOT_PATH_NS(@"/var/mobile/Library/Preferences/EvilScheme/alternatives_v0.plist"); 11 | NSData *data = [NSData dataWithContentsOfFile:path]; 12 | 13 | NSKeyedUnarchiver *u = [[NSKeyedUnarchiver alloc] initForReadingFromData:data error:&err]; 14 | [u setRequiresSecureCoding:NO]; 15 | NSDictionary *ret = [u decodeObjectForKey:NSKeyedArchiveRootObjectKey]; 16 | 17 | if(err) NSLog(@"[EVS] Error loading prefs: %@", [err localizedDescription]); 18 | 19 | return ret ? : @{}; 20 | } 21 | 22 | static NSSet *blacklist() { 23 | NSError *err; 24 | 25 | NSString *path = ROOT_PATH_NS(@"/var/mobile/Library/Preferences/EvilScheme/blacklist_v0.plist"); 26 | NSData *data = [NSData dataWithContentsOfFile:path]; 27 | 28 | NSSet *types = [NSSet setWithObjects:[NSOrderedSet class], [NSString class], nil]; 29 | NSOrderedSet *ret = [NSKeyedUnarchiver unarchivedObjectOfClasses:types 30 | fromData:data 31 | error:&err]; 32 | if(err) NSLog(@"[EVS] Error loading blacklist: %@", [err localizedDescription]); 33 | return [[ret set] setByAddingObject:@"com.apple.siri"]; 34 | } 35 | // }}} 36 | 37 | // Logging {{{ 38 | static NSDictionary *logDict() { 39 | NSError *err; 40 | NSString *path = ROOT_PATH_NS(@"file:/var/mobile/Library/Preferences/EvilScheme/log_v0.plist"); 41 | 42 | NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:path] 43 | options:0 44 | error:&err]; 45 | 46 | NSSet *types = [NSSet setWithObjects:[NSDictionary class], 47 | [NSArray class], 48 | [NSString class], 49 | [NSNumber class], nil]; 50 | 51 | NSDictionary *ret = [NSKeyedUnarchiver unarchivedObjectOfClasses:types 52 | fromData:data 53 | error:&err]; 54 | 55 | if(err) NSLog(@"[EVS] Error reading log: %@", [err localizedDescription]); 56 | 57 | return ret; 58 | } 59 | 60 | static void setLogDict(NSDictionary *dict) { 61 | NSError *err; 62 | 63 | NSString *dir = ROOT_PATH_NS(@"/var/mobile/Library/Preferences/EvilScheme/"); 64 | // Ensure dir exists 65 | if (![[NSFileManager defaultManager] fileExistsAtPath:dir 66 | isDirectory:nil]) { 67 | [[NSFileManager defaultManager] createDirectoryAtPath:dir 68 | withIntermediateDirectories:YES 69 | attributes:nil 70 | error:&err]; 71 | } 72 | 73 | NSData *data = [NSKeyedArchiver archivedDataWithRootObject:dict 74 | requiringSecureCoding:NO 75 | error:&err]; 76 | 77 | NSString *path = ROOT_PATH_NS(@"file:/var/mobile/Library/Preferences/EvilScheme/log_v0.plist"); 78 | [data writeToURL:[NSURL URLWithString:path] 79 | options:0 80 | error:&err]; 81 | 82 | if(err) NSLog(@"[EVS] Error writing to log: %@", [err localizedDescription]); 83 | } 84 | 85 | static void logString(NSString *lString) { 86 | NSLog(@"[EVS] %@", lString); 87 | NSMutableDictionary *ld = [logDict() ? : @{} mutableCopy]; 88 | if([ld[@"enabled"] boolValue]) { 89 | NSMutableArray *arr = [ld[@"data"] ? : @[] mutableCopy]; 90 | [arr addObject:lString]; 91 | ld[@"data"] = arr; 92 | setLogDict(ld); 93 | } 94 | } 95 | // }}} 96 | 97 | // Spelunk into actions as a last resort to find URL 98 | static NSURL *urlFromActions(NSArray *actions) { 99 | __block NSURL *ret; 100 | for(BSAction *action in actions) { 101 | [[[action info] allSettings] enumerateIndexesUsingBlock:^ (NSUInteger idx, BOOL *stop) { 102 | id obj = [[action info] objectForSetting:idx]; 103 | if([obj isKindOfClass:%c(NSData)]) { 104 | ret = [[NSKeyedUnarchiver unarchivedObjectOfClass:[UAUserActivityInfo class] 105 | fromData:obj 106 | error:nil] webpageURL]; 107 | } 108 | }]; 109 | } 110 | return ret; 111 | } 112 | 113 | %hook FBSystemService 114 | 115 | - (void)openApplication:(NSString *)bundleID 116 | withOptions:(FBSOpenApplicationOptions *)options 117 | originator:(BSProcessHandle *)source 118 | requestID:(NSUInteger)req 119 | completion:(id)completion { 120 | 121 | EVKAppAlternative *app = prefs()[bundleID]; 122 | NSMutableString *lString = [NSMutableString new]; 123 | if([blacklist() containsObject:[source bundleIdentifier]] 124 | || !appInstalled([app substituteBundleID])) { 125 | [lString appendFormat:@"Ignored: %@\n%@\n", bundleID, options]; 126 | } 127 | else { 128 | [lString appendFormat:@"From: %@\n%@\n", bundleID, options]; 129 | if(app) { 130 | NSURL *url; // Check all known URL locations 131 | if((url = [options dictionary][@"__PayloadURL"]) 132 | || (url = [[options dictionary][@"__AppLink4LS"] URL]) 133 | || (url = urlFromActions([options dictionary][@"__Actions"]))) { 134 | [lString appendFormat:@"%@\n", [url absoluteString]]; 135 | if([app transformURL:url]) { 136 | // Craft new request 137 | bundleID = [app substituteBundleID]; 138 | NSMutableDictionary *opts = [NSMutableDictionary new]; 139 | opts[@"__PayloadURL"] = [app transformURL:url]; 140 | opts[@"__PayloadOptions"] = [options dictionary][@"__PayloadOptions"]; 141 | [options setDictionary:opts]; 142 | } 143 | } 144 | } 145 | [lString appendFormat:@"\nTo: %@\n%@\n", bundleID, options]; 146 | } 147 | 148 | logString(lString); 149 | %orig; 150 | } 151 | 152 | %end 153 | -------------------------------------------------------------------------------- /Prefs/src/EVSPortionVC.m: -------------------------------------------------------------------------------- 1 | #import "../L0Prefs/L0Prefs.h" 2 | #import "EVSPortionVC.h" 3 | #import "EVSQueryTranslatorVC.h" 4 | 5 | @implementation EVSPortionVC 6 | 7 | #pragma mark - lifecycle 8 | 9 | - (instancetype)initWithPortion:(EVSPortionVM *)portion withIndex:(NSInteger)idx { 10 | if((self = [super init])) { 11 | _portion = portion; 12 | _index = idx; 13 | } 14 | 15 | return self; 16 | } 17 | 18 | - (void)viewDidLoad { 19 | [super viewDidLoad]; 20 | [self setupNav]; 21 | } 22 | 23 | - (void)setupNav { 24 | [self setTitle:@"Configure fragment"]; 25 | } 26 | 27 | #pragma mark - table 28 | 29 | NS_ENUM(NSInteger, PortionVCSection) { 30 | TypeSection, 31 | PropertiesSection, 32 | }; 33 | 34 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 35 | return 3; 36 | } 37 | 38 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 39 | switch(section) { 40 | case TypeSection: 41 | return 1; 42 | case PropertiesSection: 43 | return [[self portion] propertyCount]; 44 | default: 45 | return 0; 46 | } 47 | } 48 | 49 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 50 | switch(section) { 51 | case TypeSection: 52 | return @"Portion Type"; 53 | case PropertiesSection: 54 | return @"Portion Properties"; 55 | default: 56 | return @""; 57 | } 58 | } 59 | 60 | #pragma mark - cells 61 | 62 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 63 | switch([indexPath section]) { 64 | case TypeSection: { 65 | L0PickerCell *cell = [tableView dequeueReusableCellWithIdentifier:PICKER_CELL_ID forIndexPath:indexPath]; 66 | [[cell textLabel] setText:@"Type"]; 67 | [[cell field] setText:[[self portion] stringRepresentation]]; 68 | 69 | NSDictionary *mappings = [EVSPortionVM classNameMappings]; 70 | [cell setOptions:[mappings allKeys]]; 71 | 72 | for(NSString *key in [mappings allKeys]) { 73 | if([[[self portion] portion] class] == mappings[key]) { 74 | [cell selectObject:key]; 75 | break; 76 | } 77 | } 78 | 79 | [[cell field] setTag:-1]; 80 | [cell setDelegate:self]; 81 | return cell; 82 | } 83 | case PropertiesSection: { 84 | Class cellType = [[self portion] cellTypeForIndex:[indexPath row]]; 85 | NSInteger row = [indexPath row]; 86 | if(cellType == [L0ToggleCell class]) { 87 | L0ToggleCell *cell = [tableView dequeueReusableCellWithIdentifier:TOGGLE_CELL_ID forIndexPath:indexPath]; 88 | [[cell textLabel] setText:[[self portion] propertyNameForIndex:row]]; 89 | [[cell toggle] setOn:[[[self portion] objectForPropertyIndex:row] boolValue]]; 90 | [cell setDelegate:self]; 91 | [[cell toggle] setTag:row]; 92 | return cell; 93 | } else if(cellType == [L0LinkCell class]) { 94 | L0LinkCell *cell = [tableView dequeueReusableCellWithIdentifier:LINK_CELL_ID forIndexPath:indexPath]; 95 | [[cell textLabel] setText:[[self portion] propertyNameForIndex:row]]; 96 | return cell; 97 | } else if(cellType == [L0StepperCell class]) { 98 | L0StepperCell *cell = [tableView dequeueReusableCellWithIdentifier:STEPPER_CELL_ID forIndexPath:indexPath]; 99 | [cell setPrefix:[[self portion] propertyNameForIndex:row]]; 100 | [[cell stepper] setMinimumValue:-10]; 101 | [[cell stepper] setMaximumValue:10]; 102 | [[cell stepper] setTag:row]; 103 | [[cell stepper] setValue:[(NSNumber *)[[self portion] objectForPropertyIndex:row] intValue]]; 104 | [cell setDelegate:self]; 105 | [cell stepperValueDidChange:[cell stepper]]; 106 | return cell; 107 | } else { 108 | L0EditTextCell *cell = [tableView dequeueReusableCellWithIdentifier:EDIT_TEXT_CELL_ID forIndexPath:indexPath]; 109 | [[cell textLabel] setText:[[self portion] propertyNameForIndex:row]]; 110 | [[cell field] setText:[[self portion] valueStringForIndex:row]]; 111 | [cell setDelegate:self]; 112 | [[cell field] setTag:[indexPath row]]; 113 | return cell; 114 | } 115 | } 116 | default: { 117 | return nil; 118 | } 119 | } 120 | } 121 | 122 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 123 | NSInteger row = [indexPath row]; 124 | if([indexPath section] == PropertiesSection 125 | && [[self portion] cellTypeForIndex:row] == [L0LinkCell class]) { 126 | id obj = [[self portion] objectForPropertyIndex:row]; 127 | if([obj isKindOfClass:[NSDictionary class]]) { 128 | EVSQueryTranslatorVC *ctrl = [[EVSQueryTranslatorVC alloc] initWithDictionary:obj]; 129 | [ctrl setTag:row]; 130 | [ctrl setDelegate:self]; 131 | [[self navigationController] pushViewController:ctrl animated:YES]; 132 | } 133 | } 134 | 135 | [super tableView:tableView didSelectRowAtIndexPath:indexPath]; 136 | } 137 | 138 | #pragma mark - editing 139 | 140 | - (void)toggleEditing { 141 | [[self tableView] setEditing:![[self tableView] isEditing] animated:YES]; 142 | [[[self navigationItem] rightBarButtonItem] setTitle:([[self tableView] isEditing] ? @"Done" : @"Edit")]; 143 | } 144 | 145 | #pragma mark - model 146 | 147 | - (void)textFieldDidChange:(UITextField *)field { 148 | if([field tag] == -1) { 149 | [self setPortion:[[EVSPortionVM alloc] initWithPortion:[[EVSPortionVM classNameMappings][[field text]] new]]]; 150 | [[self tableView] reloadSections:[NSIndexSet indexSetWithIndex:PropertiesSection] 151 | withRowAnimation:UITableViewRowAnimationFade]; 152 | } else { 153 | [[self portion] setObject:[field text] forPropertyIndex:[field tag]]; 154 | } 155 | 156 | [[self delegate] controllerDidChangeModel:self]; 157 | } 158 | 159 | - (void)stepperValueDidChange:(UIStepper *)stepper { 160 | [[self portion] setObject:@([stepper value]) forPropertyIndex:[stepper tag]]; 161 | [[self delegate] controllerDidChangeModel:self]; 162 | } 163 | 164 | - (void)switchValueDidChange:(UISwitch *)toggle { 165 | [[self portion] setObject:@([toggle isOn]) forPropertyIndex:[toggle tag]]; 166 | [[self delegate] controllerDidChangeModel:self]; 167 | } 168 | 169 | - (void)controllerDidChangeModel:(EVSQueryTranslatorVC *)controller { 170 | [[self portion] setObject:[[controller dict] dict] forPropertyIndex:[controller tag]]; 171 | 172 | [super controllerDidChangeModel:controller]; 173 | } 174 | 175 | @end 176 | -------------------------------------------------------------------------------- /Prefs/src/EVSQueryLexiconVC.m: -------------------------------------------------------------------------------- 1 | #import "../L0Prefs/L0Prefs.h" 2 | #import "EVSQueryLexiconVC.h" 3 | #import "EVSKeyValuePairVC.h" 4 | 5 | @implementation EVSQueryLexiconVC 6 | 7 | #pragma mark - lifecycle 8 | 9 | - (instancetype)initWithKey:(NSString *)key lexicon:(EVSQueryLexiconWrapper *)lex { 10 | if((self = [super init])) { 11 | _key = key; 12 | _lex = lex; 13 | } 14 | 15 | return self; 16 | } 17 | 18 | - (void)viewDidLoad { 19 | [super viewDidLoad]; 20 | [self setupNav]; 21 | } 22 | 23 | - (void)setupNav { 24 | [self setTitle:[self key]]; 25 | [[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:@"Edit" 26 | style:UIBarButtonItemStyleDone 27 | target:self 28 | action:@selector(toggleEditing)]]; 29 | } 30 | 31 | #pragma mark - table 32 | 33 | NS_ENUM(NSUInteger, QueryLexiconVCSections) { 34 | MetaSection, 35 | SubstitutionSection, 36 | }; 37 | 38 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 39 | return section ? [[self lex] count] + 1 : 3; 40 | } 41 | 42 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 43 | return 2; 44 | } 45 | 46 | #pragma mark - cells 47 | 48 | NS_ENUM(NSUInteger, QueryLexiconTextFieldTags) { 49 | OldParamTag, 50 | NewParamTag, 51 | DefaultStateTag, 52 | }; 53 | 54 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 55 | switch([indexPath section]) { 56 | case MetaSection: { 57 | L0EditTextCell *cell; 58 | switch([indexPath row]) { 59 | case OldParamTag: { 60 | cell = [[self tableView] dequeueReusableCellWithIdentifier:EDIT_TEXT_CELL_ID forIndexPath:indexPath]; 61 | [[cell textLabel] setText:@"Old Parameter"]; 62 | [[cell field] setText:[self key]]; 63 | break; 64 | } 65 | case NewParamTag: { 66 | cell = [[self tableView] dequeueReusableCellWithIdentifier:EDIT_TEXT_CELL_ID forIndexPath:indexPath]; 67 | [[cell textLabel] setText:@"New Parameter"]; 68 | [[cell field] setText:[[self lex] param]]; 69 | break; 70 | } 71 | case DefaultStateTag: { 72 | cell = [[self tableView] dequeueReusableCellWithIdentifier:PICKER_CELL_ID forIndexPath:indexPath]; 73 | [[cell textLabel] setText:@"Default state"]; 74 | [[cell field] setText:[[self lex] defaultState]]; 75 | [(L0PickerCell *)cell setOptions:@[@"Exclude value", @"Keep original value"]]; 76 | break; 77 | } 78 | } 79 | [[cell field] setTag:[indexPath row]]; 80 | [cell setDelegate:self]; 81 | return cell; 82 | } 83 | case SubstitutionSection: { 84 | if([indexPath row] < [[self lex] count]) { 85 | UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:BASIC_CELL_ID]; 86 | [[cell textLabel] setText:[[self lex] keys][[indexPath row]]]; 87 | [[cell detailTextLabel] setText:[[self lex] objectAtIndex:[indexPath row]]]; 88 | return cell; 89 | } else { 90 | L0ButtonCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:BUTTON_CELL_ID forIndexPath:indexPath]; 91 | [[cell textLabel] setText:@"Add new"]; 92 | return cell; 93 | } 94 | } 95 | default: return nil; 96 | } 97 | } 98 | 99 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 100 | if([indexPath section] == SubstitutionSection) { 101 | NSInteger idx = [indexPath row]; 102 | EVSKeyValuePairVC *ctrl = [EVSKeyValuePairVC alloc]; 103 | 104 | if(idx < [[self lex] count]) { 105 | ctrl = [ctrl initWithKey:[[self lex] keys][idx] 106 | value:[[self lex] objectAtIndex:idx]]; 107 | } else { 108 | ctrl = [ctrl initWithKey:@"" value:@""]; 109 | } 110 | 111 | [ctrl setDelegate:self]; 112 | UINavigationController *child = [[UINavigationController alloc] initWithRootViewController:ctrl]; 113 | [self presentViewController:child animated:YES completion:nil]; 114 | } 115 | 116 | [super tableView:tableView didSelectRowAtIndexPath:indexPath]; 117 | } 118 | 119 | #pragma mark - editing 120 | 121 | - (void)toggleEditing { 122 | [[self tableView] setEditing:![[self tableView] isEditing] animated:YES]; 123 | [[[self navigationItem] rightBarButtonItem] setTitle:([[self tableView] isEditing] ? @"Done" : @"Edit")]; 124 | } 125 | 126 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 127 | return [indexPath section] == SubstitutionSection && [indexPath row] < [[self lex] count]; 128 | } 129 | 130 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 131 | if(editingStyle == UITableViewCellEditingStyleDelete) { 132 | [[self lex] removeObjectAtIndex:[indexPath row]]; 133 | [[self tableView] deleteRowsAtIndexPaths:@[indexPath] 134 | withRowAnimation:UITableViewRowAnimationMiddle]; 135 | } 136 | } 137 | 138 | #pragma mark - model 139 | 140 | - (BOOL)controller:(L0PrefVC *)controller canMoveFromKey:(NSString *)from toKey:(NSString *)to { 141 | return !([[self lex] containsKey:to] || [to isEqualToString:@""]); 142 | } 143 | 144 | - (void)controllerDidChangeModel:(EVSKeyValuePairVC *)controller { 145 | [[self lex] setObject:[controller value] forKey:[controller key]]; 146 | [super controllerDidChangeModel:controller]; 147 | } 148 | 149 | - (void)controller:(L0PrefVC *)controller willMoveFromKey:(NSString *)from toKey:(NSString *)to { 150 | [[self lex] renameKey:from toString:to]; 151 | [super controllerDidChangeModel:controller]; 152 | } 153 | 154 | - (void)textFieldDidChange:(UITextField *)field { 155 | switch([field tag]) { 156 | case OldParamTag: { 157 | if([[self delegate] controller:self canMoveFromKey:[self key] toKey:[field text]]) { 158 | [[self delegate] controller:self willMoveFromKey:[self key] toKey:[field text]]; 159 | [self setKey:[field text]]; 160 | [self setTitle:[field text]]; 161 | } 162 | break; 163 | } 164 | case NewParamTag: { 165 | [[self lex] setParam:[field text]]; 166 | [[self delegate] controllerDidChangeModel:self]; 167 | break; 168 | } 169 | case DefaultStateTag: { 170 | [[self lex] setDefaultState:[field text]]; 171 | [[self delegate] controllerDidChangeModel:self]; 172 | break; 173 | } 174 | } 175 | } 176 | 177 | @end 178 | -------------------------------------------------------------------------------- /Prefs/src/EVSOutlineVC.m: -------------------------------------------------------------------------------- 1 | #import "EVSOutlineVC.h" 2 | #import "EVSPortionVC.h" 3 | 4 | @implementation EVSOutlineVC 5 | 6 | #pragma mark - lifecycle 7 | 8 | - (void)viewDidLoad { 9 | [super viewDidLoad]; 10 | [self setupNav]; 11 | } 12 | 13 | - (void)setupNav { 14 | [self setTitle:@"Edit URL Outline"]; 15 | [[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:@"Edit" 16 | style:UIBarButtonItemStyleDone 17 | target:self 18 | action:@selector(toggleEditing)]]; 19 | } 20 | 21 | #pragma mark - table 22 | 23 | NS_ENUM(NSInteger, OutlineVCSection) { 24 | RegexSection, 25 | PortionSection, 26 | }; 27 | 28 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 29 | return 3; 30 | } 31 | 32 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 33 | switch(section) { 34 | case RegexSection: 35 | return 1; 36 | case PortionSection: 37 | return [[[self action] outline] count] + 1; 38 | default: 39 | return 0; 40 | } 41 | } 42 | 43 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 44 | switch(section) { 45 | case RegexSection: 46 | return @"Trigger"; 47 | case PortionSection: 48 | return @"Blueprint"; 49 | default: 50 | return @""; 51 | } 52 | } 53 | 54 | #pragma mark - cells 55 | 56 | NS_ENUM(NSInteger, OutlineTextFieldTags) { 57 | RegexTag, 58 | TestURLTag, 59 | }; 60 | 61 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 62 | switch([indexPath section]) { 63 | case RegexSection: { 64 | L0EditTextCell *cell = [tableView dequeueReusableCellWithIdentifier:EDIT_TEXT_CELL_ID forIndexPath:indexPath]; 65 | [[cell textLabel] setText:@"Regex"]; 66 | [[cell field] setText:[[self action] regexPattern]]; 67 | [cell setDelegate:self]; 68 | return cell; 69 | } 70 | case PortionSection: { 71 | if([indexPath row] < [[[self action] outline] count]) { 72 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LINK_CELL_ID forIndexPath:indexPath]; 73 | NSObject *item = [[self action] outline][[indexPath row]]; 74 | [[cell textLabel] setText:[item stringRepresentation]]; 75 | return cell; 76 | } else { 77 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:BUTTON_CELL_ID forIndexPath:indexPath]; 78 | [[cell textLabel] setText:@"Add new fragment"]; 79 | return cell; 80 | } 81 | } 82 | } 83 | return nil; 84 | } 85 | 86 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 87 | if([indexPath section] == PortionSection) { 88 | NSInteger row = [indexPath row]; 89 | if(row < [[[self action] outline] count]) { 90 | [self presentVCForPortion:[[self action] outline][row] withIndex:row]; 91 | } else { 92 | UIAlertController *alertCtrl = [UIAlertController alertControllerWithTitle:@"Add new URL fragment" 93 | message:@"Choose a fragment type" 94 | preferredStyle:UIAlertControllerStyleActionSheet]; 95 | [alertCtrl addAction:[UIAlertAction actionWithTitle:@"Cancel" 96 | style:UIAlertActionStyleCancel 97 | handler:^void(UIAlertAction *action) {}]]; 98 | 99 | for(NSString *key in [EVSPortionVM classNameMappings]) { 100 | [alertCtrl addAction:[UIAlertAction actionWithTitle:key 101 | style:UIAlertActionStyleDefault 102 | handler:^void(UIAlertAction *action) { 103 | [self presentVCForPortion:[[EVSPortionVM classNameMappings][key] new] 104 | withIndex:row]; 105 | [[self tableView] reloadRowsAtIndexPaths:@[indexPath] 106 | withRowAnimation:UITableViewRowAnimationMiddle]; 107 | }]]; 108 | } 109 | [self presentViewController:alertCtrl animated:YES completion:nil]; 110 | } 111 | } 112 | 113 | [super tableView:tableView didSelectRowAtIndexPath:indexPath]; 114 | } 115 | 116 | - (void)presentVCForPortion:(NSObject *)portion withIndex:(NSInteger)idx { 117 | EVSPortionVC *ctrl = [[EVSPortionVC alloc] initWithPortion:[[EVSPortionVM alloc] initWithPortion:portion] 118 | withIndex:idx]; 119 | [ctrl setDelegate:self]; 120 | [self controllerDidChangeModel:ctrl]; 121 | [[self navigationController] pushViewController:ctrl animated:YES]; 122 | } 123 | 124 | #pragma mark - editing 125 | 126 | - (void)toggleEditing { 127 | [[self tableView] setEditing:![[self tableView] isEditing] animated:YES]; 128 | [[[self navigationItem] rightBarButtonItem] setTitle:([[self tableView] isEditing] ? @"Done" : @"Edit")]; 129 | } 130 | 131 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 132 | if(editingStyle == UITableViewCellEditingStyleDelete) { 133 | NSMutableArray *arr = [[[self action] outline] mutableCopy]; 134 | [arr removeObjectAtIndex:[indexPath row]]; 135 | [[self action] setOutline:arr]; 136 | [[self tableView] deleteRowsAtIndexPaths:@[indexPath] 137 | withRowAnimation:UITableViewRowAnimationMiddle]; 138 | } 139 | [[self delegate] controllerDidChangeModel:self]; 140 | } 141 | 142 | - (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath { 143 | return ([proposedDestinationIndexPath section] == PortionSection && [proposedDestinationIndexPath row] < [[[self action] outline] count]) ? proposedDestinationIndexPath : sourceIndexPath; 144 | } 145 | 146 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 147 | return [indexPath section] == PortionSection && [indexPath row] < [[[self action] outline] count]; 148 | } 149 | 150 | - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { 151 | return [indexPath section] == PortionSection && [indexPath row] < [[[self action] outline] count]; 152 | } 153 | 154 | - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { 155 | NSObject *obj = [[self action] outline][[sourceIndexPath row]]; 156 | NSMutableArray *arr = [[[self action] outline] mutableCopy]; 157 | [arr removeObjectAtIndex:[sourceIndexPath row]]; 158 | [arr insertObject:obj atIndex:[destinationIndexPath row]]; 159 | [[self action] setOutline:arr]; 160 | [[self delegate] controllerDidChangeModel:self]; 161 | } 162 | 163 | #pragma mark - model 164 | 165 | - (void)textFieldDidChange:(UITextField *)field { 166 | if([field tag] == RegexTag && [[self delegate] controller:self canMoveFromKey:[[self action] regexPattern] toKey:[field text]]) { 167 | [[self action] setRegexPattern:[field text]]; 168 | [[self delegate] controllerDidChangeModel:self]; 169 | } 170 | } 171 | 172 | - (void)controllerDidChangeModel:(EVSPortionVC *)controller { 173 | NSMutableArray *arr = [[[self action] outline] mutableCopy]; 174 | arr[[controller index]] = [[controller portion] portion]; 175 | [[self action] setOutline:arr]; 176 | [super controllerDidChangeModel:controller]; 177 | } 178 | 179 | @end 180 | -------------------------------------------------------------------------------- /Prefs/src/EVSExperimentalPrefsVC.m: -------------------------------------------------------------------------------- 1 | #import "EVSExperimentalPrefsVC.h" 2 | #import "EVSPreferenceManager.h" 3 | 4 | @implementation EVSExperimentalPrefsVC { 5 | NSMutableArray *blacklist; 6 | NSString *selectedEngine; 7 | NSDictionary *engines; 8 | } 9 | 10 | #pragma mark - lifecycle 11 | 12 | - (void)viewDidLoad { 13 | [super viewDidLoad]; 14 | [self setupNav]; 15 | [self setupData]; 16 | } 17 | 18 | - (void)setupData { 19 | blacklist = [[EVSPreferenceManager blacklistedApps] mutableCopy]; 20 | engines = @{ 21 | @"DuckDuckGo": @"ddg.gg/?q=", 22 | @"Google": @"google.com/search?q=", 23 | @"Yahoo": @"search.yahoo.com/search?q=", 24 | @"Bing": @"bing.com/search?q=", 25 | }; 26 | selectedEngine = [EVSPreferenceManager searchEngine]; 27 | } 28 | 29 | - (void)setupTable { 30 | [super setupTable]; 31 | [[self tableView] setEditing:YES animated:YES]; 32 | [[self tableView] setAllowsSelectionDuringEditing:YES]; 33 | } 34 | 35 | - (void)setupNav { 36 | [self setTitle:@"Experimental"]; 37 | [[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:@"Apply" 38 | style:UIBarButtonItemStylePlain 39 | target:self 40 | action:@selector(saveSettings)]]; 41 | } 42 | 43 | - (void)viewWillDisappear:(BOOL)animated { 44 | [EVSPreferenceManager setBlacklistedApps:blacklist]; 45 | } 46 | 47 | #pragma mark - table 48 | 49 | NS_ENUM(NSInteger, ExperimentalPrefsSections) { 50 | SearchEngineSection, 51 | BlacklistedAppSection, 52 | }; 53 | 54 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 55 | return 2; 56 | } 57 | 58 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 59 | switch (section) { 60 | case SearchEngineSection: 61 | return engines[selectedEngine] ? 1 : 2; 62 | case BlacklistedAppSection: 63 | return [blacklist count] + 1; 64 | default: 65 | return 0; 66 | } 67 | } 68 | 69 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 70 | switch(section) { 71 | case SearchEngineSection: 72 | return @"Spotlight search engine"; 73 | case BlacklistedAppSection: 74 | return @"Disabled apps"; 75 | default: 76 | return @""; 77 | } 78 | } 79 | 80 | - (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section { 81 | switch(section) { 82 | case SearchEngineSection: 83 | return @"To apply changes to default search engines, you may have to re-apply browser presets from the presets menu"; 84 | case BlacklistedAppSection: 85 | return @"Links opened from these apps will use stock behavior"; 86 | default: 87 | return @""; 88 | } 89 | } 90 | 91 | #pragma mark - cells 92 | 93 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 94 | switch([indexPath section]) { 95 | case SearchEngineSection: { 96 | NSString *eng = [EVSPreferenceManager searchEngine]; 97 | if([indexPath row]) { 98 | L0PureEditTextCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:PURE_EDIT_TEXT_CELL_ID forIndexPath:indexPath]; 99 | [[cell field] setPlaceholder:@"ex. ddg.gg/?q="]; 100 | [[cell field] setTag:-2]; 101 | [cell setDelegate:self]; 102 | [[cell field] setText:(engines[eng] ? @"" : eng)]; 103 | return cell; 104 | } else { 105 | L0PickerCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:PICKER_CELL_ID forIndexPath:indexPath]; 106 | [cell setDelegate:self]; 107 | [[cell textLabel] setText:@"Engine"]; 108 | [[cell field] setTag:-1]; 109 | [cell setOptions:[[engines allKeys] arrayByAddingObject:@"Custom URL Base"]]; 110 | if([[cell options] containsObject:eng]) { 111 | [[cell field] setText:eng]; 112 | [cell selectObject:eng]; 113 | } 114 | else { 115 | [[cell field] setText:@"Custom URL Base"]; 116 | [cell selectObject:@"Custom URL Base"]; 117 | } 118 | return cell; 119 | } 120 | 121 | } 122 | case BlacklistedAppSection: { 123 | if([indexPath row] < [blacklist count]) { 124 | L0PureEditTextCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:PURE_EDIT_TEXT_CELL_ID forIndexPath:indexPath]; 125 | [cell setDelegate:self]; 126 | [[cell field] setTag:[indexPath row]]; 127 | [[cell field] setPlaceholder:@"ex. com.saurik.Cydia"]; 128 | [[cell field] setText:blacklist[[indexPath row]]]; 129 | 130 | return cell; 131 | } else { 132 | UITableViewCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:BASIC_CELL_ID forIndexPath:indexPath]; 133 | [[cell textLabel] setText:@"Add new"]; 134 | 135 | return cell; 136 | } 137 | 138 | return nil; 139 | } 140 | default: 141 | return nil; 142 | } 143 | } 144 | 145 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 146 | [super tableView:tableView didSelectRowAtIndexPath:indexPath]; 147 | if([indexPath section] == BlacklistedAppSection && [indexPath row] == [blacklist count]) { 148 | [self tableView:[self tableView] commitEditingStyle:UITableViewCellEditingStyleInsert forRowAtIndexPath:indexPath]; 149 | } 150 | } 151 | 152 | #pragma mark - editing 153 | 154 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 155 | return [indexPath section]; 156 | } 157 | 158 | - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { 159 | if([indexPath section]) { 160 | if([indexPath row] < [blacklist count]) { 161 | return UITableViewCellEditingStyleDelete; 162 | } 163 | return UITableViewCellEditingStyleInsert; 164 | } 165 | return UITableViewCellEditingStyleNone; 166 | } 167 | 168 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 169 | switch(editingStyle) { 170 | case UITableViewCellEditingStyleDelete: 171 | [blacklist removeObjectAtIndex:[indexPath row]]; 172 | [[self tableView] deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 173 | break; 174 | case UITableViewCellEditingStyleInsert: 175 | if(![blacklist containsObject:@""]) { 176 | [blacklist addObject:@""]; 177 | [[self tableView] insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 178 | } 179 | break; 180 | default: 181 | break; 182 | } 183 | } 184 | 185 | #pragma mark - model 186 | 187 | - (void)textFieldDidChange:(UITextField *)field { 188 | if([field tag] == -1) { 189 | NSInteger old = [self tableView:[self tableView] numberOfRowsInSection:SearchEngineSection]; 190 | selectedEngine = [field text]; 191 | BOOL diff = !(old == [self tableView:[self tableView] numberOfRowsInSection:SearchEngineSection]); 192 | if(engines[[field text]]) { 193 | [EVSPreferenceManager setSearchEngine:[field text]]; 194 | if(diff) { 195 | [[self tableView] deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:SearchEngineSection]] 196 | withRowAnimation:UITableViewRowAnimationMiddle]; 197 | } 198 | } else { 199 | if(diff) { 200 | [[self tableView] insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:SearchEngineSection]] 201 | withRowAnimation:UITableViewRowAnimationMiddle]; 202 | } 203 | } 204 | } else if([field tag] == -2) { 205 | [EVSPreferenceManager setSearchEngine:[field text]]; 206 | } 207 | else { 208 | blacklist[[field tag]] = [[field text] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; 209 | } 210 | } 211 | 212 | - (void)saveSettings { 213 | [EVSPreferenceManager setBlacklistedApps:blacklist]; 214 | UIAlertController *ctrl = [UIAlertController alertControllerWithTitle:@"Saved" message:@"Preferences applied" preferredStyle:UIAlertControllerStyleAlert]; 215 | [self presentViewController:ctrl animated:YES completion:nil]; 216 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 217 | [ctrl dismissViewControllerAnimated:YES completion:nil]; 218 | }); 219 | } 220 | 221 | @end 222 | -------------------------------------------------------------------------------- /Prefs/src/EVSRootVC.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "../L0Prefs/L0Prefs.h" 4 | #import "EVSAppAlternativeVC.h" 5 | #import "EVSPreferenceManager.h" 6 | #import "EVSRootVC.h" 7 | #import "EVSExperimentalPrefsVC.h" 8 | #import "EVSLogVC.h" 9 | 10 | @implementation EVSRootVC { 11 | NSMutableArray *appAlternatives; 12 | NSArray *linkTitles; 13 | NSArray *linkURLs; 14 | } 15 | 16 | #pragma mark - lifecycle 17 | 18 | - (void)viewDidLoad { 19 | [super viewDidLoad]; 20 | [self setupData]; 21 | [self setupNav]; 22 | [self setupHeader]; 23 | } 24 | 25 | - (void)setupData { 26 | linkTitles = @[ 27 | @"Manual", 28 | @"Source code / Bug tracker", 29 | @"License: BSD 3-Clause", 30 | @"Follow Lorenzo on Twitter", 31 | @"Maintainer - 0xkuj", 32 | @"Plant a tree for his memory", 33 | @"Read Lorenzo's story", 34 | ]; 35 | 36 | linkURLs = @[ 37 | @"https://l.pane.net/evil.html", 38 | @"https://github.com/0xkuj/EvilScheme", 39 | @"https://opensource.org/licenses/BSD-3-Clause", 40 | @"https://twitter.com/mushyware", 41 | @"https://twitter.com/omrkujman", 42 | @"https://tree.tributestore.com/memorial-tree?oId=20482611&source=TRR&cn=Obituaries&bn=AdPerfect%20-%20Block%20Communications&md=Pittsburgh%20Post%20Gazette", 43 | @"https://obituaries.post-gazette.com/obituary/lorenzo-yovetich-pane-1081878757", 44 | ]; 45 | 46 | appAlternatives = [[EVSPreferenceManager activeAlternatives] mutableCopy]; 47 | } 48 | 49 | - (void)setupNav { 50 | [self setTitle:@"Evil Scheme"]; 51 | [[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithTitle:@"Apply" 52 | style:UIBarButtonItemStylePlain 53 | target:self 54 | action:@selector(saveSettings)]]; 55 | } 56 | 57 | - (void)setupHeader { 58 | UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, 130)]; 59 | UILabel *titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 0, 300, 70)]; 60 | UILabel *memoryLabel = [[UILabel alloc] initWithFrame:CGRectMake(15, 60, [[UIScreen mainScreen] bounds].size.width, 70)]; 61 | [memoryLabel setText:@"Dedicated to the memory of Lorenzo Pane"]; 62 | [memoryLabel setFont:[UIFont monospacedSystemFontOfSize:14 weight:UIFontWeightThin]]; 63 | [titleLabel setText:@"EvilScheme://"]; 64 | [titleLabel setFont:[UIFont monospacedSystemFontOfSize:28 weight:UIFontWeightThin]]; 65 | [headerView addSubview:titleLabel]; 66 | [headerView addSubview:memoryLabel]; 67 | [[self tableView] setTableHeaderView:headerView]; 68 | } 69 | 70 | - (BOOL)isRootVC { return YES; } 71 | 72 | - (void)viewWillDisappear:(BOOL)animated { 73 | [EVSPreferenceManager setActiveAlternatives:appAlternatives]; 74 | [super viewWillDisappear:animated]; 75 | } 76 | 77 | #pragma mark - table 78 | 79 | NS_ENUM(NSInteger, RootVCSection) { 80 | MetaSection, 81 | AppAlternativeSection, 82 | MoreSettingsSection, 83 | }; 84 | 85 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 86 | return 3; 87 | } 88 | 89 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 90 | switch(section) { 91 | case MetaSection: 92 | return [linkTitles count]; 93 | case AppAlternativeSection: 94 | return [appAlternatives count] + 1; 95 | case MoreSettingsSection: 96 | return 2; 97 | default: 98 | return 0; 99 | } 100 | } 101 | 102 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 103 | switch(section) { 104 | case AppAlternativeSection: 105 | return @"Default Apps"; 106 | case MoreSettingsSection: 107 | return @"Advanced"; 108 | default: 109 | return @""; 110 | } 111 | } 112 | 113 | #pragma mark - cells 114 | 115 | NS_ENUM(NSInteger, MoreSettingsCells) { 116 | ExperimentalCell, 117 | LogCell, 118 | }; 119 | 120 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 121 | switch([indexPath section]) { 122 | case MetaSection: { 123 | L0ButtonCell *cell = [tableView dequeueReusableCellWithIdentifier:BUTTON_CELL_ID forIndexPath:indexPath]; 124 | [[cell textLabel] setText:linkTitles[[indexPath row]]]; 125 | return cell; 126 | } 127 | case AppAlternativeSection: { 128 | if([indexPath row] < [appAlternatives count]) { 129 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LINK_CELL_ID forIndexPath:indexPath]; 130 | [[cell textLabel] setText:[appAlternatives[[indexPath row]] name]]; 131 | return cell; 132 | } else { 133 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:BUTTON_CELL_ID forIndexPath:indexPath]; 134 | [[cell textLabel] setText:@"Add new"]; 135 | return cell; 136 | }; 137 | } 138 | case MoreSettingsSection: { 139 | L0LinkCell *cell = [tableView dequeueReusableCellWithIdentifier:LINK_CELL_ID forIndexPath:indexPath]; 140 | [[cell textLabel] setText:([indexPath row] == LogCell ? @"Log" : @"Experimental Settings")]; 141 | return cell; 142 | } 143 | default: 144 | return nil; 145 | } 146 | } 147 | 148 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 149 | switch ([indexPath section]) { 150 | case MetaSection: { 151 | [[UIApplication sharedApplication] openURL:[NSURL URLWithString:linkURLs[[indexPath row]]] 152 | options:@{} 153 | completionHandler:nil]; 154 | break; 155 | } 156 | case AppAlternativeSection: { 157 | EVSAppAlternativeVC *ctrl = [EVSAppAlternativeVC new]; 158 | if([indexPath row] < [appAlternatives count]) { 159 | [ctrl setAppAlternative:appAlternatives[[indexPath row]]]; 160 | [ctrl setIndex:[indexPath row]]; 161 | [ctrl setDelegate:self]; 162 | [[self navigationController] pushViewController:ctrl animated:YES]; 163 | } else { 164 | [ctrl setAppAlternative:[EVSAppAlternativeWrapper new]]; 165 | [ctrl setIndex:[indexPath row]]; 166 | [ctrl setDelegate:self]; 167 | [[self navigationController] pushViewController:ctrl animated:YES]; 168 | [ctrl showPresetView]; 169 | } 170 | break; 171 | } 172 | case MoreSettingsSection: { 173 | switch([indexPath row]) { 174 | case LogCell: { 175 | EVSLogVC *ctrl = [EVSLogVC new]; 176 | [[self navigationController] pushViewController:ctrl animated:YES]; 177 | break; 178 | } 179 | case ExperimentalCell: { 180 | EVSExperimentalPrefsVC *ctrl = [EVSExperimentalPrefsVC new]; 181 | [[self navigationController] pushViewController:ctrl animated:YES]; 182 | break; 183 | } 184 | } 185 | break; 186 | } 187 | default: 188 | break; 189 | } 190 | [super tableView:tableView didSelectRowAtIndexPath:indexPath]; 191 | } 192 | 193 | #pragma mark - editing 194 | 195 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 196 | return [indexPath section] == AppAlternativeSection && [indexPath row] < [appAlternatives count]; 197 | } 198 | 199 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 200 | if(editingStyle == UITableViewCellEditingStyleDelete) { 201 | [appAlternatives removeObjectAtIndex:[indexPath row]]; 202 | [[self tableView] deleteRowsAtIndexPaths:@[indexPath] 203 | withRowAnimation:UITableViewRowAnimationMiddle]; 204 | } 205 | } 206 | 207 | #pragma mark - model 208 | 209 | - (void)controllerDidChangeModel:(EVSAppAlternativeVC *)controller { 210 | [appAlternatives setObject:[controller appAlternative] atIndexedSubscript:[controller index]]; 211 | [super controllerDidChangeModel:controller]; 212 | } 213 | 214 | - (BOOL)controller:(L0PrefVC *)controller canMoveFromKey:(NSString *)from toKey:(NSString *)to { 215 | if([to isEqualToString:@""]) 216 | return NO; 217 | 218 | for(EVSAppAlternativeWrapper *wrapper in appAlternatives) 219 | if([[wrapper name] isEqualToString:to]) 220 | return NO; 221 | 222 | return YES; 223 | } 224 | 225 | - (void)saveSettings { 226 | [EVSPreferenceManager setActiveAlternatives:appAlternatives]; 227 | [EVSPreferenceManager applyActiveAlternatives:appAlternatives]; 228 | UIAlertController *ctrl = [UIAlertController alertControllerWithTitle:@"Saved" message:@"Preferences applied" preferredStyle:UIAlertControllerStyleAlert]; 229 | [self presentViewController:ctrl animated:YES completion:nil]; 230 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC), dispatch_get_main_queue(), ^{ 231 | [ctrl dismissViewControllerAnimated:YES completion:nil]; 232 | }); 233 | } 234 | 235 | @end 236 | -------------------------------------------------------------------------------- /Prefs/src/EVSAppAlternativeVC.m: -------------------------------------------------------------------------------- 1 | #import "../L0Prefs/L0Prefs.h" 2 | #import "EVSPresetListVC.h" 3 | #import "EVSAppAlternativeVC.h" 4 | 5 | @implementation EVSAppAlternativeVC 6 | 7 | #pragma mark - lifecycle 8 | 9 | - (void)viewDidLoad { 10 | [super viewDidLoad]; 11 | [self setupNav]; 12 | } 13 | 14 | - (void)setupNav { 15 | [self setTitle:[[self appAlternative] name]]; 16 | [[self navigationItem] setRightBarButtonItem:[[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemOrganize 17 | target:self 18 | action:@selector(showPresetView)]]; 19 | } 20 | 21 | - (void)setupTable { 22 | [super setupTable]; 23 | [[self tableView] setEditing:YES]; 24 | [[self tableView] setAllowsSelectionDuringEditing:YES]; 25 | } 26 | 27 | #pragma mark - table 28 | 29 | NS_ENUM(NSInteger, AppVCSection) { 30 | MetaSection, 31 | TargetAppSection, 32 | OutlineSection, 33 | }; 34 | 35 | - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { 36 | return 3; 37 | } 38 | 39 | - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { 40 | switch(section) { 41 | case TargetAppSection: 42 | return @"Target Bundle IDs"; 43 | case OutlineSection: 44 | return @"Actions"; 45 | default: 46 | return @""; 47 | } 48 | } 49 | 50 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 51 | switch (section) { 52 | case MetaSection: 53 | return 2; 54 | case TargetAppSection: 55 | return [[[self appAlternative] targetBundleIDs] count] + 1; 56 | case OutlineSection: 57 | return [[[self appAlternative] urlOutlines] count] + 1; 58 | default: 59 | return 0; 60 | } 61 | } 62 | 63 | #pragma mark - cells 64 | 65 | NS_ENUM(NSInteger, AppTextFieldTags) { 66 | NameTag = -1, 67 | SubstituteBundleIDTag = -2, 68 | }; 69 | 70 | NS_ENUM(NSInteger, MetaCells) { 71 | NameCell, 72 | SubstituteBundleCell, 73 | }; 74 | 75 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 76 | switch ([indexPath section]) { 77 | case MetaSection: { 78 | L0EditTextCell *cell = [tableView dequeueReusableCellWithIdentifier:EDIT_TEXT_CELL_ID forIndexPath:indexPath]; 79 | switch([indexPath row]) { 80 | case NameCell: { 81 | [[cell textLabel] setText:@"Name"]; 82 | [[cell field] setPlaceholder:@"For easy identification"]; 83 | [[cell field] setText:[[self appAlternative] name]]; 84 | [[cell field] setTag:NameTag]; 85 | break; 86 | } 87 | case SubstituteBundleCell: { 88 | [[cell textLabel] setText:@"New Bundle ID"]; 89 | [[cell field] setPlaceholder:@"ex: com.brave.ios.browser"]; 90 | [[cell field] setText:[[self appAlternative] substituteBundleID]]; 91 | [[cell field] setTag:SubstituteBundleIDTag]; 92 | break; 93 | } 94 | } 95 | [cell setDelegate:self]; 96 | return cell; 97 | } 98 | case TargetAppSection: { 99 | if([indexPath row] < [[[self appAlternative] targetBundleIDs] count]) { 100 | L0PureEditTextCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:PURE_EDIT_TEXT_CELL_ID forIndexPath:indexPath]; 101 | [cell setDelegate:self]; 102 | [[cell field] setTag:[indexPath row]]; 103 | [[cell field] setPlaceholder:@"ex. com.apple.mobilesafari"]; 104 | [[cell field] setText:[[self appAlternative] targetBundleIDs][[indexPath row]]]; 105 | 106 | return cell; 107 | } else { 108 | UITableViewCell *cell = [[self tableView] dequeueReusableCellWithIdentifier:BASIC_CELL_ID forIndexPath:indexPath]; 109 | [[cell textLabel] setText:@"Add new"]; 110 | 111 | return cell; 112 | } 113 | } 114 | case OutlineSection: { 115 | if([indexPath row] < [[[self appAlternative] urlOutlines] count]) { 116 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:LINK_CELL_ID forIndexPath:indexPath]; 117 | [[cell textLabel] setText:[[[self appAlternative] urlOutlines][[indexPath row]] regexPattern]]; 118 | return cell; 119 | } else { 120 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:BUTTON_CELL_ID forIndexPath:indexPath]; 121 | [[cell textLabel] setText:@"Add new"]; 122 | return cell; 123 | } 124 | } 125 | default: 126 | return nil; 127 | } 128 | } 129 | 130 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { 131 | switch([indexPath section]) { 132 | case OutlineSection: { 133 | EVSOutlineVC *ctrl = [EVSOutlineVC new]; 134 | if([indexPath row] < [[[self appAlternative] urlOutlines] count]) { 135 | [ctrl setIndex:[indexPath row]]; 136 | [ctrl setAction:[[self appAlternative] urlOutlines][[indexPath row]]]; 137 | } else { 138 | [ctrl setIndex:[indexPath row]]; 139 | [ctrl setAction:[EVKAction new]]; 140 | } 141 | [ctrl setDelegate:self]; 142 | [[self navigationController] pushViewController:ctrl animated:YES]; 143 | break; 144 | } 145 | case TargetAppSection: { 146 | if([indexPath row] == [[[self appAlternative] targetBundleIDs] count]) { 147 | [[self tableView] deselectRowAtIndexPath:indexPath animated:YES]; 148 | [self tableView:[self tableView] commitEditingStyle:UITableViewCellEditingStyleInsert forRowAtIndexPath:indexPath]; 149 | } 150 | break; 151 | } 152 | default: 153 | break; 154 | } 155 | 156 | [super tableView:tableView didSelectRowAtIndexPath:indexPath]; 157 | 158 | } 159 | 160 | #pragma mark - editing 161 | 162 | - (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath { 163 | switch(editingStyle) { 164 | case UITableViewCellEditingStyleDelete: { 165 | switch([indexPath section]) { 166 | case OutlineSection: { 167 | NSMutableArray *arr = [[[self appAlternative] urlOutlines] mutableCopy]; 168 | [arr removeObjectAtIndex:[indexPath row]]; 169 | [[self appAlternative] setUrlOutlines:arr]; 170 | [[self tableView] deleteRowsAtIndexPaths:@[indexPath] 171 | withRowAnimation:UITableViewRowAnimationMiddle]; 172 | break; 173 | } case TargetAppSection: { 174 | [[[self appAlternative] targetBundleIDs] removeObjectAtIndex:[indexPath row]]; 175 | [[self tableView] deleteRowsAtIndexPaths:@[indexPath] 176 | withRowAnimation:UITableViewRowAnimationMiddle]; 177 | } 178 | } 179 | break; 180 | } 181 | case UITableViewCellEditingStyleInsert: { 182 | if(![[[self appAlternative] targetBundleIDs] containsObject:@""]) { 183 | [[[self appAlternative] targetBundleIDs] addObject:@""]; 184 | [[self tableView] insertRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic]; 185 | } 186 | break; 187 | } 188 | 189 | default: 190 | break; 191 | } 192 | } 193 | 194 | - (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath { 195 | return [indexPath section] == TargetAppSection 196 | || ([indexPath section] == OutlineSection 197 | && [indexPath row] < [[[self appAlternative] urlOutlines] count]); 198 | } 199 | 200 | - (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath { 201 | return [indexPath section] == OutlineSection && [indexPath row] < [[[self appAlternative] urlOutlines] count]; 202 | } 203 | 204 | - (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath { 205 | switch([indexPath section]) { 206 | case OutlineSection: 207 | return UITableViewCellEditingStyleDelete; 208 | break; 209 | 210 | case TargetAppSection: { 211 | return [indexPath row] < [[[self appAlternative] targetBundleIDs] count] ? UITableViewCellEditingStyleDelete : UITableViewCellEditingStyleInsert; 212 | } 213 | default: 214 | return UITableViewCellEditingStyleNone; 215 | } 216 | } 217 | 218 | - (NSIndexPath *)tableView:(UITableView *)tableView targetIndexPathForMoveFromRowAtIndexPath:(NSIndexPath *)sourceIndexPath toProposedIndexPath:(NSIndexPath *)proposedDestinationIndexPath { 219 | return ([proposedDestinationIndexPath section] == OutlineSection && [proposedDestinationIndexPath row] < [[[self appAlternative] urlOutlines] count]) ? proposedDestinationIndexPath : sourceIndexPath; 220 | } 221 | 222 | - (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath { 223 | NSMutableArray *mut = [[[self appAlternative] urlOutlines] mutableCopy]; 224 | EVKAction *action = mut[[sourceIndexPath row]]; 225 | [mut removeObjectAtIndex:[sourceIndexPath row]]; 226 | [mut insertObject:action atIndex:[destinationIndexPath row]]; 227 | [[self appAlternative] setUrlOutlines:mut]; 228 | [[self delegate] controllerDidChangeModel:self]; 229 | } 230 | 231 | #pragma mark - model 232 | 233 | - (BOOL)controller:(L0PrefVC *)controller canMoveFromKey:(NSString *)from toKey:(NSString *)to { 234 | return !([to isEqualToString:@""] || [[self appAlternative] containsKey:to]); 235 | 236 | } 237 | 238 | - (void)showPresetView { 239 | EVSPresetListVC *ctrl = [EVSPresetListVC new]; 240 | [ctrl setDelegate:self]; 241 | UINavigationController *child = [[UINavigationController alloc] initWithRootViewController:ctrl]; 242 | [self presentViewController:child animated:YES completion:nil]; 243 | } 244 | 245 | - (void)controllerDidChangeModel:(EVSOutlineVC *)controller { 246 | if([controller isKindOfClass:[EVSOutlineVC class]]) { 247 | NSMutableArray *arr = [[[self appAlternative] urlOutlines] mutableCopy]; 248 | arr[[controller index]] = [controller action]; 249 | [[self appAlternative] setUrlOutlines:arr]; 250 | 251 | } 252 | 253 | [self setTitle:[[self appAlternative] name]]; 254 | [super controllerDidChangeModel:controller]; 255 | } 256 | 257 | - (void)textFieldDidChange:(UITextField *)textField { 258 | switch([textField tag]) { 259 | case NameTag: 260 | if([[self delegate] controller:self canMoveFromKey:[[self appAlternative] name] toKey:[textField text]]) { 261 | [[self appAlternative] setName:[textField text]]; 262 | } 263 | break; 264 | case SubstituteBundleIDTag: 265 | [[self appAlternative] setSubstituteBundleID:[textField text]]; 266 | break; 267 | default: { 268 | [[self appAlternative] targetBundleIDs][[textField tag]] = [textField text]; 269 | break; 270 | } 271 | } 272 | 273 | [[self delegate] controllerDidChangeModel:self]; 274 | } 275 | 276 | @end 277 | -------------------------------------------------------------------------------- /EvilKit/Tests/EvilKitTests.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | 5 | @interface EvilKitTests : XCTestCase 6 | @end 7 | 8 | @implementation EvilKitTests { 9 | NSArray *urlStrings; 10 | NSMutableArray *urls; 11 | } 12 | 13 | - (void)setUp { 14 | urlStrings = @[ 15 | @"", 16 | @"mB*RTDCwT7kVYmyM4(dr5c6jxTr%83ICoHEXdKn7Z$~ABHq#B=3n82WuUResWcGK", 17 | @"example.com", 18 | @"www.example.com", 19 | @"http://example.com", 20 | @"https://www.example.com", 21 | @"https://www.example.com/path/to/neat/file.txt", 22 | @"https://www.example.com/?arg1=420&arg2=69", 23 | @"https://johnny:p4ssw0rd@www.example.com:443/script.ext;param=value?query=value#ref", 24 | @"firefox-focus://open-url?url=http%3A%2F%2Fexample%2Ecom%2F%3Farg1%3D420%26arg2%3D69", 25 | @"https://maps.apple.com/?address=1%20Infinite%20Loop%20Cupertino%20CA%2095014%20United%20States&abPersonID=3", 26 | @"maps:?saddr=San+Jose&daddr=San+Francisco&dirflg=r", 27 | @"maps://?q=Mexican+Restaurant&sll=50.894967,4.341626&z=10&t=r", 28 | @"x-web-search://?this%20is%20a%20test", 29 | @"x-web-search://?this%20is%20a%20test-/:;()$$&%E2%80%9D@,?%E2%80%99%20n%5D+%E2%82%AC%23!%7C", 30 | @"mailto:test@example.com", 31 | @"mailto:test@example.com?cc=bar@example.com&subject=Greetings%20from%20Cupertino!&body=Wish%20you%20were%20here!", 32 | @"https://youtu.be/dQw4w9WgXc", 33 | @"https://example.com/?n1=one&n2=two", 34 | @"https://example.com/?nOne=1&nTwo=2", 35 | ]; 36 | 37 | urls = [NSMutableArray new]; 38 | for(NSString *url in urlStrings) 39 | [urls addObject:[NSURL URLWithString:url]]; 40 | } 41 | 42 | // Trivial portion tests {{{ 43 | - (void)testQuery { 44 | NSArray *queries = @[ 45 | @"", 46 | @"", 47 | @"", 48 | @"", 49 | @"", 50 | @"", 51 | @"", 52 | @"arg1=420&arg2=69", 53 | @"query=value", 54 | @"url=http%3A%2F%2Fexample%2Ecom%2F%3Farg1%3D420%26arg2%3D69", 55 | @"address=1%20Infinite%20Loop%20Cupertino%20CA%2095014%20United%20States&abPersonID=3", 56 | @"saddr=San+Jose&daddr=San+Francisco&dirflg=r", 57 | @"q=Mexican+Restaurant&sll=50.894967,4.341626&z=10&t=r", 58 | @"this%20is%20a%20test", 59 | @"this%20is%20a%20test-/:;()$$&%E2%80%9D@,?%E2%80%99%20n%5D+%E2%82%AC%23!%7C", 60 | @"", 61 | @"cc=bar@example.com&subject=Greetings%20from%20Cupertino!&body=Wish%20you%20were%20here!", 62 | @"", 63 | @"n1=one&n2=two", 64 | @"nOne=1&nTwo=2", 65 | ]; 66 | 67 | NSString *target; 68 | NSString *result; 69 | for(int i = 0; i < [urlStrings count]; i++) { 70 | target = queries[i]; 71 | result = [[EVKQueryPortion new] evaluateWithURL:urls[i]]; 72 | XCTAssertTrue([target isEqualToString:result]); 73 | } 74 | } 75 | 76 | - (void)testResource { 77 | NSArray *schemes = @[ 78 | @"", 79 | @"mB*RTDCwT7kVYmyM4(dr5c6jxTr%83ICoHEXdKn7Z$~ABHq#B=3n82WuUResWcGK", 80 | @"example.com", 81 | @"www.example.com", 82 | @"example.com", 83 | @"www.example.com", 84 | @"www.example.com/path/to/neat/file.txt", 85 | @"www.example.com/?arg1=420&arg2=69", 86 | @"johnny:p4ssw0rd@www.example.com:443/script.ext;param=value?query=value#ref", 87 | @"open-url?url=http%3A%2F%2Fexample%2Ecom%2F%3Farg1%3D420%26arg2%3D69", 88 | @"maps.apple.com/?address=1%20Infinite%20Loop%20Cupertino%20CA%2095014%20United%20States&abPersonID=3", 89 | @"?saddr=San+Jose&daddr=San+Francisco&dirflg=r", 90 | @"?q=Mexican+Restaurant&sll=50.894967,4.341626&z=10&t=r", 91 | @"?this%20is%20a%20test", 92 | @"?this%20is%20a%20test-/:;()$$&%E2%80%9D@,?%E2%80%99%20n%5D+%E2%82%AC%23!%7C", 93 | @"test@example.com", 94 | @"test@example.com?cc=bar@example.com&subject=Greetings%20from%20Cupertino!&body=Wish%20you%20were%20here!", 95 | @"youtu.be/dQw4w9WgXc", 96 | @"example.com/?n1=one&n2=two", 97 | @"example.com/?nOne=1&nTwo=2", 98 | ]; 99 | 100 | NSString *target; 101 | NSString *result; 102 | for(int i = 0; i < [urlStrings count]; i++) { 103 | target = schemes[i]; 104 | result = [[EVKTrimmedResourceSpecifierPortion new] evaluateWithURL:urls[i]]; 105 | XCTAssertTrue([target isEqualToString:result]); 106 | } 107 | } 108 | 109 | - (void)testHost { 110 | NSArray *schemes = @[ 111 | @"", 112 | @"", 113 | @"", 114 | @"", 115 | @"example.com", 116 | @"www.example.com", 117 | @"www.example.com", 118 | @"www.example.com", 119 | @"www.example.com", 120 | @"open-url", 121 | @"maps.apple.com", 122 | @"", 123 | @"", 124 | @"", 125 | @"", 126 | @"", 127 | @"", 128 | @"youtu.be", 129 | @"example.com", 130 | @"example.com", 131 | ]; 132 | 133 | NSString *target; 134 | NSString *result; 135 | for(int i = 0; i < [urlStrings count]; i++) { 136 | target = schemes[i]; 137 | result = [[EVKHostPortion new] evaluateWithURL:urls[i]]; 138 | XCTAssertTrue([target isEqualToString:result]); 139 | } 140 | } 141 | 142 | - (void)testPath { 143 | NSArray *schemes = @[ 144 | @"", 145 | @"", 146 | @"example.com", 147 | @"www.example.com", 148 | @"", 149 | @"", 150 | @"path/to/neat/file.txt", 151 | @"", 152 | @"script.ext;param=value", 153 | @"", 154 | @"", 155 | @"", 156 | @"", 157 | @"", 158 | @"", 159 | @"test@example.com", 160 | @"test@example.com", 161 | @"dQw4w9WgXc", 162 | @"", 163 | @"", 164 | ]; 165 | 166 | NSString *target; 167 | NSString *result; 168 | for(int i = 0; i < [urlStrings count]; i++) { 169 | target = schemes[i]; 170 | result = [[EVKTrimmedPathPortion portionWithPercentEncodingIterations:0] evaluateWithURL:urls[i]]; 171 | XCTAssertTrue([target isEqualToString:result]); 172 | } 173 | } 174 | 175 | - (void)testScheme { 176 | NSArray *schemes = @[ 177 | @"", 178 | @"", 179 | @"", 180 | @"", 181 | @"http", 182 | @"https", 183 | @"https", 184 | @"https", 185 | @"https", 186 | @"firefox-focus", 187 | @"https", 188 | @"maps", 189 | @"maps", 190 | @"x-web-search", 191 | @"x-web-search", 192 | @"mailto", 193 | @"mailto", 194 | @"https", 195 | @"https", 196 | @"https", 197 | ]; 198 | 199 | NSString *target; 200 | NSString *result; 201 | for(int i = 0; i < [urlStrings count]; i++) { 202 | target = schemes[i]; 203 | result = [[EVKSchemePortion new] evaluateWithURL:urls[i]]; 204 | XCTAssertTrue([target isEqualToString:result]); 205 | } 206 | } 207 | 208 | - (void)testFullURL { 209 | NSString *target; 210 | NSString *result; 211 | for(int i = 0; i < [urlStrings count]; i++) { 212 | target = urlStrings[i]; 213 | result = [[EVKFullURLPortion portionWithPercentEncodingIterations:0] evaluateWithURL:urls[i]]; 214 | XCTAssertTrue([target isEqualToString:result]); 215 | } 216 | } 217 | 218 | - (void)testStaticString {/* good luck breaking that */} 219 | // }}} 220 | 221 | // Complex portion tests {{{ 222 | - (void)testQueryTranslations { 223 | EVKTranslatedQueryPortion *example = [EVKTranslatedQueryPortion portionWithDictionary:@{ 224 | @"n1" : [[EVKQueryItemLexicon alloc] initWithKeyName:@"nOne" 225 | dictionary:@{@"one" : @"1"} 226 | defaultState:URLQueryStateNull], 227 | @"n2" : [[EVKQueryItemLexicon alloc] initWithKeyName:@"nTwo" 228 | dictionary:@{@"two" : @"2"} 229 | defaultState:URLQueryStateNull], 230 | } percentEncodingIterations:0]; 231 | EVKTranslatedQueryPortion *maps = [EVKTranslatedQueryPortion portionWithDictionary:@{ 232 | @"t" : [[EVKQueryItemLexicon alloc] initWithKeyName:@"directionsmode" 233 | dictionary:@{ 234 | @"d": @"driving", 235 | @"w": @"walking", 236 | @"r": @"transit", 237 | } 238 | defaultState:URLQueryStateNull], 239 | @"dirflg": [[EVKQueryItemLexicon alloc] initWithKeyName:@"views" 240 | dictionary:@{ 241 | @"k": @"satelite", 242 | @"r": @"transit", 243 | } 244 | defaultState:URLQueryStateNull], 245 | @"address" : [EVKQueryItemLexicon identityLexiconWithName:@"daddr"], 246 | @"daddr" : [EVKQueryItemLexicon identityLexiconWithName:@"daddr"], 247 | @"saddr" : [EVKQueryItemLexicon identityLexiconWithName:@"saddr"], 248 | @"q" : [EVKQueryItemLexicon identityLexiconWithName:@"q"], 249 | @"ll" : [EVKQueryItemLexicon identityLexiconWithName:@"q"], 250 | @"z" : [EVKQueryItemLexicon identityLexiconWithName:@"zoom"], 251 | } percentEncodingIterations:0]; 252 | 253 | XCTAssertTrue([[example evaluateWithURL:urls[18]] isEqualToString:@"nOne=1&nTwo=2"]); 254 | XCTAssertTrue([[maps evaluateWithURL:urls[10]] isEqualToString:@"daddr=1%20Infinite%20Loop%20Cupertino%20CA%2095014%20United%20States"]); 255 | XCTAssertTrue([[maps evaluateWithURL:urls[11]] isEqualToString:@"saddr=San+Jose&daddr=San+Francisco&views=transit"]); 256 | XCTAssertTrue([[maps evaluateWithURL:urls[12]] isEqualToString:@"q=Mexican+Restaurant&zoom=10&directionsmode=transit"]); 257 | } 258 | 259 | - (void)testRegexSubstitution {/* apple's behaviour */} 260 | // }}} 261 | 262 | #pragma GCC diagnostic ignored "-Wdeprecated-declarations" 263 | - (void)testArchive { 264 | NSError *error; 265 | 266 | NSString *ffx = @"firefox-focus://open-url?url="; 267 | NSString *ddg = @"https://ddg.gg/?q="; 268 | EVKAppAlternative *browser = [EVKAppAlternative alloc]; 269 | browser = [browser initWithTargetBundleID:@"com.apple.mobilesafari" 270 | substituteBundleID:@"org.mozilla.ios.Focus" 271 | urlOutlines:@[ 272 | [EVKAction actionWithPattern:@"^x-web-search:" outline:@[ 273 | [EVKStaticStringPortion portionWithString:ffx percentEncodingIterations:0], 274 | [EVKStaticStringPortion portionWithString:ddg percentEncodingIterations:1], 275 | [EVKQueryPortion portionWithPercentEncodingIterations:1], 276 | ]], 277 | [EVKAction actionWithPattern:@"^http(s?):" outline:@[ 278 | [EVKStaticStringPortion portionWithString:ffx percentEncodingIterations:0], 279 | [EVKFullURLPortion portionWithPercentEncodingIterations:1], 280 | ]], 281 | ]]; 282 | 283 | EVKAppAlternative *mail = [EVKAppAlternative alloc]; 284 | mail = [mail initWithTargetBundleID:@"com.apple.mobilemail" 285 | substituteBundleID:@"com.readdle.smartemail" 286 | urlOutlines:@[ 287 | [EVKAction actionWithPattern:@"^mailto:[^\?]*$" outline:@[ 288 | [EVKStaticStringPortion portionWithString:@"readdle-spark://compose?recipient=" percentEncodingIterations:0], 289 | [EVKTrimmedPathPortion portionWithPercentEncodingIterations:0], 290 | ]], 291 | [EVKAction actionWithPattern:@"^mailto:.*\?.*$" outline:@[ 292 | [EVKStaticStringPortion portionWithString:@"readdle-spark://compose?recipient=" percentEncodingIterations:0], 293 | [EVKTrimmedPathPortion portionWithPercentEncodingIterations:0], 294 | [EVKStaticStringPortion portionWithString:@"&" percentEncodingIterations:0], 295 | [EVKTranslatedQueryPortion portionWithDictionary:@{ 296 | @"bcc" : [EVKQueryItemLexicon identityLexiconWithName:@"bcc"], 297 | @"body" : [EVKQueryItemLexicon identityLexiconWithName:@"body"], 298 | @"cc" : [EVKQueryItemLexicon identityLexiconWithName:@"cc"], 299 | @"subject" : [EVKQueryItemLexicon identityLexiconWithName:@"subject"], 300 | @"to" : [EVKQueryItemLexicon identityLexiconWithName:@"recipient"], 301 | } percentEncodingIterations:0], 302 | ]], 303 | ]]; 304 | 305 | EVKAppAlternative *navigator = [EVKAppAlternative alloc]; 306 | navigator = [navigator initWithTargetBundleID:@"com.apple.Maps" 307 | substituteBundleID:@"com.google.Maps" 308 | urlOutlines:@[ 309 | [EVKAction actionWithPattern:@"^(((http(s?)://)?maps.apple.com)|(maps:))" outline:@[ 310 | [EVKStaticStringPortion portionWithString:@"comgooglemaps://?" percentEncodingIterations:0], 311 | [EVKTranslatedQueryPortion portionWithDictionary:@{ 312 | @"t" : [[EVKQueryItemLexicon alloc] initWithKeyName:@"directionsmode" 313 | dictionary:@{ 314 | @"d": @"driving", 315 | @"w": @"walking", 316 | @"r": @"transit", 317 | } 318 | defaultState:URLQueryStateNull], 319 | @"dirflg" : [[EVKQueryItemLexicon alloc] initWithKeyName:@"views" 320 | dictionary:@{ 321 | @"k": @"satelite", 322 | @"r": @"transit", 323 | } 324 | defaultState:URLQueryStateNull], 325 | @"address" : [EVKQueryItemLexicon identityLexiconWithName:@"daddr"], 326 | @"daddr" : [EVKQueryItemLexicon identityLexiconWithName:@"daddr"], 327 | @"saddr" : [EVKQueryItemLexicon identityLexiconWithName:@"saddr"], 328 | @"q" : [EVKQueryItemLexicon identityLexiconWithName:@"q"], 329 | @"ll" : [EVKQueryItemLexicon identityLexiconWithName:@"q"], 330 | @"z" : [EVKQueryItemLexicon identityLexiconWithName:@"zoom"], 331 | } percentEncodingIterations:0] 332 | ]]]]; 333 | 334 | NSDictionary *appAlternatives = @{ 335 | @"com.apple.mobilesafari" : browser, 336 | @"com.apple.Maps" : navigator, 337 | @"com.apple.mobilemail" : mail, 338 | }; 339 | 340 | NSData *data = [NSKeyedArchiver archivedDataWithRootObject:appAlternatives 341 | requiringSecureCoding:YES 342 | error:&error]; 343 | XCTAssertNil(error); 344 | 345 | [data writeToURL:[NSURL URLWithString:@"file:/tmp/archive.plist"] 346 | options:0 347 | error:&error]; 348 | 349 | XCTAssertNil(error); 350 | 351 | data = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"file:/tmp/archive.plist"] 352 | options:0 353 | error:&error]; 354 | 355 | XCTAssertNil(error); 356 | 357 | [NSKeyedUnarchiver unarchiveTopLevelObjectWithData:data 358 | error:&error]; 359 | 360 | XCTAssertNil(error); 361 | } 362 | #pragma GCC diagnostic pop 363 | 364 | // Preset tests {{{ 365 | - (void)testFocus { 366 | NSArray *targets = @[ 367 | @"", 368 | @"mB*RTDCwT7kVYmyM4(dr5c6jxTr%83ICoHEXdKn7Z$~ABHq#B=3n82WuUResWcGK", 369 | @"example.com", 370 | @"www.example.com", 371 | @"firefox-focus://open-url?url=%68%74%74%70%3A%2F%2F%65%78%61%6D%70%6C%65%2E%63%6F%6D", 372 | @"firefox-focus://open-url?url=%68%74%74%70%73%3A%2F%2F%77%77%77%2E%65%78%61%6D%70%6C%65%2E%63%6F%6D", 373 | @"firefox-focus://open-url?url=%68%74%74%70%73%3A%2F%2F%77%77%77%2E%65%78%61%6D%70%6C%65%2E%63%6F%6D%2F%70%61%74%68%2F%74%6F%2F%6E%65%61%74%2F%66%69%6C%65%2E%74%78%74", 374 | @"firefox-focus://open-url?url=%68%74%74%70%73%3A%2F%2F%77%77%77%2E%65%78%61%6D%70%6C%65%2E%63%6F%6D%2F%3F%61%72%67%31%3D%34%32%30%26%61%72%67%32%3D%36%39", 375 | @"firefox-focus://open-url?url=%68%74%74%70%73%3A%2F%2F%6A%6F%68%6E%6E%79%3A%70%34%73%73%77%30%72%64%40%77%77%77%2E%65%78%61%6D%70%6C%65%2E%63%6F%6D%3A%34%34%33%2F%73%63%72%69%70%74%2E%65%78%74%3B%70%61%72%61%6D%3D%76%61%6C%75%65%3F%71%75%65%72%79%3D%76%61%6C%75%65%23%72%65%66", 376 | @"firefox-focus://open-url?url=http%3A%2F%2Fexample%2Ecom%2F%3Farg1%3D420%26arg2%3D69", 377 | @"firefox-focus://open-url?url=%68%74%74%70%73%3A%2F%2F%6D%61%70%73%2E%61%70%70%6C%65%2E%63%6F%6D%2F%3F%61%64%64%72%65%73%73%3D%31%25%32%30%49%6E%66%69%6E%69%74%65%25%32%30%4C%6F%6F%70%25%32%30%43%75%70%65%72%74%69%6E%6F%25%32%30%43%41%25%32%30%39%35%30%31%34%25%32%30%55%6E%69%74%65%64%25%32%30%53%74%61%74%65%73%26%61%62%50%65%72%73%6F%6E%49%44%3D%33", 378 | @"maps:?saddr=San+Jose&daddr=San+Francisco&dirflg=r", 379 | @"maps://?q=Mexican+Restaurant&sll=50.894967,4.341626&z=10&t=r", 380 | @"firefox-focus://open-url?url=%68%74%74%70%73%3A%2F%2F%64%64%67%2E%67%67%2F%3F%71%3D%74%68%69%73%25%32%30%69%73%25%32%30%61%25%32%30%74%65%73%74", 381 | @"firefox-focus://open-url?url=%68%74%74%70%73%3A%2F%2F%64%64%67%2E%67%67%2F%3F%71%3D%74%68%69%73%25%32%30%69%73%25%32%30%61%25%32%30%74%65%73%74%2D%2F%3A%3B%28%29%24%24%26%25%45%32%25%38%30%25%39%44%40%2C%3F%25%45%32%25%38%30%25%39%39%25%32%30%6E%25%35%44%2B%25%45%32%25%38%32%25%41%43%25%32%33%21%25%37%43", 382 | @"mailto:test@example.com", 383 | @"mailto:test@example.com?cc=bar@example.com&subject=Greetings%20from%20Cupertino!&body=Wish%20you%20were%20here!", 384 | @"firefox-focus://open-url?url=%68%74%74%70%73%3A%2F%2F%79%6F%75%74%75%2E%62%65%2F%64%51%77%34%77%39%57%67%58%63", 385 | @"firefox-focus://open-url?url=%68%74%74%70%73%3A%2F%2F%65%78%61%6D%70%6C%65%2E%63%6F%6D%2F%3F%6E%31%3D%6F%6E%65%26%6E%32%3D%74%77%6F", 386 | @"firefox-focus://open-url?url=%68%74%74%70%73%3A%2F%2F%65%78%61%6D%70%6C%65%2E%63%6F%6D%2F%3F%6E%4F%6E%65%3D%31%26%6E%54%77%6F%3D%32" 387 | ]; 388 | 389 | NSString *ffx = @"firefox-focus://open-url?url="; 390 | NSString *ddg = @"https://ddg.gg/?q="; 391 | EVKAppAlternative *browser = [EVKAppAlternative alloc]; 392 | browser = [browser initWithTargetBundleID:@"com.apple.mobilesafari" 393 | substituteBundleID:@"org.mozilla.ios.Focus" 394 | urlOutlines:@[ 395 | [EVKAction actionWithPattern:@"^x-web-search:" outline:@[ 396 | [EVKStaticStringPortion portionWithString:ffx percentEncodingIterations:0], 397 | [EVKStaticStringPortion portionWithString:ddg percentEncodingIterations:1], 398 | [EVKQueryPortion portionWithPercentEncodingIterations:1], 399 | ]], 400 | [EVKAction actionWithPattern:@"^http(s?):" outline:@[ 401 | [EVKStaticStringPortion portionWithString:ffx percentEncodingIterations:0], 402 | [EVKFullURLPortion portionWithPercentEncodingIterations:1], 403 | ]], 404 | ]]; 405 | NSString *target; 406 | NSString *result; 407 | for(int i = 0; i < [urlStrings count]; i++) { 408 | target = targets[i]; 409 | result = [[browser transformURL:[NSURL URLWithString:urlStrings[i]]] absoluteString]; 410 | XCTAssertTrue(!result || [result isEqualToString:target]); 411 | } 412 | } 413 | 414 | - (void)testChrome { 415 | NSArray *targets = @[ 416 | [NSNull null], 417 | @"mB*RTDCwT7kVYmyM4(dr5c6jxTr%83ICoHEXdKn7Z$~ABHq#B=3n82WuUResWcGK", 418 | @"example.com", 419 | @"www.example.com", 420 | @"googlechrome://example.com", 421 | @"googlechromes://www.example.com", 422 | @"googlechromes://www.example.com/path/to/neat/file.txt", 423 | @"googlechromes://www.example.com/?arg1=420&arg2=69", 424 | @"googlechromes://johnny:p4ssw0rd@www.example.com:443/script.ext;param=value?query=value#ref", 425 | @"firefox-focus://open-url?url=http%3A%2F%2Fexample%2Ecom%2F%3Farg1%3D420%26arg2%3D69", 426 | @"googlechromes://maps.apple.com/?address=1%20Infinite%20Loop%20Cupertino%20CA%2095014%20United%20States&abPersonID=3", 427 | @"maps:?saddr=San+Jose&daddr=San+Francisco&dirflg=r", 428 | @"maps://?q=Mexican+Restaurant&sll=50.894967,4.341626&z=10&t=r", 429 | @"googlechromes://ddg.gg/?q=%74%68%69%73%25%32%30%69%73%25%32%30%61%25%32%30%74%65%73%74", 430 | @"googlechromes://ddg.gg/?q=%74%68%69%73%25%32%30%69%73%25%32%30%61%25%32%30%74%65%73%74%2D%2F%3A%3B%28%29%24%24%26%25%45%32%25%38%30%25%39%44%40%2C%3F%25%45%32%25%38%30%25%39%39%25%32%30%6E%25%35%44%2B%25%45%32%25%38%32%25%41%43%25%32%33%21%25%37%43", 431 | @"mailto:test@example.com", 432 | @"mailto:test@example.com?cc=bar@example.com&subject=Greetings%20from%20Cupertino!&body=Wish%20you%20were%20here!", 433 | @"googlechromes://youtu.be/dQw4w9WgXc", 434 | @"googlechromes://example.com/?n1=one&n2=two", 435 | @"googlechromes://example.com/?nOne=1&nTwo=2", 436 | ]; 437 | 438 | 439 | NSString *chr = @"googlechrome://"; 440 | NSString *chrs = @"googlechromes://"; 441 | NSString *ddg = @"ddg.gg/?q="; 442 | EVKAppAlternative *browser = [EVKAppAlternative alloc]; 443 | browser = [browser initWithTargetBundleID:@"com.apple.mobilesafari" 444 | substituteBundleID:@"com.google.chrome.ios" 445 | urlOutlines:@[ 446 | [EVKAction actionWithPattern:@"^http:" outline:@[ 447 | [EVKStaticStringPortion portionWithString:chr percentEncodingIterations:0], 448 | [EVKTrimmedResourceSpecifierPortion portionWithPercentEncodingIterations:0], 449 | ]], 450 | [EVKAction actionWithPattern:@"^https:" outline:@[ 451 | [EVKStaticStringPortion portionWithString:chrs percentEncodingIterations:0], 452 | [EVKTrimmedResourceSpecifierPortion portionWithPercentEncodingIterations:0], 453 | ]], 454 | [EVKAction actionWithPattern:@"^x-web-search:" outline:@[ 455 | [EVKStaticStringPortion portionWithString:chrs percentEncodingIterations:0], 456 | [EVKStaticStringPortion portionWithString:ddg percentEncodingIterations:0], 457 | [EVKQueryPortion portionWithPercentEncodingIterations:1], 458 | ]], 459 | ]]; 460 | NSString *target; 461 | NSString *result; 462 | for(int i = 0; i < [urlStrings count]; i++) { 463 | target = targets[i]; 464 | result = [[browser transformURL:[NSURL URLWithString:urlStrings[i]]] absoluteString]; 465 | XCTAssertTrue(!result || [result isEqualToString:target]); 466 | } 467 | } 468 | 469 | - (void)testBrave { 470 | NSArray *targets = @[ 471 | [NSNull null], 472 | @"mB*RTDCwT7kVYmyM4(dr5c6jxTr%83ICoHEXdKn7Z$~ABHq#B=3n82WuUResWcGK", 473 | @"example.com", 474 | @"www.example.com", 475 | @"brave://open-url?url=%68%74%74%70%3A%2F%2F%65%78%61%6D%70%6C%65%2E%63%6F%6D", 476 | @"brave://open-url?url=%68%74%74%70%73%3A%2F%2F%77%77%77%2E%65%78%61%6D%70%6C%65%2E%63%6F%6D", 477 | @"brave://open-url?url=%68%74%74%70%73%3A%2F%2F%77%77%77%2E%65%78%61%6D%70%6C%65%2E%63%6F%6D%2F%70%61%74%68%2F%74%6F%2F%6E%65%61%74%2F%66%69%6C%65%2E%74%78%74", 478 | @"brave://open-url?url=%68%74%74%70%73%3A%2F%2F%77%77%77%2E%65%78%61%6D%70%6C%65%2E%63%6F%6D%2F%3F%61%72%67%31%3D%34%32%30%26%61%72%67%32%3D%36%39", 479 | @"brave://open-url?url=%68%74%74%70%73%3A%2F%2F%6A%6F%68%6E%6E%79%3A%70%34%73%73%77%30%72%64%40%77%77%77%2E%65%78%61%6D%70%6C%65%2E%63%6F%6D%3A%34%34%33%2F%73%63%72%69%70%74%2E%65%78%74%3B%70%61%72%61%6D%3D%76%61%6C%75%65%3F%71%75%65%72%79%3D%76%61%6C%75%65%23%72%65%66", 480 | @"firefox-focus://open-url?url=http%3A%2F%2Fexample%2Ecom%2F%3Farg1%3D420%26arg2%3D69", 481 | @"brave://open-url?url=%68%74%74%70%73%3A%2F%2F%6D%61%70%73%2E%61%70%70%6C%65%2E%63%6F%6D%2F%3F%61%64%64%72%65%73%73%3D%31%25%32%30%49%6E%66%69%6E%69%74%65%25%32%30%4C%6F%6F%70%25%32%30%43%75%70%65%72%74%69%6E%6F%25%32%30%43%41%25%32%30%39%35%30%31%34%25%32%30%55%6E%69%74%65%64%25%32%30%53%74%61%74%65%73%26%61%62%50%65%72%73%6F%6E%49%44%3D%33", 482 | @"maps:?saddr=San+Jose&daddr=San+Francisco&dirflg=r", 483 | @"maps://?q=Mexican+Restaurant&sll=50.894967,4.341626&z=10&t=r", 484 | @"brave://open-url?url=%68%74%74%70%73%3A%2F%2F%64%64%67%2E%67%67%2F%3F%71%3D%74%68%69%73%25%32%30%69%73%25%32%30%61%25%32%30%74%65%73%74", 485 | @"brave://open-url?url=%68%74%74%70%73%3A%2F%2F%64%64%67%2E%67%67%2F%3F%71%3D%74%68%69%73%25%32%30%69%73%25%32%30%61%25%32%30%74%65%73%74%2D%2F%3A%3B%28%29%24%24%26%25%45%32%25%38%30%25%39%44%40%2C%3F%25%45%32%25%38%30%25%39%39%25%32%30%6E%25%35%44%2B%25%45%32%25%38%32%25%41%43%25%32%33%21%25%37%43", 486 | @"mailto:test@example.com", 487 | @"mailto:test@example.com?cc=bar@example.com&subject=Greetings%20from%20Cupertino!&body=Wish%20you%20were%20here!", 488 | @"brave://open-url?url=%68%74%74%70%73%3A%2F%2F%79%6F%75%74%75%2E%62%65%2F%64%51%77%34%77%39%57%67%58%63", 489 | @"brave://open-url?url=%68%74%74%70%73%3A%2F%2F%65%78%61%6D%70%6C%65%2E%63%6F%6D%2F%3F%6E%31%3D%6F%6E%65%26%6E%32%3D%74%77%6F", 490 | @"brave://open-url?url=%68%74%74%70%73%3A%2F%2F%65%78%61%6D%70%6C%65%2E%63%6F%6D%2F%3F%6E%4F%6E%65%3D%31%26%6E%54%77%6F%3D%32" 491 | ]; 492 | 493 | NSString *ffx = @"brave://open-url?url="; 494 | NSString *ddg = @"https://ddg.gg/?q="; 495 | EVKAppAlternative *browser = [EVKAppAlternative alloc]; 496 | browser = [browser initWithTargetBundleID:@"com.apple.mobilesafari" 497 | substituteBundleID:@"com.brave.ios.browser" 498 | urlOutlines:@[ 499 | [EVKAction actionWithPattern:@"^x-web-search:" outline:@[ 500 | [EVKStaticStringPortion portionWithString:ffx percentEncodingIterations:0], 501 | [EVKStaticStringPortion portionWithString:ddg percentEncodingIterations:1], 502 | [EVKQueryPortion portionWithPercentEncodingIterations:1], 503 | ]], 504 | [EVKAction actionWithPattern:@"^http(s?):" outline:@[ 505 | [EVKStaticStringPortion portionWithString:ffx percentEncodingIterations:0], 506 | [EVKFullURLPortion portionWithPercentEncodingIterations:1], 507 | ]], 508 | ]]; 509 | NSString *target; 510 | NSString *result; 511 | for(int i = 0; i < [urlStrings count]; i++) { 512 | target = targets[i]; 513 | result = [[browser transformURL:[NSURL URLWithString:urlStrings[i]]] absoluteString]; 514 | XCTAssertTrue(!result || [result isEqualToString:target]); 515 | } 516 | } 517 | 518 | - (void)testReddit { 519 | NSDictionary *URLs = @{ 520 | @"https://reddit.com": @"apollo://", 521 | @"http://www.reddit.com/r/jailbreak/": @"apollo://reddit.com/r/jailbreak", 522 | @"https://www.reddit.com/r/jailbreak/comments/gth4zu/": @"apollo://reddit.com/r/jailbreak/comments/gth4zu", 523 | @"https://m.reddit.com/r/jailbreak/comments/gth4zu/upcoming_free_release_evil_scheme_change_your/": @"apollo://reddit.com/r/jailbreak/comments/gth4zu/upcoming_free_release_evil_scheme_change_your", 524 | @"https://amp.reddit.com/branch-redirect?creative=AppSelectorModal&experiment=app_selector_contrast_iteration&path=%2Fr%2Fjailbreak%2Fcomments%2Fgth4zu%2Fupcoming_free_release_evil_scheme_change_your%2F&variant=control_2": @"apollo://reddit.com/r/jailbreak/comments/gth4zu/upcoming_free_release_evil_scheme_change_your/", 525 | @"https://reddit.app.link/?channel=xpromo&feature=amp&campaign=app_selector_contrast_iteration&tags=AppSelectorModal&keyword=blue_header&%24og_redirect=https%3A%2F%2Fwww.reddit.com%2Fr%2Fjailbreak%2Fcomments%2Fgth4zu%2Fupcoming_free_release_evil_scheme_change_your%2F&%24deeplink_path=%2Fr%2Fjailbreak%2Fcomments%2Fgth4zu%2Fupcoming_free_release_evil_scheme_change_your%2F&%24android_deeplink_path=reddit%2Fr%2Fjailbreak%2Fcomments%2Fgth4zu%2Fupcoming_free_release_evil_scheme_change_your%2F&utm_source=xpromo&utm_medium=amp&utm_name=app_selector_contrast_iteration&utm_term=blue_header&utm_content=AppSelectorModal&mweb_loid=;" : @"apollo://reddit.com/r/jailbreak/comments/gth4zu/upcoming_free_release_evil_scheme_change_your/", 526 | @"reddit:///r/jailbreak/comments/gth4zu/": @"apollo://reddit.com/r/jailbreak/comments/gth4zu", 527 | }; 528 | 529 | EVKAppAlternative *apollo = [[EVKAppAlternative alloc] initWithTargetBundleID:@"com.reddit.Reddit" 530 | substituteBundleID:@"com.christianselig.Apollo" 531 | urlOutlines:@[ 532 | [EVKAction actionWithPattern:@".*reddit.com/r.*" outline:@[ 533 | [EVKStaticStringPortion portionWithString:@"apollo://reddit.com/" percentEncodingIterations:0], 534 | [EVKTrimmedPathPortion portionWithPercentEncodingIterations:0], 535 | ]], 536 | [EVKAction actionWithPattern:@".*reddit.com(/?)$" outline:@[ 537 | [EVKStaticStringPortion portionWithString:@"apollo://" percentEncodingIterations:0], 538 | ]], 539 | [EVKAction actionWithPattern:@"amp.reddit.com/branch-redirect.*" outline:@[ 540 | [EVKStaticStringPortion portionWithString:@"apollo://reddit.com" percentEncodingIterations:0], 541 | [EVKQueryParameterValuePortion portionWithParameter:@"path" percentEncodingIterations:0] 542 | ]], 543 | [EVKAction actionWithPattern:@".*reddit.app.link.*" outline:@[ 544 | [EVKStaticStringPortion portionWithString:@"apollo://reddit.com" percentEncodingIterations:0], 545 | [EVKQueryParameterValuePortion portionWithParameter:@"$deeplink_path" percentEncodingIterations:0] 546 | ]], 547 | [EVKAction actionWithPattern:@"reddit:///r/.*" outline:@[ 548 | [EVKStaticStringPortion portionWithString:@"apollo://reddit.com/" percentEncodingIterations:0], 549 | [EVKTrimmedResourceSpecifierPortion portionWithPercentEncodingIterations:0], 550 | ]], 551 | ]]; 552 | for(NSString *url in URLs) { 553 | NSString *target = URLs[url]; 554 | NSString *result = [[apollo transformURL:[NSURL URLWithString:url]] absoluteString]; 555 | XCTAssertTrue(!result || [result isEqualToString:target]); 556 | } 557 | } 558 | 559 | - (void)testZebra { 560 | NSDictionary *urls = @{ 561 | @"cydia://url/https://cydia.saurik.com/api/share#?source=https://repo.dynastic.co": @"zbra://sources/add/https://repo.dynastic.co", 562 | @"cydia://url/https://cydia.saurik.com/api/share#?source=https://repo.dynastic.co/&package=net.pane.l.evilscheme": @"zbra://packages/net.pane.l.evilscheme?source=https://repo.dynastic.co/", 563 | @"cydia://url/https://cydia.saurik.com/api/share#?package=net.pane.l.evilscheme": @"zbra://packages/net.pane.l.evilscheme", 564 | }; 565 | 566 | EVKAppAlternative *zebra = [[EVKAppAlternative alloc] initWithTargetBundleID:@"com.saurik.Cydia" 567 | substituteBundleID:@"xyz.willy.Zebra" 568 | urlOutlines:@[ 569 | [EVKAction actionWithPattern:@"^cydia:.*(((source).*(package))|((package).*(source)))" outline:@[ 570 | [EVKStaticStringPortion portionWithString:@"zbra://packages/" percentEncodingIterations:0], 571 | [EVKRegexSubstitutionPortion portionWithRegex:@".*?package=(.*?)(&|$).*" 572 | template:@"$1" 573 | percentEncodingIterations:0], 574 | [EVKStaticStringPortion portionWithString:@"?source=" percentEncodingIterations:0], 575 | [EVKRegexSubstitutionPortion portionWithRegex:@".*?source=(.*?)(&|$).*" 576 | template:@"$1" 577 | percentEncodingIterations:0], 578 | ]], 579 | [EVKAction actionWithPattern:@"^cydia:.*package" outline:@[ 580 | [EVKStaticStringPortion portionWithString:@"zbra://packages/" percentEncodingIterations:0], 581 | [EVKRegexSubstitutionPortion portionWithRegex:@".*?package=(.*?)(&|$).*" 582 | template:@"$1" 583 | percentEncodingIterations:0], 584 | ]], 585 | [EVKAction actionWithPattern:@"^cydia:.*source" outline:@[ 586 | [EVKStaticStringPortion portionWithString:@"zbra://sources/add/" percentEncodingIterations:0], 587 | [EVKRegexSubstitutionPortion portionWithRegex:@".*?source=(.*?)(&|$).*" 588 | template:@"$1" 589 | percentEncodingIterations:0], 590 | ]], 591 | ]]; 592 | 593 | for(NSString *url in urls) { 594 | NSURL *result = [zebra transformURL:[NSURL URLWithString:url]]; 595 | XCTAssertTrue([[result absoluteString] isEqualToString:urls[url]]); 596 | } 597 | } 598 | 599 | - (void)testTweetbot { 600 | NSArray *urls = @[ 601 | @"https://twitter.com/mushyware", 602 | @"https://www.twitter.com/mushyware/status/1267615150321827840", 603 | @"http://twitter.com/i/lists/1005878912671461376", 604 | @"https://twitter.com/pwn20wnd/status/1163111004370161666?s=21", 605 | ]; 606 | 607 | EVKAppAlternative *tweetbot = [[EVKAppAlternative alloc] initWithTargetBundleID:@"com.atebits.Tweetie2" 608 | substituteBundleID:@"com.tapbots.Tweetbot4" 609 | urlOutlines:@[ 610 | [EVKAction actionWithPattern:@".*twitter.com/[^/]+/?$" 611 | outline:@[ 612 | [EVKStaticStringPortion portionWithString:@"tweetbotbot:///user_profile/" percentEncodingIterations:0], 613 | [EVKTrimmedPathPortion portionWithPercentEncodingIterations:0], 614 | ]], 615 | [EVKAction actionWithPattern:@".*twitter.com/i/lists/.*$" 616 | outline:@[ 617 | [EVKStaticStringPortion portionWithString:@"tweetbot:///list/" percentEncodingIterations:0], 618 | [EVKRegexSubstitutionPortion portionWithRegex:@"^.*/i/lists/(\\d+).*$" 619 | template:@"$1" 620 | percentEncodingIterations:0], 621 | ]], 622 | [EVKAction actionWithPattern:@".*twitter.com/.+/status/.*$" 623 | outline:@[ 624 | [EVKStaticStringPortion portionWithString:@"tweetbot:///status/" percentEncodingIterations:0], 625 | [EVKRegexSubstitutionPortion portionWithRegex:@"^.*/[^/]+/status/(\\d+).*$" 626 | template:@"$1" 627 | percentEncodingIterations:0], 628 | ]], 629 | [EVKAction actionWithPattern:@".*twitter.com.*" 630 | outline:@[ 631 | [EVKStaticStringPortion portionWithString:@"tweetbotbot:///" percentEncodingIterations:0], 632 | [EVKTrimmedPathPortion portionWithPercentEncodingIterations:0], 633 | ]], 634 | ]]; 635 | for(NSString *url in urls) { 636 | NSLog(@"%@", [tweetbot transformURL:[NSURL URLWithString:url]]); 637 | } 638 | } 639 | 640 | // }}} 641 | 642 | @end 643 | --------------------------------------------------------------------------------