jekyll-webmention_io 3.3.7 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,16 +15,30 @@ require "json"
15
15
  require "net/http"
16
16
  require "uri"
17
17
  require "openssl"
18
- require "string_inflection"
18
+ require "active_support"
19
19
  require "indieweb/endpoints"
20
20
  require "webmention"
21
21
 
22
22
  module Jekyll
23
23
  module WebmentionIO
24
+ module UriState
25
+ UNSUPPORTED = "unsupported"
26
+ ERROR = "error"
27
+ FAILURE = "failure"
28
+ SUCCESS = "success"
29
+ end
30
+
31
+ module UriPolicy
32
+ BAN = "ban"
33
+ IGNORE = "ignore"
34
+ RETRY = "retry"
35
+ end
36
+
24
37
  class << self
25
38
  # define simple getters and setters
26
39
  attr_reader :config, :jekyll_config, :cache_files, :cache_folder,
27
- :file_prefix, :types, :supported_templates, :js_handler
40
+ :file_prefix, :types, :supported_templates, :js_handler,
41
+ :uri_whitelist, :uri_blacklist
28
42
  attr_writer :api_suffix
29
43
  end
30
44
 
@@ -69,6 +83,24 @@ module Jekyll
69
83
  end
70
84
 
71
85
  @js_handler = WebmentionIO::JSHandler.new(site)
86
+
87
+ @uri_whitelist = @config
88
+ .fetch("bad_uri_policy", {})
89
+ .fetch("whitelist", [])
90
+ .clone
91
+ .insert(-1, "^https?://webmention.io/")
92
+ .map { |expr| Regexp.new(expr) }
93
+
94
+ @uri_blacklist = @config
95
+ .fetch("bad_uri_policy", {})
96
+ .fetch("blacklist", [])
97
+ .map { |expr| Regexp.new(expr) }
98
+
99
+ # Backward compatibility config for html_proofer setting
100
+
101
+ if @config['html_proofer'] == true
102
+ @config['html_proofer_ignore'] = "templates"
103
+ end
72
104
  end
73
105
 
74
106
  # Setter
@@ -81,6 +113,10 @@ module Jekyll
81
113
  Jekyll.sanitized_path(@cache_folder, "#{@file_prefix}#{filename}")
82
114
  end
83
115
 
116
+ def self.max_attempts()
117
+ @config.dig("max_attempts")
118
+ end
119
+
84
120
  def self.get_cache_file_path(key)
85
121
  @cache_files[key] || false
86
122
  end
@@ -213,11 +249,11 @@ module Jekyll
213
249
  endpoint = IndieWeb::Endpoints.get(uri)[:webmention]
214
250
  unless endpoint
215
251
  log("info", "Could not find a webmention endpoint at #{uri}")
216
- uri_is_not_ok(uri)
252
+ update_uri_cache(uri, UriState::UNSUPPORTED)
217
253
  end
218
254
  rescue StandardError => e
219
255
  log "info", "Endpoint lookup failed for #{uri}: #{e.message}"
220
- uri_is_not_ok(uri)
256
+ update_uri_cache(uri, UriState::FAILURE)
221
257
  endpoint = false
222
258
  end
223
259
  endpoint
@@ -231,10 +267,24 @@ module Jekyll
231
267
  case response.code
232
268
  when 200, 201, 202
233
269
  log "info", "Webmention successful!"
270
+ update_uri_cache(target, UriState::SUCCESS)
234
271
  response.body
235
272
  else
236
273
  log "info", response.inspect
237
274
  log "info", "Webmention failed, but will remain queued for next time"
275
+
276
+ if response.body
277
+ begin
278
+ body = JSON.parse(response.body)
279
+
280
+ if body.key? "error"
281
+ log "msg", "Endpoint returned error: #{body['error']}"
282
+ end
283
+ rescue
284
+ end
285
+ end
286
+
287
+ update_uri_cache(target, UriState::ERROR)
238
288
  false
239
289
  end
