diff --git a/README.md b/README.md index 54de8dd..1038aaf 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ This library is divided into 7 parts, which are available as CocoaPods subspecs. * **Container** - This subspec provides a simple `ContainerViewController` without any built-in navigation construct. * **ActiveLabel** - This subspec provides a `UILabel` subclass that renders gradient "loading" animations while the label's `text` property is set to `nil`. * **Obfuscation** - This subspec provides simple routines to remove plaintext passwords or keys from your source code. +* **LanguageFilter** - This subspec provides an extension on `String` that can check if a given string has potentially offensive language in it. It can also replace potentially offensive language using a provided character. The offensive word list is ROT13ed so that this language won't appear in your codebase directly. ## Usage @@ -277,6 +278,20 @@ To use an obfuscated key in your code, create one and use the builder variables let key = ObfuscatedKey().T.h.i.s.underscore.I.s.dash.o.b.f.u.s.c.a.t.e.d.value ``` +### LanguageFilter + +To find if a string contains potentially objectionable input, create a filter and place a query. + +``` swift +// TODO +``` + +To replace all instances of potentially objectionable input within a string, create a filter and request a substitution + +``` swift +// TODO +``` + ## Example To run the example project, clone the repo, open `UtiliKit.xcworkspace`, and run the "UtiliKit-iOSExample" project. diff --git a/Sources/UtiliKit/Container/ContainerViewController.swift b/Sources/UtiliKit/Container/ContainerViewController.swift index 72e387e..f5b6866 100644 --- a/Sources/UtiliKit/Container/ContainerViewController.swift +++ b/Sources/UtiliKit/Container/ContainerViewController.swift @@ -66,7 +66,7 @@ open class ContainerViewController: UIViewController { } open func addManagedChildIfNeeded(_ child: ManagedChild) { - if !managedChildren.contains { $0.viewController === child.viewController } { + if !managedChildren.contains(where: { $0.viewController === child.viewController }) { managedChildren.insert(child, at: managedChildren.startIndex) } diff --git a/Sources/UtiliKit/LanguageFilter/String+LanguageFilter.swift b/Sources/UtiliKit/LanguageFilter/String+LanguageFilter.swift new file mode 100644 index 0000000..4482abf --- /dev/null +++ b/Sources/UtiliKit/LanguageFilter/String+LanguageFilter.swift @@ -0,0 +1,76 @@ +// +// String+LanguageFilter.swift +// UtiliKit-iOS +// +// Created by Russell Mirabelli on 6/19/20. +// Copyright © 2020 Bottle Rocket Studios. All rights reserved. +// + +import Foundation + +public extension String { + + /// Performs rot13 (dumb) encoding on a `UnicodeScalar` to provide minimal + /// filtering of bad words. + /// - Parameter unicodeScalar: The character to be `rot13`'ed + /// - Returns: the `rot13` value + private func rot13(unicodeScalar: UnicodeScalar) -> Character { + var result = unicodeScalar.value + + switch unicodeScalar { + case "A"..."M", "a"..."m": + result += 13 + case "N"..."Z", "n"..."z": + result -= 13 + default: + break + } + + return Character(UnicodeScalar(result) ?? " ") + } + + /// Encodes / decodes a `String` using rot13 + /// - Parameter input: The `String` to be encoded / decoded + /// - Returns: The encoded or decoded `String` + private func rot13(input: String) -> String { + return String(input.unicodeScalars.map(rot13)) + } + + /// A list of words which could be found to be offensive. Stored in source + /// using rot13. + private var offensiveWords: [String] { + ["nany", "nahf", "nefr", "nff", "onyyfnpx", "onyyf", "onfgneq", "ovgpu", "ovngpu", "oybbql", + "oybjwbo", "oybj wbo", "obyybpx", "obyybx", "obare", "obbo", "ohttre", "ohz", "ohgg", "ohggcyht", + "pyvgbevf", "pbpx", "pbba", "penc", "phag", "qnza", "qvpx", "qvyqb", "qlxr", "snt", "srpx", + "sryyngr", "sryyngvb", "srypuvat", "shpx", "s h p x", "shqtrcnpxre", "shqtr cnpxre", "synatr", + "Tbqqnza", "Tbq qnza", "uryy", "ubzb", "wrex", "wvmm", "xvxr", "xaboraq", "xabo raq", "ynovn", + "yznb", "yzsnb", "zhss", "avttre", "avttn", "bzt", "cravf", "cvff", "cbbc", "cevpx", "chor", + "chffl", "dhrre", "fpebghz", "frk", "fuvg", "f uvg", "fu1g", "fyhg", "fzrtzn", "fchax", "gvg", + "gbffre", "gheq", "gjng", "intvan", "jnax", "juber", "jgs" + ].map { rot13(input: $0) } + } + + /// Returns true if the `String` matches a set of potentially offensive + /// words. + var containsOffensiveLanguage: Bool { + !(offensiveWords.filter { self.contains($0) }.isEmpty) + } + + + /// Returns a `String` where potentially offensive language is replaced + /// with asterisks + var removingOffensiveLanguage: String { + self.replacingOffensiveWords(with: "*") + } + + /// Replaces offensive words with the provided `String` (a single character is recommended) + /// - Parameter replacement: The `String` to use for replacement + /// - Returns: The string with offensive words replaced. + func replacingOffensiveWords(with substitute: String) -> String { + if !self.containsOffensiveLanguage { return self } + var replacement = self + for word in offensiveWords { replacement = replacement.replacingOccurrences(of: word, with: String(repeating: (substitute.first ?? "*"), count: word.count)) } + return replacement + } + +} diff --git a/Tests/LanguageFilterTests.swift b/Tests/LanguageFilterTests.swift new file mode 100644 index 0000000..80577ad --- /dev/null +++ b/Tests/LanguageFilterTests.swift @@ -0,0 +1,61 @@ +// +// LanguageFilterTests.swift +// UtiliKit-iOSTests +// +// Created by Russell Mirabelli on 6/19/20. +// Copyright © 2020 Bottle Rocket Studios. All rights reserved. +// + +import UtiliKit +import XCTest + +class LanguageFilterTests: XCTestCase { + + func test_noOffensiveLanguageDoesNotReturnTrue() { + // arrange + let unoffensive = "angel" + // act + let containsOffensiveLanguage = unoffensive.containsOffensiveLanguage + // assert + XCTAssert(!containsOffensiveLanguage, "Should not trigger the offensive language filter") + } + + func test_yesOffensiveLanguageDoesReturnTrue() { + // arrange + let offensive = "ass" + // act + let containsOffensiveLanguage = offensive.containsOffensiveLanguage + // assert + XCTAssert(containsOffensiveLanguage, "Should not trigger the offensive language filter") + } + + func test_replacesWithCorrectLength() { + //arrange + let offensive = "bite my shiny metal ass" + // act + let replaced = offensive.removingOffensiveLanguage + let asterisks = replaced.filter { $0 == "*" } + // assert + XCTAssert(asterisks.count == 3, "Incorrect replacement length in \(replaced)") + } + + func test_doesNotReplaceInoffensive() { + // arrange + let unoffensive = "my hovercraft is full of eels" + // act + let replaced = unoffensive.removingOffensiveLanguage + // assert + XCTAssert(!replaced.contains("*"), "Incorrect replacement in \(replaced)") + } + + func test_replacesWithAlternateCharacter() { + //arrange + let offensive = "bite my shiny metal ass" + // act + let replaced = offensive.replacingOffensiveWords(with: "•") + let asterisks = replaced.filter { $0 == "•" } + // assert + XCTAssert(asterisks.count == 3, "Incorrect replacement length in \(replaced)") + } + +} diff --git a/UtiliKit.xcodeproj/project.pbxproj b/UtiliKit.xcodeproj/project.pbxproj index 73571b0..975ab4a 100644 --- a/UtiliKit.xcodeproj/project.pbxproj +++ b/UtiliKit.xcodeproj/project.pbxproj @@ -15,7 +15,6 @@ 0EFAFE9A21F81659005B2B16 /* ContainerViewControllerTransitionCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EFAFE9921F81659005B2B16 /* ContainerViewControllerTransitionCoordinator.swift */; }; 0EFAFE9D21F81743005B2B16 /* ContainerTransitionContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EFAFE9C21F81743005B2B16 /* ContainerTransitionContext.swift */; }; 0EFAFEA121FA0FA2005B2B16 /* ContainerPercentDrivenInteractiveTransitioner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EFAFEA021FA0FA2005B2B16 /* ContainerPercentDrivenInteractiveTransitioner.swift */; }; - 0EFAFEA221FBA89C005B2B16 /* UtiliKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8677313920601BB400C54343 /* UtiliKit.framework */; }; 1D419DBD21235EE100B62D91 /* ManagedChild.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D419DBC21235EE100B62D91 /* ManagedChild.swift */; }; 1D419DBF21236CC300B62D91 /* UtiliKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8677313920601BB400C54343 /* UtiliKit.framework */; }; 5C574BF72264EC10003D1641 /* ActiveLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C574BF62264EC10003D1641 /* ActiveLabel.swift */; }; @@ -27,6 +26,8 @@ 62F5256F2257F4FE0052B8E7 /* CHANGELOG.md in Resources */ = {isa = PBXBuildFile; fileRef = 62F5256E2257F4FE0052B8E7 /* CHANGELOG.md */; }; 7D5611B822E79AD40017008D /* ObfuscationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D5611B722E79AD40017008D /* ObfuscationTests.swift */; }; 7D5611BA22E79B4C0017008D /* ObfuscatedKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D5611B922E79B4C0017008D /* ObfuscatedKey.swift */; }; + 7D97C3C7249CF11600CB0E21 /* String+LanguageFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D97C3C6249CF11600CB0E21 /* String+LanguageFilter.swift */; }; + 7D97C3C9249CF21100CB0E21 /* LanguageFilterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D97C3C8249CF21100CB0E21 /* LanguageFilterTests.swift */; }; 8677314220601BB400C54343 /* UtiliKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8677313920601BB400C54343 /* UtiliKit.framework */; }; 8677315720601D1A00C54343 /* UtiliKitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 869826261FE87BFA0024B73D /* UtiliKitTests.swift */; }; 8677315820601D1A00C54343 /* DateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 869826271FE87BFA0024B73D /* DateTests.swift */; }; @@ -128,6 +129,8 @@ 62F5256E2257F4FE0052B8E7 /* CHANGELOG.md */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = net.daringfireball.markdown; path = CHANGELOG.md; sourceTree = ""; }; 7D5611B722E79AD40017008D /* ObfuscationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscationTests.swift; sourceTree = ""; }; 7D5611B922E79B4C0017008D /* ObfuscatedKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObfuscatedKey.swift; sourceTree = ""; }; + 7D97C3C6249CF11600CB0E21 /* String+LanguageFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+LanguageFilter.swift"; sourceTree = ""; }; + 7D97C3C8249CF21100CB0E21 /* LanguageFilterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageFilterTests.swift; sourceTree = ""; }; 8677313920601BB400C54343 /* UtiliKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UtiliKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8677314120601BB400C54343 /* UtiliKit-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "UtiliKit-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 8677315A20601DD800C54343 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -209,7 +212,6 @@ buildActionMask = 2147483647; files = ( 1D419DBF21236CC300B62D91 /* UtiliKit.framework in Frameworks */, - 0EFAFEA221FBA89C005B2B16 /* UtiliKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -284,6 +286,7 @@ children = ( 869826271FE87BFA0024B73D /* DateTests.swift */, 7D5611B722E79AD40017008D /* ObfuscationTests.swift */, + 7D97C3C8249CF21100CB0E21 /* LanguageFilterTests.swift */, 869826261FE87BFA0024B73D /* UtiliKitTests.swift */, 0ED9476220FD461300B642AB /* ContainerTests.swift */, 5C574BFA2264ED61003D1641 /* ActiveLabelTests.swift */, @@ -300,6 +303,14 @@ path = Obfuscation; sourceTree = ""; }; + 7D97C3C5249CEF8900CB0E21 /* LanguageFilter */ = { + isa = PBXGroup; + children = ( + 7D97C3C6249CF11600CB0E21 /* String+LanguageFilter.swift */, + ); + path = LanguageFilter; + sourceTree = ""; + }; 8677315920601DD800C54343 /* Supporting Files */ = { isa = PBXGroup; children = ( @@ -333,6 +344,7 @@ 8698CFA220619EAD0065AE20 /* Container */, 8698CFA720619EAD0065AE20 /* General */, 8698CFAA20619EAD0065AE20 /* Instantiation */, + 7D97C3C5249CEF8900CB0E21 /* LanguageFilter */, 7D5611B622E79AAE0017008D /* Obfuscation */, 8698CFB220619EAD0065AE20 /* TimelessDate */, 8698CFB520619EAD0065AE20 /* Version */, @@ -556,7 +568,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 0930; + LastUpgradeCheck = 1150; ORGANIZATIONNAME = "Bottle Rocket Studios"; TargetAttributes = { 8677313820601BB400C54343 = { @@ -648,6 +660,7 @@ 8698CFC320619EAD0065AE20 /* StoryboardIdentifiable.swift in Sources */, 7D5611BA22E79B4C0017008D /* ObfuscatedKey.swift in Sources */, 8698CFC920619EAD0065AE20 /* Bundle+Extensions.swift in Sources */, + 7D97C3C7249CF11600CB0E21 /* String+LanguageFilter.swift in Sources */, 8698CFC520619EAD0065AE20 /* UIStoryboard+Extensions.swift in Sources */, 8698CFC220619EAD0065AE20 /* ReuseIdentifiable.swift in Sources */, 8698CFC620619EAD0065AE20 /* UITableView+Extensions.swift in Sources */, @@ -671,6 +684,7 @@ buildActionMask = 2147483647; files = ( 5C574BFB2264ED61003D1641 /* ActiveLabelTests.swift in Sources */, + 7D97C3C9249CF21100CB0E21 /* LanguageFilterTests.swift in Sources */, 0ED9476420FD473700B642AB /* ContainerTests.swift in Sources */, 8677315720601D1A00C54343 /* UtiliKitTests.swift in Sources */, 8677315820601D1A00C54343 /* DateTests.swift in Sources */, @@ -860,7 +874,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = dwarf; @@ -894,7 +908,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = ""; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES;