Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//
// Constants.swift
// SelfControl
//
// Created by Satendra Singh on 09/10/25.
//



enum Constants {
/// File name for the JSON file with Safari rules.
static let SAFARI_BLOCKER_FILE_NAME = "blockerList.json"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
//
// ContentBlockerExtensionRequestHandler.swift
// SelfControl
//
// Created by Satendra Singh on 09/10/25.
//

import os.log
import Foundation

public enum ContentBlockerExtensionRequestHandler {
/// Handles content blocking extension request for rules.
///
/// This method loads the content blocker rules JSON file from the shared container
/// and attaches it to the extension context to be used by Safari.
///
/// - Parameters:
/// - context: The extension context that initiated the request.
/// - groupIdentifier: The app group identifier used to access the shared container.
public static func handleRequest(with context: NSExtensionContext, groupIdentifier: String) {
os_log(.info, "[SC] 🔍] Safari Start loading the content blocker, %{public}@", context.inputItems.description)

// Get the shared container URL using the provided group identifier
guard
let appGroupURL = FileManager.default.containerURL(
forSecurityApplicationGroupIdentifier: groupIdentifier
)
else {
context.cancelRequest(
withError: createError(code: 1001, message: "Failed to access App Group container.")
)
return
}

// Construct the path to the shared blocker list file
let sharedFileURL = appGroupURL.appendingPathComponent(Constants.SAFARI_BLOCKER_FILE_NAME)

// Determine which blocker list file to use
var blockerListFileURL = sharedFileURL
if !FileManager.default.fileExists(atPath: sharedFileURL.path) {
os_log(.info, "[SC] 🔍] Safari No blocker list file found. Using the default one.")

// Fall back to the default blocker list included in the bundle
guard
let defaultURL = Bundle.main.url(forResource: "blockerList", withExtension: "json")
else {
context.cancelRequest(
withError: createError(
code: 1002,
message: "[SC] 🔍] Safari Failed to find default blocker list."
)
)
return
}
blockerListFileURL = defaultURL
}

// Create an attachment with the blocker list file
guard let attachment = NSItemProvider(contentsOf: blockerListFileURL) else {
context.cancelRequest(
withError: createError(code: 1003, message: "Failed to create attachment.")
)
return
}

// Prepare and complete the extension request with the blocker list
let item = NSExtensionItem()
item.attachments = [attachment]
// item.attributedTitle = NSAttributedString(string: "Hellow world!")
// item.attributedContentText = NSAttributedString(string: "Hello Content of the world!")

context.completeRequest(
returningItems: [item]
) { _ in
os_log(.info, "[SC] 🔍] Safari Finished loading the content blocker")
}
}

/// Creates an NSError with the specified code and message.
///
/// - Parameters:
/// - code: The error code.
/// - message: The error message.
/// - Returns: An NSError object with the specified parameters.
private static func createError(code: Int, message: String) -> NSError {
return NSError(
domain: "extension request handler",
code: code,
userInfo: [NSLocalizedDescriptionKey: message]
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// ContentBlockerRequestHandler.swift
// SelfControl Safari Extension
//
// Created by Satendra Singh on 07/10/25.
//

import Foundation
import UniformTypeIdentifiers
import os.log

class ContentBlockerRequestHandler: NSObject, NSExtensionRequestHandling {
// Must match the App Group used by the host app
private let appGroup = "group.com.application.SelfControl.corebits"
private let sharedFileName = "blockerList.json"

func beginRequest(with context: NSExtensionContext) {
ContentBlockerExtensionRequestHandler.handleRequest(with: context, groupIdentifier: appGroup)
return
let item = NSExtensionItem()

// Try to load rules from the shared App Group container first
if let sharedFileURL = sharedRulesFileURL(), FileManager.default.fileExists(atPath: sharedFileURL.path) {
let provider = NSItemProvider(contentsOf: sharedFileURL)!
item.attachments = [provider]
context.completeRequest(returningItems: [item], completionHandler: nil)
return
}

// Fallback: load bundled blockerList.json from the extension resources
if let bundledURL = Bundle.main.url(forResource: "blockerList", withExtension: "json") {
let provider = NSItemProvider(contentsOf: bundledURL)!
item.attachments = [provider]
context.completeRequest(returningItems: [item], completionHandler: nil)
return
}

// If neither exists, return an empty rules array to avoid errors
// let emptyRulesData = Data("[]".utf8)
// let tmpURL = writeTempData(emptyRulesData, suggestedName: sharedFileName)
// let provider = NSItemProvider(contentsOf: tmpURL)!
// item.attachments = [provider]
// context.completeRequest(returningItems: [item], completionHandler: nil)
}
Comment on lines +17 to +44
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove unreachable code after early return.

Lines 20-43 are unreachable because of the return statement on line 19. This dead code duplicates logic already present in ContentBlockerExtensionRequestHandler.handleRequest and should be removed.

     func beginRequest(with context: NSExtensionContext) {
         ContentBlockerExtensionRequestHandler.handleRequest(with: context, groupIdentifier: appGroup)
-        return
-        let item = NSExtensionItem()
-
-        // Try to load rules from the shared App Group container first
-        if let sharedFileURL = sharedRulesFileURL(), FileManager.default.fileExists(atPath: sharedFileURL.path) {
-            let provider = NSItemProvider(contentsOf: sharedFileURL)!
-            item.attachments = [provider]
-            context.completeRequest(returningItems: [item], completionHandler: nil)
-            return
-        }
-
-        // Fallback: load bundled blockerList.json from the extension resources
-        if let bundledURL = Bundle.main.url(forResource: "blockerList", withExtension: "json") {
-            let provider = NSItemProvider(contentsOf: bundledURL)!
-            item.attachments = [provider]
-            context.completeRequest(returningItems: [item], completionHandler: nil)
-            return
-        }
-
-        // If neither exists, return an empty rules array to avoid errors
-//        let emptyRulesData = Data("[]".utf8)
-//        let tmpURL = writeTempData(emptyRulesData, suggestedName: sharedFileName)
-//        let provider = NSItemProvider(contentsOf: tmpURL)!
-//        item.attachments = [provider]
-//        context.completeRequest(returningItems: [item], completionHandler: nil)
     }
🤖 Prompt for AI Agents
In Self-Control-Extension/SelfControl/SelfControl Safari
Extension/ContentBlockerRequestHandler.swift around lines 17 to 44, there is
dead/unreachable code after an early return in beginRequest (the return on line
19 makes lines 20-43 unreachable); remove the duplicated fallback logic and
commented temp-data block that follow the return so beginRequest only delegates
to ContentBlockerExtensionRequestHandler.handleRequest(with:groupIdentifier:)
and returns, keeping the function minimal and eliminating the unreachable code.


// MARK: - Helpers

private func sharedRulesFileURL() -> URL? {
let fileManager = FileManager.default
guard let containerURL = fileManager.containerURL(forSecurityApplicationGroupIdentifier: appGroup) else {
NSLog("❌ App Group container not found for %@", appGroup)
os_log("[SC] 🔍] Safari ❌ App Group container not found for: %{public}@", appGroup)
return nil
}
return containerURL.appendingPathComponent(sharedFileName)
}

private func writeTempData(_ data: Data, suggestedName: String) -> URL {
let tmpDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
let url = tmpDir.appendingPathComponent(suggestedName)
do {
try data.write(to: url, options: .atomic)
} catch {
NSLog("❌ Failed to write temporary rules file: \(error.localizedDescription)")
os_log("[SC] 🔍] Safari ❌ Failed to write temporary rules file: %{public}@", error.localizedDescription)

}
return url
}


func cancelRequest(withError error: any Error) {

os_log("[SC] 🔍] Safari ❌ cancelRequeste: %{public}@", error.localizedDescription)

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.Safari.content-blocker</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ContentBlockerRequestHandler</string>
</dict>
<key>NSHumanReadableDescription</key>
<string>Add a description of what your extension does here.</string>
Comment on lines +12 to +13
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace placeholder description before release.

The NSHumanReadableDescription contains a generic placeholder that should be replaced with a meaningful description of what the SelfControl content blocker does.

Apply this diff:

 	<key>NSHumanReadableDescription</key>
-	<string>Add a description of what your extension does here.</string>
+	<string>Blocks distracting websites to help you stay focused and productive.</string>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<key>NSHumanReadableDescription</key>
<string>Add a description of what your extension does here.</string>
<key>NSHumanReadableDescription</key>
<string>Blocks distracting websites to help you stay focused and productive.</string>
🤖 Prompt for AI Agents
In Self-Control-Extension/SelfControl/SelfControl Safari Extension/Info.plist
around lines 12 to 13, the NSHumanReadableDescription value is a placeholder;
replace the generic "Add a description of what your extension does here." with a
concise, user-facing description of the extension’s behavior (e.g., that
SelfControl is a content blocker that prevents access to user-configured
distracting sites and trackers during focus periods), ensuring the new string
accurately and clearly describes the app for App Store/review and localization
if needed.

</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.application.SelfControl.corebits</string>
</array>
</dict>
</plist>
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Site Blocked</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
background: #fafafa;
text-align: center;
padding: 80px 20px;
}
h1 { color: #e74c3c; }
p { color: #333; font-size: 17px; }
a {
display: inline-block;
margin-top: 30px;
text-decoration: none;
background: #007aff;
color: white;
padding: 10px 18px;
border-radius: 8px;
}
</style>
</head>
<body>
<h1>🚫 Site Blocked</h1>
<p>This website has been blocked by your Safari content blocker.</p>
<p>You can change this in <strong>Safari ▸ Extensions</strong> if you want to allow it.</p>
<a href="safari://extensions">Open Safari Extensions</a>
Comment on lines +28 to +31
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify the Safari extensions URL scheme.

The page uses safari://extensions to link to Safari's extensions preferences. Ensure this URL scheme is valid and will work on all supported macOS versions.


🌐 Web query:

Is safari://extensions a valid URL scheme for opening Safari Extensions preferences on macOS?

💡 Result:

No. Safari does not document a safari://extensions URL scheme. To open Safari's Extensions pane Apple instructs using the Safari menu (Safari > Settings/Preferences > Extensions) or Safari > Safari Extensions to open the App Store. [1][2]

Sources:
[1] Apple Support — "Use Safari extensions on your Mac."
[2] Apple Support — "If you can’t turn on extensions in Safari for Mac."


Remove the non-functional safari://extensions link.

Safari does not document a safari://extensions URL scheme. The link in line 31 will not work. To direct users to Safari extensions, Apple instructs using the Safari menu (Safari > Settings/Preferences > Extensions) or Safari > Safari Extensions to open the App Store. Either remove the link or replace it with clear text instructions.

🤖 Prompt for AI Agents
In Self-Control-Extension/SelfControl/SelfControl Safari Extension/block.html
around lines 28 to 31, the anchor using the non-functional safari://extensions
URL should be removed or replaced; update the markup so the clickable link is
removed and instead provide plain text instructions like "Open Safari ▸
Settings/Preferences ▸ Extensions" (or "Safari ▸ Safari Extensions" to open the
App Store) so users are guided correctly, ensuring no invalid URL remains.

</body>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
[
{
"action": {
"type": "block"
},
"trigger": {
"url-filter": "facebook.com/friends"
}
},
{
"action": {
"type": "block"
},
"trigger": {
"url-filter": "facebook.com/marketplace"
}
},
{
"action": {
"type": "block"
},
"trigger": {
"url-filter": "facebook.com/watch"
}
}
]
//[
// {
// "trigger" : {
// "url_filter" : "^https?:\/\/(www\\.)?facebook\\.com\/friends.*"
// },
// "action" : {
// "type" : "block"
// }
// },
// {
// "trigger" : {
// "url_filter" : "^https?:\/\/(www\\.)?linkedin\\.com\/mynetwork.*"
// },
// "action" : {
// "type" : "block"
// }
// },
// {
// "trigger" : {
// "url_filter" : "^https?:\/\/(www\\.)?linkedin\\.com\/jobs.*"
// },
// "action" : {
// "type" : "block"
// }
// },
// {
// "trigger" : {
// "url_filter" : "^https?:\/\/(www\\.)?facebook\\.com\/marketplace.*"
// },
// "action" : {
// "type" : "block"
// }
// }
//]
Comment on lines +27 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Remove commented-out JSON code.

JSON files cannot contain comments. Lines 27-60 contain commented-out rules that will either cause parsing errors or be silently ignored. If you need to preserve these alternative regex-based rules for reference, move them to documentation or a separate example file.

Apply this diff to remove the invalid comments:

     }
 ]
-//[
-//  {
-//    "trigger" : {
-//      "url_filter" : "^https?:\/\/(www\\.)?facebook\\.com\/friends.*"
-//    },
-//    "action" : {
-//      "type" : "block"
-//    }
-//  },
-//  {
-//    "trigger" : {
-//      "url_filter" : "^https?:\/\/(www\\.)?linkedin\\.com\/mynetwork.*"
-//    },
-//    "action" : {
-//      "type" : "block"
-//    }
-//  },
-//  {
-//    "trigger" : {
-//      "url_filter" : "^https?:\/\/(www\\.)?linkedin\\.com\/jobs.*"
-//    },
-//    "action" : {
-//      "type" : "block"
-//    }
-//  },
-//  {
-//    "trigger" : {
-//      "url_filter" : "^https?:\/\/(www\\.)?facebook\\.com\/marketplace.*"
-//    },
-//    "action" : {
-//      "type" : "block"
-//    }
-//  }
-//]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//[
// {
// "trigger" : {
// "url_filter" : "^https?:\/\/(www\\.)?facebook\\.com\/friends.*"
// },
// "action" : {
// "type" : "block"
// }
// },
// {
// "trigger" : {
// "url_filter" : "^https?:\/\/(www\\.)?linkedin\\.com\/mynetwork.*"
// },
// "action" : {
// "type" : "block"
// }
// },
// {
// "trigger" : {
// "url_filter" : "^https?:\/\/(www\\.)?linkedin\\.com\/jobs.*"
// },
// "action" : {
// "type" : "block"
// }
// },
// {
// "trigger" : {
// "url_filter" : "^https?:\/\/(www\\.)?facebook\\.com\/marketplace.*"
// },
// "action" : {
// "type" : "block"
// }
// }
//]
}
}
]
🧰 Tools
🪛 Biome (2.1.2)

[error] 26-60: End of file expected

Use an array for a sequence of values: [1, 2]

(parse)

🤖 Prompt for AI Agents
In Self-Control-Extension/SelfControl/SelfControl Safari
Extension/blockerList.json around lines 27 to 60, there is a block of
commented-out JSON (//[...] entries) which is invalid in JSON; remove that
entire commented section from the file (or move it into a separate text/example
file such as docs/blockerList_examples.json) so the file contains only valid
JSON; after removal, validate the JSON structure (no stray commas, matching
brackets) to ensure the blocker list parses correctly.

Loading