240
290
  end
@@ -260,7 +310,8 @@ module Jekyll
260
310
  end
261
311
 
262
312
  def self.html_templates
263
- proofer = if @config['html_proofer'] == true
313
+ setting = @config['html_proofer_ignore']
314
+ proofer = if setting == "all" || setting == "templates"
264
315
  ' data-proofer-ignore'
265
316
  else
266
317
  ''
@@ -291,12 +342,12 @@ module Jekyll
291
342
  redirect_to = redirect_to.relative? ? "#{original_uri.scheme}://#{original_uri.host}" + redirect_to.to_s : redirect_to.to_s
292
343
  return get_uri_source(redirect_to, redirect_limit - 1, original_uri)
293
344
  else
294
- uri_is_not_ok(uri)
345
+ update_uri_cache(uri, UriState::FAILURE)
295
346
  return false
296
347
  end
297
348
  else
298
349
  log("warn", "too many redirects for #{original_uri}") if original_uri
299
- uri_is_not_ok(uri)
350
+ update_uri_cache(uri, UriState::FAILURE)
300
351
  return false
301
352
  end
302
353
  end
@@ -347,37 +398,176 @@ module Jekyll
347
398
  return response
348
399
  rescue *EXCEPTIONS => e
349
400
  log "warn", "Got an error checking #{uri}: #{e}"
350
- uri_is_not_ok(uri)
401
+ update_uri_cache(uri, UriState::FAILURE)
351
402
  return false
352
403
  end
353
404
  end
354
405
 
355
- # Cache bad URLs for a bit
356
- def self.uri_is_not_ok(uri)
406
+ # Given the provided state value (see UriState), retrieve the policy
407
+ # entry. If no entry exists, return a new default entry that
408
+ # indicates unlimited retries.
409
+ def self.get_bad_uri_policy_entry(state)
410
+ settings = @config.fetch("bad_uri_policy", {})
411
+
412
+ default_policy = { "policy" => UriPolicy::RETRY }
413
+ policy_entry = nil
414
+
415
+ # Retrieve the policy entry, the default entry, or the canned default
416
+ policy_entry = settings.fetch(state) {
417
+ settings.fetch("default", default_policy)
418
+ }
419
+
420
+ # Convert shorthand entry to full policy record
421
+ if policy_entry.instance_of? String
422
+ policy_entry = { "policy" => policy_entry }
423
+ end
424
+
425
+ if policy_entry["policy"] == UriPolicy::RETRY and ! policy_entry.key? "retry_delay"
426
+ # If this is a retry policy and no delay is set, set up the default
427
+ # delay policy. This inherits from the legacy cache_bad_uris_for
428
+ # setting to enable backward compatibility with older configurations.
429
+ #
430
+ # We do this here to make the rule enforcement logic a little tidier.
431
+
432
+ policy_entry["retry_delay"] = [ @config.fetch("cache_bad_uris_for", 1) * 24 ]
433
+ end
434
+
435
+ return policy_entry
436
+ end
437
+
438
+ # Retrieve the bad_uris cache entry for the given URI. This method
439
+ # takes the cache and a URI instance (i.e. parsing must already be done).
440
+ #
441
+ # If the URI has no entry in the cache, returns nil and *not* a default
442
+ # entry.
443
+ def self.get_bad_uri_cache_entry(bad_uris, uri)
444
+ return nil if ! bad_uris.key? uri.host
445
+
446
+ entry = bad_uris[uri.host].clone
447
+
448
+ if entry.instance_of? String
449
+ # Older version of the bad URL cache, convert to new format with some
450
+ # "sensible" defaults.
451
+
452
+ entry = {
453
+ "state" => UriState::UNSUPPORTED,
454
+ "last_checked" => DateTime.parse(entry).to_time,
455
+ "attempts" => 1
456
+ }
457
+ else
458
+ # Otherwise, parse the check time into a real Time object before
459
+ # returning the entry.
460
+ #
461
+ # We convert to a Time object so we can do arithmetic on it later.
462
+
463
+ entry["last_checked"] = DateTime.parse(entry["last_checked"]).to_time
464
+ end
465
+
466
+ return entry
467
+ end
468
+
469
+ # Update the URI cache for this entry.
470
+ #
471
+ # If the state is UriState.SUCCESS or the URI is whitelisted or
472
+ # blacklisted, we delete any existing entries since no policy will
473
+ # apply. This ensures we reset the policy state when a webmention
474
+ # succeeds.
475
+ #
476
+ # Otherwise, we either create or update an entry for the URI, recording
477
+ # the state and the current attempt counter.
478
+ def self.update_uri_cache(uri, state)
357
479
  uri = URI::Parser.new.parse(uri.to_s)
