Skip to content

Commit 5c65157

Browse files
authored
Merge branch 'main' into dave-testing-matomo-code
2 parents 07e3bca + 0bb4e78 commit 5c65157

22 files changed

Lines changed: 449 additions & 75 deletions

.env.test

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
ALMA_OPENURL=https://na06.alma.exlibrisgroup.com/view/uresolver/01MIT_INST/openurl?
2+
TURNSTILE_SITEKEY=test-sitekey
3+
TURNSTILE_SECRET=test-secret
24
FEATURE_TIMDEX_FULLTEXT=true
35
FEATURE_GEODATA=false
46
MIT_PRIMO_URL=https://mit.primo.exlibrisgroup.com

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
44
ruby '3.4.8'
55

66
gem 'bootsnap', require: false
7+
gem 'crawler_detect'
78
gem 'graphql'
89
gem 'graphql-client'
910
gem 'http'
@@ -14,6 +15,7 @@ gem 'openssl'
1415
gem 'puma'
1516
gem 'rack-attack'
1617
gem 'rack-timeout'
18+
gem 'rails_cloudflare_turnstile'
1719
gem 'rails', '~> 7.2.0'
1820
gem 'redis'
1921
gem 'scout_apm'

Gemfile.lock

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ GEM
120120
bigdecimal
121121
rexml
122122
crass (1.0.6)
123+
crawler_detect (1.2.9)
124+
qonfig (>= 0.24)
123125
date (3.5.1)
124126
debug (1.11.1)
125127
irb (~> 1.10)
@@ -134,6 +136,12 @@ GEM
134136
drb (2.2.3)
135137
erb (5.1.3)
136138
erubi (1.13.1)
139+
faraday (2.14.1)
140+
faraday-net_http (>= 2.0, < 3.5)
141+
json
142+
logger
143+
faraday-net_http (3.4.2)
144+
net-http (~> 0.5)
137145
ffi (1.17.2-aarch64-linux-gnu)
138146
ffi (1.17.2-arm64-darwin)
139147
ffi (1.17.2-x86_64-darwin)
@@ -206,6 +214,8 @@ GEM
206214
mocha (2.8.2)
207215
ruby2_keywords (>= 0.0.5)
208216
msgpack (1.8.0)
217+
net-http (0.9.1)
218+
uri (>= 0.11.1)
209219
net-imap (0.5.13)
210220
date
211221
net-protocol
@@ -243,6 +253,8 @@ GEM
243253
public_suffix (6.0.2)
244254
puma (7.2.0)
245255
nio4r (~> 2.0)
256+
qonfig (0.30.0)
257+
base64 (>= 0.2)
246258
racc (1.8.1)
247259
rack (3.1.20)
248260
rack-attack (6.8.0)
@@ -276,6 +288,9 @@ GEM
276288
rails-html-sanitizer (1.7.0)
277289
loofah (~> 2.25)
278290
nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
291+
rails_cloudflare_turnstile (0.5.0)
292+
faraday (>= 1.0, < 3.0)
293+
rails (>= 6.0, < 8.2)
279294
railties (7.2.3)
280295
actionpack (= 7.2.3)
281296
activesupport (= 7.2.3)
@@ -381,6 +396,7 @@ GEM
381396
unicode-display_width (3.2.0)
382397
unicode-emoji (~> 4.1)
383398
unicode-emoji (4.2.0)
399+
uri (1.1.1)
384400
useragent (0.16.11)
385401
vcr (6.4.0)
386402
web-console (4.2.1)
@@ -421,6 +437,7 @@ DEPENDENCIES
421437
bootsnap
422438
capybara
423439
climate_control
440+
crawler_detect
424441
debug
425442
dotenv-rails
426443
graphql
@@ -437,6 +454,7 @@ DEPENDENCIES
437454
rack-attack
438455
rack-timeout
439456
rails (~> 7.2.0)
457+
rails_cloudflare_turnstile
440458
redis
441459
rubocop
442460
rubocop-rails

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ See `Optional Environment Variables` for more information.
9595
- `BOOLEAN_OPTIONS`: comma separated list of values to present to testers on instances where `BOOLEAN_PICKER` feature is enabled.
9696
- `FEATURE_BOOLEAN_PICKER`: feature to allow users to select their preferred boolean type. If set to `true`, feature is enabled. This feature is only intended for internal team
9797
testing and should never be enabled in production (mostly because the UI is a mess more than it would cause harm).
98+
- `FEATURE_BOT_DETECTION`: When set to `true`, enables bot detection using crawler_detect and Cloudflare Turnstile challenges for suspected bots on search result pages. Requires `TURNSTILE_SITEKEY` and `TURNSTILE_SECRET` to be set. If disabled, bots may crawl search results freely.
9899
- `FEATURE_GEODATA`: Enables features related to geospatial data discovery. Setting this variable to `true` will trigger geodata
99100
mode. Note that this is currently intended _only_ for the geodata app and
100101
may have unexpected consequences if applied to other TIMDEX UI apps.
@@ -146,6 +147,8 @@ instance is sending what search traffic. Defaults to "unset" if not defined.
146147
- `TIMDEX_INDEX`: Name of the index, or alias, to provide to the GraphQL endpoint. Defaults to `nil` which will let TIMDEX determine the best index to use. Wildcard values can be set, for example `rdi*` would search any indexes that begin with `rdi` in the underlying OpenSearch instance behind TIMDEX.
147148
- `TIMDEX_SOURCES`: Comma-separated list of sources to display in the advanced-search source selection element. This
148149
overrides the default which is set in ApplicationHelper.
150+
- `TURNSTILE_SECRET`: The Cloudflare Turnstile secret key used to verify challenge responses. If not set, bot challenge protection is disabled.
151+
- `TURNSTILE_SITEKEY`: The Cloudflare Turnstile site key used to render the challenge widget. If not set, bot challenge protection is disabled.
149152

