metainspector 5.18.0 → 5.19.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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +15 -0
- data/.github/dependabot.yml +1 -1
- data/.gitignore +1 -1
- data/CHANGELOG.md +15 -0
- data/README.md +56 -1
- data/lib/meta_inspector/document.rb +17 -3
- data/lib/meta_inspector/parser.rb +3 -2
- data/lib/meta_inspector/parsers/images.rb +17 -13
- data/lib/meta_inspector/parsers/links.rb +2 -1
- data/lib/meta_inspector/parsers/texts.rb +13 -4
- data/lib/meta_inspector/request.rb +34 -11
- data/lib/meta_inspector/version.rb +1 -1
- data/meta_inspector.gemspec +5 -6
- data/spec/document_spec.rb +20 -0
- data/spec/meta_inspector/head_first_spec.rb +195 -0
- data/spec/meta_inspector/images_spec.rb +233 -0
- data/spec/meta_inspector/links_spec.rb +22 -0
- data/spec/meta_inspector/redirections_spec.rb +20 -6
- data/spec/meta_inspector/texts_spec.rb +115 -0
- data/spec/request_spec.rb +29 -0
- metadata +16 -23
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5bc16dfdcdd22661ba7bb196d4db8750a5fa4d48d054197982768fa7fac44835
|
|
4
|
+
data.tar.gz: ce356b1f0f2511069ff96329e775e3581282d68d395c4dada93a9275e5dd4aa6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a669057baeb8107c8ae6940f4323f60664982fa3c8c74ce4f029b434ebfe227436c743a6b6041acbcddfdc2ea7fb38704a6126d3204b18479a8ca81b2b63a0e
|
|
7
|
+
data.tar.gz: 123f1cbb41fd5492307d96e316b17c91fc4f17d523a12f9fd865be49a3de31d8b932ceae6365bc7e1f6f49fd9721f8626eb9a8b5bbcbf9d86b2e972aedaa1544
|
data/.circleci/config.yml
CHANGED
|
@@ -35,6 +35,20 @@ jobs:
|
|
|
35
35
|
name: Run tests
|
|
36
36
|
command: bundle exec rake
|
|
37
37
|
|
|
38
|
+
# Same as test_4_0, but ignores Gemfile.lock and uses the newest versions the
|
|
39
|
+
# gemspec allows, which is what people who install the gem get.
|
|
40
|
+
test_4_0_latest_deps:
|
|
41
|
+
docker:
|
|
42
|
+
- image: cimg/ruby:4.0
|
|
43
|
+
steps:
|
|
44
|
+
- checkout
|
|
45
|
+
- run:
|
|
46
|
+
name: Update dependencies to the newest allowed versions
|
|
47
|
+
command: bundle update
|
|
48
|
+
- run:
|
|
49
|
+
name: Run tests
|
|
50
|
+
command: bundle exec rake
|
|
51
|
+
|
|
38
52
|
workflows:
|
|
39
53
|
version: 2
|
|
40
54
|
deploy:
|
|
@@ -42,3 +56,4 @@ workflows:
|
|
|
42
56
|
- test_3_3
|
|
43
57
|
- test_3_4
|
|
44
58
|
- test_4_0
|
|
59
|
+
- test_4_0_latest_deps
|
data/.github/dependabot.yml
CHANGED
data/.gitignore
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# MetaInspector Changelog
|
|
2
2
|
|
|
3
|
+
## [Changes in 5.19.0](https://github.com/jaimeiniesta/metainspector/compare/v5.18.1...v5.19.0)
|
|
4
|
+
|
|
5
|
+
* Security: stop modifying the `faraday_options` hash passed to MetaInspector. When the same hash was reused, its `redirect` options were deleted after the first request, so a redirect `callback` used to block private hosts only ran once and later requests followed any redirect ([GHSA-mm2p-6q2v-7ph5](https://github.com/jaimeiniesta/metainspector/security/advisories/GHSA-mm2p-6q2v-7ph5)). Frozen option hashes no longer raise `FrozenError`.
|
|
6
|
+
* Add `language` to expose the root HTML `lang` attribute, trimming surrounding whitespace and returning `nil` for missing or blank values. Include it in `to_hash` (#121).
|
|
7
|
+
* Add opt-in `max_image_downloads` to limit image size lookup attempts while preserving dimensions already available in HTML (#242).
|
|
8
|
+
* Add `parsely-title`, `parsely-author`, and `parsely-image-url` fallbacks to `best_title`, `best_author`, and `images.owner_suggested` / `images.best`, preserving existing preferred metadata sources (#214).
|
|
9
|
+
* Resolve relative base hrefs against the full document URL, preserving nested paths and non-default ports (#240).
|
|
10
|
+
* Skip invalid image URLs in `images.with_size`, preventing `images.best` from failing when FastImage receives a nil URL ([#521](https://github.com/jaimeiniesta/metainspector/issues/521), follow-up to [#359](https://github.com/jaimeiniesta/metainspector/issues/359)).
|
|
11
|
+
* Add opt-in `head_first: true` to check content type with HEAD and reject non-HTML files before downloading them (#221).
|
|
12
|
+
|
|
13
|
+
## [Changes in 5.18.1](https://github.com/jaimeiniesta/metainspector/compare/v5.18.0...v5.18.1)
|
|
14
|
+
|
|
15
|
+
* Require nokogiri 1.19.5, which fixes three low-severity security advisories.
|
|
16
|
+
* Commit Gemfile.lock to the repository, and run CI against the newest allowed dependencies as well.
|
|
17
|
+
|
|
3
18
|
## [Changes in 5.18.0](https://github.com/jaimeiniesta/metainspector/compare/v5.17.4...v5.18.0)
|
|
4
19
|
|
|
5
20
|
* Remove support for Ruby 3.2, which reached end of life on 2026-04-01.
|
data/README.md
CHANGED
|
@@ -84,6 +84,7 @@ page.feeds # Get rss or atom links in meta data fields as array of
|
|
|
84
84
|
### Texts
|
|
85
85
|
|
|
86
86
|
```ruby
|
|
87
|
+
page.language # declared language from the root <html lang="..."> attribute
|
|
87
88
|
page.title # title of the page from the head section, as string
|
|
88
89
|
page.best_title # best title of the page, from a selection of candidates
|
|
89
90
|
page.author # author of the page from the meta author tag
|
|
@@ -98,6 +99,20 @@ page.h5 # returns h5 text array
|
|
|
98
99
|
page.h6 # returns h6 text array
|
|
99
100
|
```
|
|
100
101
|
|
|
102
|
+
`language` trims surrounding whitespace and preserves the declared value, including
|
|
103
|
+
capitalization and regional tags such as `en-US`. It returns `nil` when the `lang`
|
|
104
|
+
attribute is missing, empty, or whitespace-only. It does not infer language from
|
|
105
|
+
the page content or other attributes. The value is also included in `to_hash`
|
|
106
|
+
under the `"language"` key.
|
|
107
|
+
|
|
108
|
+
`best_title` selects the first non-empty value from the standard `title` meta tag,
|
|
109
|
+
`og:title`, the head `<title>`, a body `<title>`, the first `<h1>`, and finally
|
|
110
|
+
`parsely-title`.
|
|
111
|
+
|
|
112
|
+
`best_author` selects the first non-empty value from the standard `author` meta tag,
|
|
113
|
+
the first link with `rel="author"`, the first `<address>`, `twitter:creator`, and
|
|
114
|
+
finally `parsely-author`. Both methods trim and collapse whitespace in their results.
|
|
115
|
+
|
|
101
116
|
### Links
|
|
102
117
|
|
|
103
118
|
```ruby
|
|
@@ -114,10 +129,15 @@ page.links.external # every external link found on the page as an absolute
|
|
|
114
129
|
```ruby
|
|
115
130
|
page.images # enumerable collection, with every img found on the page as an absolute URL
|
|
116
131
|
page.images.with_size # a sorted array (by descending area) of [image_url, width, height]
|
|
117
|
-
page.images.best # Most relevant image
|
|
132
|
+
page.images.best # Most relevant image from og:image, twitter:image, or parsely-image-url; falls back to page.images.largest
|
|
118
133
|
page.images.favicon # absolute URL to the favicon
|
|
119
134
|
```
|
|
120
135
|
|
|
136
|
+
`images.owner_suggested` tries `og:image`, `twitter:image`, and `parsely-image-url`
|
|
137
|
+
in that order, skipping empty or whitespace-only values. Relative image URLs are
|
|
138
|
+
resolved against the document's base URL. `images.best` uses this suggested image
|
|
139
|
+
before falling back to the largest suitable image on the page.
|
|
140
|
+
|
|
121
141
|
### Meta tags
|
|
122
142
|
|
|
123
143
|
When it comes to meta tags, you have several options:
|
|
@@ -385,6 +405,28 @@ page.content_type # "image/png"
|
|
|
385
405
|
page.description # will return a garbled string
|
|
386
406
|
```
|
|
387
407
|
|
|
408
|
+
### Check content type before downloading
|
|
409
|
+
|
|
410
|
+
To avoid downloading large non-HTML files, enable `head_first`:
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
page = MetaInspector.new('http://example.com/page', head_first: true)
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
This sends an HTTP HEAD request before GET. If a successful HEAD response declares
|
|
417
|
+
a content type other than `text/html`, MetaInspector raises `MetaInspector::NonHtmlError`
|
|
418
|
+
without downloading the body. Content-type parameters such as `charset` are ignored.
|
|
419
|
+
With `allow_non_html_content: true`, the download proceeds regardless of content type.
|
|
420
|
+
|
|
421
|
+
If HEAD omits the content type or returns an unsuccessful status (including 405 or
|
|
422
|
+
501 for servers that do not support HEAD), MetaInspector continues with GET and its
|
|
423
|
+
usual content-type check. HEAD timeouts and connection errors use the same exception
|
|
424
|
+
handling as GET. The final page data, response headers and status come from GET.
|
|
425
|
+
|
|
426
|
+
This option is off by default because it adds a request and not every server
|
|
427
|
+
supports HEAD. Both requests use the configured headers, timeouts, retries and
|
|
428
|
+
redirect settings. Passing a `document` directly makes no HTTP requests.
|
|
429
|
+
|
|
388
430
|
### URL Normalization
|
|
389
431
|
|
|
390
432
|
By default, URLs are normalized using the Addressable gem. For example:
|
|
@@ -413,6 +455,19 @@ If you want to disable this, you can specify it like this:
|
|
|
413
455
|
page = MetaInspector.new('http://example.com', download_images: false)
|
|
414
456
|
```
|
|
415
457
|
|
|
458
|
+
To limit how many images are downloaded to determine their dimensions, use `max_image_downloads`:
|
|
459
|
+
|
|
460
|
+
```ruby
|
|
461
|
+
page = MetaInspector.new('http://example.com', max_image_downloads: 10)
|
|
462
|
+
page.images.with_size
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
The default is `nil` (unlimited). The option accepts a non-negative integer or `nil`; `0` disables image downloads. `download_images: false` always disables downloads, regardless of the limit.
|
|
466
|
+
|
|
467
|
+
The limit counts image size lookup attempts in HTML order, after excluding unparseable URLs and duplicates. Images with both HTML dimensions already present do not consume the limit, and those dimensions are preserved even after the limit is reached. Failed lookups count toward the limit. Images with incomplete dimensions that are not downloaded remain in `images.with_size` with dimensions `[0, 0]`, matching the behavior when downloads are disabled.
|
|
468
|
+
|
|
469
|
+
The limit leaves `images.to_a` unchanged, but can affect `images.largest` and the largest-image fallback in `images.best`, since images with unknown dimensions cannot be ranked by size. Size lookup results are cached per document, so repeated calls reuse the same results. This limits size lookup attempts, not elapsed time or HTTP requests made while following image redirects.
|
|
470
|
+
|
|
416
471
|
### Caching responses
|
|
417
472
|
|
|
418
473
|
MetaInspector can be configured to use [Faraday::HttpCache](https://github.com/plataformatec/faraday-http-cache) to cache page responses. For that you should pass the `faraday_http_cache` option with at least the `:store` key, for example:
|
|
@@ -9,12 +9,20 @@ module MetaInspector
|
|
|
9
9
|
# * read_timeout: defaults to 20 seconds
|
|
10
10
|
# * retries: defaults to 3 times
|
|
11
11
|
# * allow_redirections: when true, follow HTTP redirects. Defaults to true
|
|
12
|
+
# * head_first: check content type with HEAD before GET. Defaults to false
|
|
12
13
|
# * document: the html of the url as a string
|
|
13
14
|
# * headers: object containing custom headers for the request
|
|
14
15
|
# * normalize_url: true by default
|
|
16
|
+
# * download_images: fetch missing image dimensions. Defaults to true
|
|
17
|
+
# * max_image_downloads: maximum image size lookups, or nil for unlimited
|
|
15
18
|
# * faraday_options: an optional hash of options to pass to Faraday on the request
|
|
16
19
|
def initialize(initial_url, options = {})
|
|
17
20
|
options = defaults.merge(options)
|
|
21
|
+
@max_image_downloads = options[:max_image_downloads]
|
|
22
|
+
unless @max_image_downloads.nil? || (@max_image_downloads.is_a?(Integer) && @max_image_downloads >= 0)
|
|
23
|
+
raise ArgumentError, 'max_image_downloads must be a non-negative Integer or nil'
|
|
24
|
+
end
|
|
25
|
+
|
|
18
26
|
@connection_timeout = options[:connection_timeout]
|
|
19
27
|
@read_timeout = options[:read_timeout]
|
|
20
28
|
@retries = options[:retries]
|
|
@@ -31,6 +39,8 @@ module MetaInspector
|
|
|
31
39
|
@faraday_http_cache = options[:faraday_http_cache]
|
|
32
40
|
@url = MetaInspector::URL.new(initial_url, normalize: @normalize_url)
|
|
33
41
|
@request = MetaInspector::Request.new(@url, allow_redirections: @allow_redirections,
|
|
42
|
+
head_first: options[:head_first],
|
|
43
|
+
allow_non_html_content: @allow_non_html_content,
|
|
34
44
|
connection_timeout: @connection_timeout,
|
|
35
45
|
read_timeout: @read_timeout,
|
|
36
46
|
retries: @retries,
|
|
@@ -38,7 +48,8 @@ module MetaInspector
|
|
|
38
48
|
headers: @headers,
|
|
39
49
|
faraday_options: @faraday_options,
|
|
40
50
|
faraday_http_cache: @faraday_http_cache) unless @document
|
|
41
|
-
@parser = MetaInspector::Parser.new(self, download_images: @download_images
|
|
51
|
+
@parser = MetaInspector::Parser.new(self, download_images: @download_images,
|
|
52
|
+
max_image_downloads: @max_image_downloads)
|
|
42
53
|
end
|
|
43
54
|
|
|
44
55
|
extend Forwardable
|
|
@@ -47,7 +58,7 @@ module MetaInspector
|
|
|
47
58
|
|
|
48
59
|
delegate [:content_type, :response] => :@request
|
|
49
60
|
|
|
50
|
-
delegate [:parsed, :title, :best_title, :author, :best_author,
|
|
61
|
+
delegate [:parsed, :language, :title, :best_title, :author, :best_author,
|
|
51
62
|
:h1, :h2, :h3, :h4, :h5, :h6, :description, :best_description, :links,
|
|
52
63
|
:images, :feeds, :feed, :charset, :meta_tags,
|
|
53
64
|
:meta_tag, :meta, :favicon,
|
|
@@ -60,6 +71,7 @@ module MetaInspector
|
|
|
60
71
|
'scheme' => scheme,
|
|
61
72
|
'host' => host,
|
|
62
73
|
'root_url' => root_url,
|
|
74
|
+
'language' => language,
|
|
63
75
|
'title' => title,
|
|
64
76
|
'best_title' => best_title,
|
|
65
77
|
'author' => author,
|
|
@@ -101,8 +113,10 @@ module MetaInspector
|
|
|
101
113
|
},
|
|
102
114
|
:allow_redirections => true,
|
|
103
115
|
:allow_non_html_content => false,
|
|
116
|
+
:head_first => false,
|
|
104
117
|
:normalize_url => true,
|
|
105
|
-
:download_images => true
|
|
118
|
+
:download_images => true,
|
|
119
|
+
:max_image_downloads => nil }
|
|
106
120
|
end
|
|
107
121
|
|
|
108
122
|
def default_user_agent
|
|
@@ -14,7 +14,8 @@ module MetaInspector
|
|
|
14
14
|
@meta_tag_parser = MetaInspector::Parsers::MetaTagsParser.new(self)
|
|
15
15
|
@links_parser = MetaInspector::Parsers::LinksParser.new(self)
|
|
16
16
|
@download_images = options[:download_images]
|
|
17
|
-
@images_parser = MetaInspector::Parsers::ImagesParser.new(self, download_images: @download_images
|
|
17
|
+
@images_parser = MetaInspector::Parsers::ImagesParser.new(self, download_images: @download_images,
|
|
18
|
+
max_image_downloads: options[:max_image_downloads])
|
|
18
19
|
@texts_parser = MetaInspector::Parsers::TextsParser.new(self)
|
|
19
20
|
|
|
20
21
|
parsed # parse early so we can fail early
|
|
@@ -26,7 +27,7 @@ module MetaInspector
|
|
|
26
27
|
delegate [:head_links, :stylesheets, :canonicals, :feeds, :feed] => :@head_links_parser
|
|
27
28
|
delegate [:links, :base_url] => :@links_parser
|
|
28
29
|
delegate :images => :@images_parser
|
|
29
|
-
delegate [:title, :best_title, :author, :best_author, :description, :best_description,
|
|
30
|
+
delegate [:language, :title, :best_title, :author, :best_author, :description, :best_description,
|
|
30
31
|
:h1, :h2, :h3, :h4, :h5, :h6] => :@texts_parser
|
|
31
32
|
|
|
32
33
|
# Returns the whole parsed document
|
|
@@ -10,6 +10,7 @@ module MetaInspector
|
|
|
10
10
|
|
|
11
11
|
def initialize(main_parser, options = {})
|
|
12
12
|
@download_images = options[:download_images]
|
|
13
|
+
@max_image_downloads = options[:max_image_downloads]
|
|
13
14
|
super(main_parser)
|
|
14
15
|
end
|
|
15
16
|
|
|
@@ -17,7 +18,7 @@ module MetaInspector
|
|
|
17
18
|
self
|
|
18
19
|
end
|
|
19
20
|
|
|
20
|
-
# Returns
|
|
21
|
+
# Returns the Facebook Open Graph image, Twitter image, Parsely image or
|
|
21
22
|
# the largest image in the image collection
|
|
22
23
|
def best
|
|
23
24
|
owner_suggested || largest
|
|
@@ -26,9 +27,10 @@ module MetaInspector
|
|
|
26
27
|
# Returns the parsed image from Facebook's open graph property tags
|
|
27
28
|
# Most major websites now define this property and is usually relevant
|
|
28
29
|
# See doc at http://developers.facebook.com/docs/opengraph/
|
|
29
|
-
# If none found, tries with Twitter image
|
|
30
|
+
# If none found, tries with Twitter image, then Parsely image
|
|
30
31
|
def owner_suggested
|
|
31
|
-
suggested_img = content_of(meta['og:image']) || content_of(meta['twitter:image'])
|
|
32
|
+
suggested_img = content_of(meta['og:image']) || content_of(meta['twitter:image']) ||
|
|
33
|
+
content_of(meta['parsely-image-url'])
|
|
32
34
|
URL.absolutify(suggested_img, base_url, normalize: false) if suggested_img
|
|
33
35
|
end
|
|
34
36
|
|
|
@@ -39,17 +41,19 @@ module MetaInspector
|
|
|
39
41
|
imgs_with_size = img_nodes.map do |img_node|
|
|
40
42
|
[URL.absolutify(img_node['src'], base_url, normalize: false), img_node['width'], img_node['height']]
|
|
41
43
|
end
|
|
44
|
+
imgs_with_size.reject! { |url, width, height| url.nil? || url.empty? }
|
|
42
45
|
imgs_with_size.uniq! { |url, width, height| url }
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
download_attempts = 0
|
|
47
|
+
imgs_with_size.map! do |url, width, height|
|
|
48
|
+
if width.nil? || height.nil?
|
|
49
|
+
if @download_images && (@max_image_downloads.nil? || download_attempts < @max_image_downloads)
|
|
50
|
+
download_attempts += 1
|
|
51
|
+
width, height = FastImage.size(url)
|
|
52
|
+
else
|
|
53
|
+
width, height = [0, 0]
|
|
54
|
+
end
|
|
52
55
|
end
|
|
56
|
+
[url, width.to_i, height.to_i]
|
|
53
57
|
end
|
|
54
58
|
imgs_with_size.sort_by { |url, width, height| -(width.to_i * height.to_i) }
|
|
55
59
|
end
|
|
@@ -93,7 +97,7 @@ module MetaInspector
|
|
|
93
97
|
end
|
|
94
98
|
|
|
95
99
|
def content_of(content)
|
|
96
|
-
return nil if content.nil? || content.empty?
|
|
100
|
+
return nil if content.nil? || content.strip.empty?
|
|
97
101
|
content
|
|
98
102
|
end
|
|
99
103
|
end
|
|
@@ -46,8 +46,9 @@ module MetaInspector
|
|
|
46
46
|
# Returns the base url to absolutify relative links.
|
|
47
47
|
# This can be the one set on a <base> tag,
|
|
48
48
|
# or the url of the document if no <base> tag was found.
|
|
49
|
+
# Relative base hrefs are resolved against the document URL.
|
|
49
50
|
def base_url
|
|
50
|
-
current_base_href = base_href.to_s.strip.empty? ? nil : URL.absolutify(base_href,
|
|
51
|
+
current_base_href = base_href.to_s.strip.empty? ? nil : URL.absolutify(base_href, url)
|
|
51
52
|
current_base_href || url
|
|
52
53
|
end
|
|
53
54
|
|
|
@@ -3,6 +3,12 @@ module MetaInspector
|
|
|
3
3
|
class TextsParser < Base
|
|
4
4
|
delegate [:parsed, :meta] => :@main_parser
|
|
5
5
|
|
|
6
|
+
# Returns the declared HTML language, or nil when missing or blank
|
|
7
|
+
def language
|
|
8
|
+
value = parsed.at_css('html')&.[]('lang')&.strip
|
|
9
|
+
value unless value.to_s.empty?
|
|
10
|
+
end
|
|
11
|
+
|
|
6
12
|
# Returns the parsed document title, from the content of the <title> tag
|
|
7
13
|
# within the <head> section.
|
|
8
14
|
def title
|
|
@@ -42,12 +48,13 @@ module MetaInspector
|
|
|
42
48
|
@author ||= meta['author']
|
|
43
49
|
end
|
|
44
50
|
|
|
45
|
-
# An author getter that returns the first non-
|
|
51
|
+
# An author getter that returns the first non-empty author
|
|
46
52
|
# from the following candidates:
|
|
47
|
-
# - the standard meta
|
|
53
|
+
# - the standard meta author
|
|
48
54
|
# - a link with the relational attribute "author"
|
|
49
55
|
# - address tag which may contain the author
|
|
50
56
|
# - the twitter:creator meta tag for the username
|
|
57
|
+
# - the parsely-author meta tag
|
|
51
58
|
def best_author
|
|
52
59
|
@best_author ||= find_best_author
|
|
53
60
|
end
|
|
@@ -80,7 +87,8 @@ module MetaInspector
|
|
|
80
87
|
meta['og:title'],
|
|
81
88
|
parsed.css('head title'),
|
|
82
89
|
parsed.css('body title'),
|
|
83
|
-
parsed.css('h1').first
|
|
90
|
+
parsed.css('h1').first,
|
|
91
|
+
meta['parsely-title']
|
|
84
92
|
]
|
|
85
93
|
candidates.flatten!
|
|
86
94
|
candidates.compact!
|
|
@@ -94,7 +102,8 @@ module MetaInspector
|
|
|
94
102
|
meta['author'],
|
|
95
103
|
parsed.css('a[rel="author"]').first,
|
|
96
104
|
parsed.css('address').first,
|
|
97
|
-
meta['twitter:creator']
|
|
105
|
+
meta['twitter:creator'],
|
|
106
|
+
meta['parsely-author']
|
|
98
107
|
]
|
|
99
108
|
candidates.flatten!
|
|
100
109
|
candidates.compact!
|
|
@@ -18,6 +18,8 @@ module MetaInspector
|
|
|
18
18
|
fail MetaInspector::RequestError.new('URL must be HTTP') unless @url.url =~ /http[s]?:\/\//i
|
|
19
19
|
|
|
20
20
|
@allow_redirections = options[:allow_redirections]
|
|
21
|
+
@head_first = options[:head_first]
|
|
22
|
+
@allow_non_html_content = options[:allow_non_html_content]
|
|
21
23
|
@connection_timeout = options[:connection_timeout]
|
|
22
24
|
@read_timeout = options[:read_timeout]
|
|
23
25
|
@retries = options[:retries]
|
|
@@ -42,8 +44,7 @@ module MetaInspector
|
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
def content_type
|
|
45
|
-
|
|
46
|
-
response.headers['content-type'].split(';')[0] if response
|
|
47
|
+
response_content_type(response)
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
def response
|
|
@@ -58,10 +59,12 @@ module MetaInspector
|
|
|
58
59
|
|
|
59
60
|
def fetch
|
|
60
61
|
Timeout::timeout(fatal_timeout) do
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
# Work on copies: the caller may reuse or freeze the hashes it passed in, and
|
|
63
|
+
# losing :redirect would drop a callback that blocks redirects to private hosts
|
|
64
|
+
faraday_options = @faraday_options.merge(:url => url)
|
|
65
|
+
follow_redirects_options = (faraday_options.delete(:redirect) || {}).dup
|
|
63
66
|
|
|
64
|
-
session = Faraday.new(
|
|
67
|
+
session = Faraday.new(faraday_options) do |faraday|
|
|
65
68
|
faraday.request :retry, max: @retries
|
|
66
69
|
|
|
67
70
|
faraday.request :gzip
|
|
@@ -73,8 +76,9 @@ module MetaInspector
|
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
if @faraday_http_cache.is_a?(Hash)
|
|
76
|
-
@faraday_http_cache
|
|
77
|
-
|
|
79
|
+
cache_options = @faraday_http_cache.dup
|
|
80
|
+
cache_options[:serializer] ||= Marshal
|
|
81
|
+
faraday.use Faraday::HttpCache, **cache_options
|
|
78
82
|
end
|
|
79
83
|
|
|
80
84
|
faraday.headers.merge!(@headers || {})
|
|
@@ -82,10 +86,8 @@ module MetaInspector
|
|
|
82
86
|
faraday.adapter :net_http
|
|
83
87
|
end
|
|
84
88
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
req.options.open_timeout = @read_timeout
|
|
88
|
-
end
|
|
89
|
+
check_head_content_type(session) if @head_first
|
|
90
|
+
response = perform_request(session, :get)
|
|
89
91
|
|
|
90
92
|
if @allow_redirections
|
|
91
93
|
@url.url = response.env.url.to_s
|
|
@@ -97,6 +99,27 @@ module MetaInspector
|
|
|
97
99
|
raise MetaInspector::TimeoutError.new(e)
|
|
98
100
|
end
|
|
99
101
|
|
|
102
|
+
def check_head_content_type(session)
|
|
103
|
+
head_response = perform_request(session, :head)
|
|
104
|
+
return unless head_response.success?
|
|
105
|
+
|
|
106
|
+
type = response_content_type(head_response)
|
|
107
|
+
if !@allow_non_html_content && !type.nil? && type != 'text/html'
|
|
108
|
+
fail MetaInspector::NonHtmlError.new "The url provided contains #{type} content instead of text/html content"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def response_content_type(response)
|
|
113
|
+
response.headers['content-type']&.split(';')&.first
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def perform_request(session, method)
|
|
117
|
+
session.public_send(method) do |req|
|
|
118
|
+
req.options.timeout = @connection_timeout
|
|
119
|
+
req.options.open_timeout = @read_timeout
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
100
123
|
# Timeouts when connecting / reading a request are handled by Faraday, but in the
|
|
101
124
|
# case of URLs that respond with streaming, Faraday will never return. In that case,
|
|
102
125
|
# we'll resort to our own timeout
|
data/meta_inspector.gemspec
CHANGED
|
@@ -8,14 +8,14 @@ Gem::Specification.new do |gem|
|
|
|
8
8
|
gem.homepage = "https://github.com/jaimeiniesta/metainspector"
|
|
9
9
|
gem.license = "MIT"
|
|
10
10
|
|
|
11
|
-
gem.files = `git ls-files`.split("\n")
|
|
11
|
+
gem.files = `git ls-files`.split("\n") - ["Gemfile.lock"]
|
|
12
12
|
gem.test_files = `git ls-files -- {spec}/*`.split("\n")
|
|
13
13
|
gem.name = "metainspector"
|
|
14
14
|
gem.require_paths = ["lib"]
|
|
15
15
|
gem.version = MetaInspector::VERSION
|
|
16
16
|
gem.required_ruby_version = '>= 3.3'
|
|
17
17
|
|
|
18
|
-
gem.add_dependency 'nokogiri', '~> 1.19.
|
|
18
|
+
gem.add_dependency 'nokogiri', '~> 1.19.5'
|
|
19
19
|
gem.add_dependency 'faraday', '~> 2.14', '>= 2.14.3'
|
|
20
20
|
gem.add_dependency 'faraday-cookie_jar', '~> 0.0'
|
|
21
21
|
gem.add_dependency 'faraday-encoding', '~> 0.0'
|
|
@@ -29,11 +29,10 @@ Gem::Specification.new do |gem|
|
|
|
29
29
|
|
|
30
30
|
gem.add_development_dependency 'rspec', '~> 3.11'
|
|
31
31
|
gem.add_development_dependency 'webmock', '~> 3.17'
|
|
32
|
-
gem.add_development_dependency 'awesome_print', '~> 1.9'
|
|
33
32
|
gem.add_development_dependency 'rake', '~> 13.0'
|
|
34
33
|
gem.add_development_dependency 'pry', '~> 0.14'
|
|
35
|
-
gem.add_development_dependency 'puma', '~> 8.0.0'
|
|
34
|
+
gem.add_development_dependency 'puma', '~> 8.0', '>= 8.0.2'
|
|
36
35
|
gem.add_development_dependency 'rubocop', '~> 1.34'
|
|
37
|
-
gem.add_development_dependency 'resolv', '~> 0.8
|
|
38
|
-
gem.add_development_dependency 'sinatra', '~> 4.2
|
|
36
|
+
gem.add_development_dependency 'resolv', '~> 0.8'
|
|
37
|
+
gem.add_development_dependency 'sinatra', '~> 4.2'
|
|
39
38
|
end
|
data/spec/document_spec.rb
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
require 'spec_helper'
|
|
2
2
|
|
|
3
3
|
describe MetaInspector::Document do
|
|
4
|
+
describe 'max_image_downloads option' do
|
|
5
|
+
[-1, 1.5, '1', true, false].each do |limit|
|
|
6
|
+
it "should reject #{limit.inspect} before fetching the document" do
|
|
7
|
+
expect do
|
|
8
|
+
MetaInspector::Document.new('http://example.com/', max_image_downloads: limit)
|
|
9
|
+
end.to raise_error(ArgumentError, 'max_image_downloads must be a non-negative Integer or nil')
|
|
10
|
+
|
|
11
|
+
expect(WebMock).not_to have_requested(:get, 'http://example.com/')
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
4
16
|
describe 'passing the contents of the document as html' do
|
|
5
17
|
let(:doc) { MetaInspector::Document.new('http://cnn.com/', :document => "<html><head><title>Hello From Passed Html</title><a href='/hello'>Hello link</a></head><body></body></html>") }
|
|
6
18
|
|
|
@@ -17,6 +29,13 @@ describe MetaInspector::Document do
|
|
|
17
29
|
expect(MetaInspector::Document.new('http://pagerankalert.com').to_s.class).to eq(String)
|
|
18
30
|
end
|
|
19
31
|
|
|
32
|
+
it 'should include a nil language in the hash when the document has no declared language' do
|
|
33
|
+
stub_request(:get, 'http://example.com/').to_return(body: '<html><body></body></html>', headers: { 'Content-Type' => 'text/html' })
|
|
34
|
+
doc = MetaInspector::Document.new('http://example.com/')
|
|
35
|
+
|
|
36
|
+
expect(doc.to_hash).to include('language' => nil)
|
|
37
|
+
end
|
|
38
|
+
|
|
20
39
|
it "should return a Hash with all the values set" do
|
|
21
40
|
doc = MetaInspector::Document.new('http://pagerankalert.com')
|
|
22
41
|
expect(doc.to_hash).to eq({
|
|
@@ -24,6 +43,7 @@ describe MetaInspector::Document do
|
|
|
24
43
|
"scheme" => "http",
|
|
25
44
|
"host" => "pagerankalert.com",
|
|
26
45
|
"root_url" => "http://pagerankalert.com/",
|
|
46
|
+
"language" => "en",
|
|
27
47
|
"title" => "PageRankAlert.com :: Track your PageRank changes & receive alerts",
|
|
28
48
|
"best_title" => "PageRankAlert.com :: Track your PageRank changes & receive alerts",
|
|
29
49
|
"author" => nil,
|