358
- # Never cache webmention.io in here
359
- return if uri.host == "webmention.io"
480
+ uri_str = uri.to_s
360
481
 
361
482
  cache_file = @cache_files["bad_uris"]
362
483
  bad_uris = load_yaml(cache_file)
363
- bad_uris[uri.host] = Time.now.to_s
484
+
485
+ if state == UriState::SUCCESS or
486
+ @uri_whitelist.any? { |expr| expr.match uri_str } or
487
+ @uri_blacklist.any? { |expr| expr.match uri_str }
488
+
489
+ return if bad_uris.delete(uri.host).nil?
490
+ else
491
+ old_entry = get_bad_uri_cache_entry(bad_uris, uri) || {}
492
+
493
+ bad_uris[uri.host] = {
494
+ "state" => state,
495
+ "attempts" => old_entry.fetch("attempts", 0) + 1,
496
+ "last_checked" => Time.now.to_s
497
+ }
498
+ end
499
+
364
500
  dump_yaml(cache_file, bad_uris)
365
501
  end
366
502
 
503
+ # Check if we should attempt to send a webmention to the given URI based
504
+ # on the error handling policy and the last attempt.
367
505
  def self.uri_ok?(uri)
368
506
  uri = URI::Parser.new.parse(uri.to_s)
369
507
  now = Time.now.to_s
508
+ uri_str = uri.to_s
509
+
510
+ # If the URI is whitelisted, it's always ok!
511
+ return true if @uri_whitelist.any? { |expr| expr.match uri_str }
512
+
513
+ # If the URI is blacklisted, it's never ok!
514
+ return false if @uri_blacklist.any? { |expr| expr.match uri_str }
515
+
370
516
  bad_uris = load_yaml(@cache_files["bad_uris"])
371
- if bad_uris.key? uri.host
372
- last_checked = DateTime.parse(bad_uris[uri.host])
373
- cache_bad_uris_for = @config["cache_bad_uris_for"] || 1 # in days
374
- recheck_at = last_checked.next_day(cache_bad_uris_for).to_s
375
- return false if recheck_at > now
517
+ entry = get_bad_uri_cache_entry(bad_uris, uri)
518
+
519
+ # If the entry isn't in our cache yet, then it's ok.
520
+ return true if entry.nil?
521
+
522
+ # Okay, the last time we tried to send a webmention to this URI it
523
+ # failed, so depending on what happened and the policy, we need to
524
+ # decide what to do.
525
+ #
526
+ # First pull the retry policy given the type of the last error for the URI
527
+ policy_entry = get_bad_uri_policy_entry(entry["state"])
528
+ policy = policy_entry["policy"]
529
+
530
+ if policy == UriPolicy::BAN
531
+ return false
532
+ elsif policy == UriPolicy::IGNORE
533
+ return true
534
+ elsif policy == UriPolicy::RETRY
535
+ now = Time.now
536
+
537
+ attempts = entry["attempts"]
538
+ max_attempts = policy_entry["max_attempts"]
539
+
540
+ if ! max_attempts.nil? and attempts >= max_attempts
541
+ # If there's a retry limit and we've hit it, URI is not ok.
542
+ log "msg", "Skipping #{uri}, attempted #{attempts} times and max is #{max_attempts}"
543
+
544
+ return false
545
+ end
546
+
547
+ retry_delay = policy_entry["retry_delay"]
548
+
549
+ # Sneaky trick. By clamping to the array length, the last entry in
550
+ # the retry_delay list is used for all remaining retries.
551
+ delay = retry_delay[(attempts - 1).clamp(0, retry_delay.length - 1)]
552
+
553
+ recheck_at = (entry["last_checked"] + delay * 3600)
554
+
555
+ if recheck_at.to_r > now.to_r
556
+ log "msg", "Skipping #{uri}, next attempt will happen after #{recheck_at}"
557
+
558
+ return false
559
+ end
560
+ else
561
+ log "error", "Invalid bad URI policy type: #{policy}"
376
562
  end