150153
#### Test Environment-only Variables
151154

app/assets/stylesheets/partials/_pagination.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
padding-top: 1em;
77
border-top: 1px solid $color-border-default;
88
gap: 24px;
9+
margin-bottom: 48px;
910

1011
.center-elements {
1112
display: flex;

app/assets/stylesheets/partials/_results.scss

Lines changed: 100 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -397,7 +397,7 @@
397397
}
398398

399399
// -----------------------------
400-
// Result list and Sidebar - USE
400+
// Result list - USE
401401
// -----------------------------
402402

403403
#content-wrapper {
@@ -428,77 +428,18 @@
428428
flex-grow: 1;
429429
}
430430

431-
#results-sidebar {
432-
flex: 0 0 360px;
433-
434-
display: flex;
435-
flex-direction: column;
436-
gap: 32px;
437-
438-
> div {
439-
display: flex;
440-
gap: 12px;
441-
442-
i {
443-
font-size: 2rem;
444-
margin-top: 1px;
445-
}
446-
}
447-
448-
@media (max-width: 1140px) {
449-
flex-basis: 280px;
450-
}
451-
452-
p {
453-
line-height: 1.4;
454-
}
455-
456-
a {
457-
@include underlinedLinks;
458-
font-weight: $fw-medium;
459-
}
460-
}
461-
462431
// Put the sidebar below the results when the screen is narrower than 1024px
463432
@media (max-width: $bp-screen-lg) {
464433
flex-direction: column;
465-
gap: 32px;
466-
467-
// Put the sidebar items side by side when space is available
468-
#results-sidebar {
469-
flex-direction: row;
470-
padding-bottom: 40px;
471-
gap: 12px;
472-
flex-basis: auto;
473-
474-
> div {
475-
padding-right: 24px;
476-
}
477-
}
478-
479-
#callout-wrapper {
480-
margin-bottom: 0;
481-
}
482-
}
483-
484-
// Put sidebar items in a column below 768px
485-
@media (max-width: $bp-screen-md) {
486-
487-
#results-sidebar {
488-
flex-direction: column;
489-
gap: 32px;
490-
}
491-
434+
gap: 0px;
492435
}
493-
}
494-
495436
}
496437

497438
// ----------------------------
498439
// Result callout boxes
499440
// ----------------------------
500441

501-
#callout-wrapper {
442+
.callout-wrapper {
502443
display: flex;
503444
gap: 12px;
504445
margin-bottom: 32px;
@@ -514,6 +455,7 @@
514455
i {
515456
font-size: 2rem;
516457
margin-top: 1px;
458+
flex: 0 0 20px;
517459
}
518460

519461
p {
@@ -529,3 +471,99 @@
529471
flex-direction: column;
530472
}
531473
}
474+
475+
// Callout box responsive behavior
476+
// SIDEBAR COLLAPSES BELOW RESULTS
477+
@media (max-width: $bp-screen-lg) {
478+
// Hide the bordered callouts
479+
#results > .callout-wrapper {
480+
display: none;
481+
}
482+
}
483+
484+
485+
// -----------------------------
486+
// Result sidebar
487+
// -----------------------------
488+
489+
#results-layout-wrapper #results-sidebar {
490+
flex: 0 0 360px;
491+
492+
display: flex;
493+
flex-direction: column;
494+
gap: 32px;
495+
496+
.core-sidebar-items {
497+
display: flex;
498+
flex-direction: column;
499+
gap: 32px;
500+
501+
> div {
502+
display: flex;
503+
gap: 12px;
504+
505+
i {
506+
font-size: 2rem;
507+
margin-top: 1px;
508+
flex: 0 0 20px;
509+
}
510+
}
511+
}
512+
513+
@media (max-width: 1140px) {
514+
flex-basis: 280px;
515+
}
516+
517+
p {
518+
line-height: 1.4;
519+
}
520+
521+
a {
522+
@include underlinedLinks;
523+
font-weight: $fw-medium;
524+
}
525+
526+
// Override some of the styling of the callout boxes when they're in the sidebar.
527+
.callout-wrapper {
528+
aside {
529+
border: none;
530+
padding: 0;
531+
532+
a {
533+
text-decoration: none;
534+
}
535+
}
536+
537+
flex-direction: column;
538+
gap: 32px;
539+
border-bottom: 1px solid $color-border-default;
540+
padding-bottom: 32px;
541+
margin-bottom: 0;
542+
}
543+
544+
// Re-layout the sidebar contents when below results on smaller screens
545+
@media (max-width: $bp-screen-lg) {
546+
547+
flex-direction: column;
548+
549+
.callout-wrapper {
550+
flex-direction: row;
551+
}
552+
553+
.core-sidebar-items {
554+
flex-direction: row;
555+
}
556+
557+
}
558+
559+
@media (max-width: $bp-screen-sm2) {
560+
.callout-wrapper {
561+
flex-direction: column;
562+
}
563+
564+
.core-sidebar-items {
565+
flex-direction: column;
566+
}
567+
}
568+
}
569+
}

app/assets/stylesheets/partials/_variables.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ $container-max-width: 1280px;
116116

117117
// Additional breakpoints over mitlib-style
118118
$bp-screen-xs: 410px;
119+
$bp-screen-sm2: 640px;
119120

120121
// Left/Right Edge Padding
121122
$edge-padding-desktop: 32px;

app/controllers/search_controller.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ class SearchController < ApplicationController
22
before_action :validate_q!, only: %i[results]
33
before_action :validate_format_token, only: %i[results]
44
before_action :set_active_tab, only: %i[results]
5+
before_action :challenge_bots!, only: %i[results]
56
around_action :sleep_if_too_fast, only: %i[results]
67

78
before_action :validate_geobox_presence!, only: %i[results]
@@ -271,6 +272,15 @@ def validate_q!
271272
redirect_to root_url
272273
end
273274

275+
# Redirect suspected crawlers to Turnstile when the bot_detection feature is enabled.
276+
def challenge_bots!
277+
return unless Feature.enabled?(:bot_detection)
278+
return if session[:passed_turnstile]
279+
return unless BotDetector.should_challenge?(request)
280+
281+
redirect_to turnstile_path(return_to: request.fullpath)
282+
end
283+
274284
def validate_geodistance_presence!
275285
return unless Feature.enabled?(:geodata)
276286

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
class TurnstileController < ApplicationController
2+
before_action :validate_cloudflare_turnstile, only: :verify
3+
4+
rescue_from RailsCloudflareTurnstile::Forbidden, with: :handle_forbidden
5+
6+
def show
7+
@return_to = params[:return_to].presence || root_path
8+
end
9+
10+
def verify
11+
session[:passed_turnstile] = true
12+
redirect_to safe_return_path
13+
end
14+
15+
private
16+
17+
# Handles Turnstile rejecting token submission due to invalid token, network issue, etc.
18+
def handle_forbidden
19+
flash.now[:error] = "We couldn't complete the verification. Please try again."
20+
render :show, status: :unprocessable_entity
21+
end
22+
23+
# Returns a safe path to redirect to after Turnstile verification. Valid paths should begin with
24+
# a single slash. Falls back to root_path if the provided path is invalid.
25+
def safe_return_path
26+
return_to = params[:return_to].to_s
27+
return root_path if return_to.blank?
28+
return root_path if return_to.start_with?('//')
29+
return return_to if return_to.start_with?('/')
30+
31+
root_path
32+
end
33+
end

app/javascript/filters.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ function initFilterToggle() {
33
var filter_toggle = document.getElementById('filter-toggle');
44
var filter_panel = document.getElementById('filter-container');
55
var filter_categories = document.getElementsByClassName('filter-category');
6+
7+
// No need for event listeners if filters aren't present.
8+
if (!filter_toggle || !filter_panel) {
9+
return;
10+
}
11+
612
filter_toggle.addEventListener('click', event => {
713
filter_panel.classList.toggle('hidden-md');
814
filter_toggle.classList.toggle('expanded');

0 commit comments

Comments
 (0)