563
+
377
564
  return true
378
565
  end
379
566
 
380
- private_class_method :get_http_response, :uri_is_not_ok
567
+ private_class_method :get_http_response,
568
+ :get_bad_uri_policy_entry,
569
+ :get_bad_uri_cache_entry,
570
+ :update_uri_cache
381
571
  end
382
572
  end
383
573
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jekyll-webmention_io
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.7
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aaron Gustafson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-02-19 00:00:00.000000000 Z
11
+ date: 2025-08-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: jekyll
@@ -65,19 +65,25 @@ dependencies:
65
65
  - !ruby/object:Gem::Version
66
66
  version: '4.0'
67
67
  - !ruby/object:Gem::Dependency
68
- name: string_inflection
68
+ name: activesupport
69
69
  requirement: !ruby/object:Gem::Requirement
70
70
  requirements:
71
71
  - - "~>"
72
72
  - !ruby/object:Gem::Version
73
- version: '0.1'
73
+ version: '7.0'
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: 7.0.4.3
74
77
  type: :runtime
75
78
  prerelease: false
76
79
  version_requirements: !ruby/object:Gem::Requirement
77
80
  requirements:
78
81
  - - "~>"
79
82
  - !ruby/object:Gem::Version
80
- version: '0.1'
83
+ version: '7.0'
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 7.0.4.3
81
87
  - !ruby/object:Gem::Dependency
82
88
  name: htmlbeautifier
83
89
  requirement: !ruby/object:Gem::Requirement
@@ -120,6 +126,20 @@ dependencies:
120
126
  - - "~>"
121
127
  - !ruby/object:Gem::Version
122
128
  version: '7.0'
129
+ - !ruby/object:Gem::Dependency
130
+ name: jsonpath
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - "~>"
134
+ - !ruby/object:Gem::Version
135
+ version: 1.0.1
136
+ type: :runtime
137
+ prerelease: false
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - "~>"
141
+ - !ruby/object:Gem::Version
142
+ version: 1.0.1
123
143
  - !ruby/object:Gem::Dependency
124
144
  name: bundler
125
145
  requirement: !ruby/object:Gem::Requirement
@@ -154,14 +174,14 @@ dependencies:
154
174
  requirements:
155
175
  - - "~>"
156
176
  - !ruby/object:Gem::Version
157
- version: '12.0'
177
+ version: '13.0'
158
178
  type: :development
159
179
  prerelease: false
160
180
  version_requirements: !ruby/object:Gem::Requirement
161
181
  requirements:
162
182
  - - "~>"
163
183
  - !ruby/object:Gem::Version
164
- version: '12.0'
184
+ version: '13.0'
165
185
  - !ruby/object:Gem::Dependency
166
186
  name: rspec
167
187
  requirement: !ruby/object:Gem::Requirement
@@ -297,7 +317,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
297
317
  - !ruby/object:Gem::Version
298
318
  version: '0'
299
319
  requirements: []
300
- rubygems_version: 3.3.3
320
+ rubygems_version: 3.4.22
301
321
  signing_key:
302
322
  specification_version: 4
303
323
  summary: A Jekyll plugin for sending & receiving webmentions via Webmention.io.