This repository has been archived by the owner on Oct 15, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 941
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
463 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,112 @@ | ||
package DDG::Spice::Whois; | ||
# ABSTRACT: Returns an internet domain's availability and whois information. | ||
|
||
use DDG::Spice; | ||
|
||
# Metadata for this spice | ||
name 'Whois'; | ||
source 'Whois API'; | ||
description 'Whois info and registration links for web domains'; | ||
primary_example_queries 'whois duckduckgo.com', 'whois http://duckduckgo.com'; | ||
secondary_example_queries 'domain duckduckgo.com', 'who owns duckduckgo.com', 'duckduckgo.com available'; | ||
category 'programming'; | ||
topics 'computing', 'geek', 'programming', 'sysadmin'; | ||
code_url 'https://github.com/duckduckgo/zeroclickinfo-spice/blob/master/lib/DDG/Spice/Whois.pm'; | ||
|
||
attribution twitter => 'bjennelle', | ||
github => ["b1ake", 'Blake Jennelle']; | ||
|
||
|
||
# turns on/off debugging output | ||
my $is_debug = 0; | ||
|
||
# regex for allowed TLDS (grabbed from DDG core repo, in /lib/DDG/Util/Constants.pm) | ||
my $tlds_qr = qr/(?:c(?:o(?:m|op)?|at?|[iykgdmnxruhcfzvl])|o(?:rg|m)|n(?:et?|a(?:me)?|[ucgozrfpil])|e(?:d?u|[gechstr])|i(?:n(?:t|fo)?|[stqldroem])|m(?:o(?:bi)?|u(?:seum)?|i?l|[mcyvtsqhaerngxzfpwkd])|g(?:ov|[glqeriabtshdfmuywnp])|b(?:iz?|[drovfhtaywmzjsgbenl])|t(?:r(?:avel)?|[ncmfzdvkopthjwg]|e?l)|k[iemygznhwrp]|s[jtvberindlucygkhaozm]|u[gymszka]|h[nmutkr]|r[owesu]|d[kmzoej]|a(?:e(?:ro)?|r(?:pa)?|[qofiumsgzlwcnxdt])|p(?:ro?|[sgnthfymakwle])|v[aegiucn]|l[sayuvikcbrt]|j(?:o(?:bs)?|[mep])|w[fs]|z[amw]|f[rijkom]|y[eut]|qa)/i; | ||
|
||
# regex for parsing URLs | ||
my $url_qr = qr/(?:http:\/\/)? # require http | ||
([^\s\.]*\.)* # capture any subdomains | ||
([^\s\.]*?) # capture the domain | ||
\.($tlds_qr) # capture the tld, such as .com | ||
(\:?[0-9]{1,4})? # look for a port, such as :3000 | ||
([^\s]*)/x; # look for an extended path, such as /pages/about.htm | ||
|
||
# additional keywords that trigger this spice | ||
my $whois_keywords_qr = qr/whois|lookup|(?:is\s|)domain|(?:is\s|)available|register|owner(?:\sof|)|who\sowns|(?:how\sto\s|)buy/i; | ||
|
||
# trigger this spice when either: | ||
# - query contains only a URL | ||
# - query contains starts or end with any of the whois keywords | ||
# | ||
# note that there are additional guards in the handle() function that | ||
# narrow this spice's query space. | ||
# | ||
triggers query_raw => | ||
# allow the naked url with leading and trailing spaces | ||
qr/^\s*$url_qr\s*$/, | ||
|
||
# allow the whois keywords at the beginning or end of the string | ||
# with leading or trailing spaces. | ||
# | ||
# if at the end of the string, allow a trailing question mark. | ||
qr/^\s*$whois_keywords_qr | ||
|$whois_keywords_qr[?]?\s*$/x; | ||
|
||
# API call details for Whois API (http://www.whoisxmlapi.com/) | ||
spice to => 'http://www.whoisxmlapi.com/whoisserver/WhoisService?domainName=$1&outputFormat=JSON&callback={{callback}}&username={{ENV{DDG_SPICE_WHOIS_USERNAME}}}&password={{ENV{DDG_SPICE_WHOIS_PASSWORD}}}'; | ||
|
||
handle sub { | ||
my ($query) = @_; | ||
return if !$query; # do not trigger this spice if the query is blank | ||
|
||
# trim any leading and trailing spaces | ||
$query = trim($query); | ||
|
||
# remove any trailing question marks, which are allowed | ||
# but can disrupt the regexs | ||
$query =~ s/\?$//; | ||
|
||
# parse the URL into its parts | ||
my ($subdomains, $domain, $tld, $port, $resource_path) = $query =~ $url_qr; | ||
|
||
# debugging output | ||
warn 'query: ', $query, "\t", 'sub: ', $subdomains || '', "\t", 'domain: ', $domain || '', "\t", 'tld: ', $tld || '', "\t", 'port: ', $port || '', "\t", 'resource path: ', $resource_path || '' if $is_debug; | ||
|
||
# get the non-URL text from the query by combining the text before and after the match | ||
my $non_url_text = $` . $'; #' <-- closing tick added for syntax highlighting | ||
|
||
# is the string a naked domain, i.e. is there any text besides the domain? | ||
my $is_naked_domain = trim($non_url_text) eq ''; | ||
|
||
# skip if we're missing a domain or a tld | ||
return if !defined $domain || $domain eq '' || !defined $tld || $tld eq ''; | ||
|
||
# skip if we have naked domain that contains a non-www subdomain, a port or a resource_path. | ||
# e.g. continue: 'http://duckduckgo.com' is allowed | ||
# skip: 'http://blog.duckduckgo.com' | ||
# skip: 'http://duckduckgo.com:8080' | ||
# skip: 'http://blog.duckduckgo.com/hello.html' | ||
# | ||
# note that if the user includes a whois keyword to any of these, | ||
# such as 'whois http://blog.duckduckgo.com', they we continue. | ||
# | ||
# this signals to us that the user wants a whois result, and isn't just | ||
# trying to nav to the URL they typed. | ||
# | ||
return if $is_naked_domain | ||
&& ( (defined $subdomains && $subdomains !~ /^www.$/) | ||
|| (defined $port && $port ne '') | ||
|| (defined $resource_path && $resource_path ne '')); | ||
|
||
# return the combined domain + tld (after adding a period in between) | ||
return lc "$domain.$tld"; | ||
}; | ||
|
||
# Returns a string with leading and trailing spaces removed. | ||
sub trim { | ||
my ($str) = @_; | ||
$str =~ s/^\s*(.*)?\s*$/$1/; | ||
return $str; | ||
} | ||
|
||
1; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
<strong>The domain '<strong>{{domainName}}</strong>' may be available!</strong><br /> | ||
To register, try <a href="https://domai.nr/{{domainName}}" target="_blank">Domainr</a>, <a href="https://www.namecheap.com/domains/registration/results.aspx?domain={{domainName}}" target="_blank">NameCheap</a> or <a href="https://www.101domain.com/domain-availability-search.htm?q={{domainName}}" target="_blank">101Domains</a><br /> | ||
<br /> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
|
||
@media (max-width: 600px) { | ||
.zci--whois table { | ||
table-layout: fixed; | ||
width: 100%; | ||
} | ||
|
||
.zci--whois table td { | ||
word-wrap: break-word; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
(function (env) { | ||
"use strict"; | ||
|
||
// turns on/off debugging output | ||
var is_debug = false; | ||
|
||
// spice callback function | ||
env.ddg_spice_whois = function(raw_api_result) { | ||
|
||
// for debugging | ||
if(is_debug) console.log('in start of JS, raw_api_result:', raw_api_result); | ||
|
||
// normalize the api output | ||
var api_result = normalize_api_result(raw_api_result); | ||
|
||
if(is_debug) console.log('normalized api_result:', api_result || 'empty'); | ||
|
||
// Check for API error and exit early if found | ||
// (with error message when in debug mode) | ||
if (!api_result) { | ||
if(is_debug) console.log('Error with whois API. raw_api_result:', raw_api_result || 'empty', ', normalized api_result:', api_result || 'empty'); | ||
|
||
return Spice.failed('whois'); | ||
} | ||
|
||
// is the domain available? | ||
var is_avail = api_result.available; | ||
|
||
// if the domain isn't available, do we want to show | ||
// whois information? | ||
var is_whois_allowed = is_whois_query(DDG.get_query()); | ||
|
||
// for debugging | ||
if(is_debug) console.log("is_avail:", is_avail, "is_whois_allowed:", is_whois_allowed); | ||
|
||
// decide which template to show, if any | ||
if(is_avail) { | ||
// show message saying the domain is available | ||
show_available(api_result); | ||
} else if(is_whois_allowed) { | ||
// show whois info for the domain | ||
show_whois(api_result); | ||
} else { | ||
// by default, show nothing | ||
} | ||
|
||
}; | ||
|
||
// Returns whether we should show whois data if this | ||
// domain is not available. | ||
var is_whois_query = function(query) { | ||
// show whois results except when the query contains only the domain | ||
// and no other keywords, which we test by looking for a space in the query. | ||
return /\s/.test($.trim(query)); | ||
}; | ||
|
||
// parse the api response into a standard format | ||
var normalize_api_result = function(api_result) { | ||
|
||
// return nothing if no api_result, if error, or if WhoisRecord object is missing | ||
if(!api_result || api_result.error || !api_result.WhoisRecord) return; | ||
|
||
// use only the 'WhoisRecord' portion, because that's where | ||
// all the data is stored. | ||
api_result = api_result.WhoisRecord; | ||
|
||
// sometimes the data is nested inside the 'registryData' object | ||
if(!api_result.createdDate | ||
&& api_result.registryData | ||
&& (api_result.registryData.createdDate || api_result.registryData.expiresDate) ){ | ||
|
||
api_result = api_result.registryData; | ||
} | ||
|
||
// store the domain's various contacts in an array. | ||
// | ||
// we'll iterate through this array in order, using | ||
// info from the first contact that contains the field we want. | ||
var contacts = [ | ||
api_result.registrant, | ||
api_result.administrativeContact, | ||
api_result.technicalContact | ||
]; | ||
|
||
// return the normalized output as a hash | ||
var normalized = { | ||
|
||
// these first fields are not displayed | ||
// (hence the camelCase, which the user will not see) | ||
|
||
'domainName': api_result.domainName, | ||
'available': is_domain_available(api_result), | ||
|
||
// the remaining fields are displayed | ||
// (hence the user-friendly capitalization and spaces) | ||
|
||
'Status': is_domain_available(api_result) ? 'Available' : 'Registered', | ||
'Registered to': get_first_by_key(contacts, 'name'), | ||
'Email': get_first_by_key(contacts, 'email'), | ||
|
||
// trim dates so they are shown without times | ||
// (if no time was found, the replace() call will return undef, | ||
// so we need to fallback to the original string) | ||
'Last updated': api_result.updatedDate | ||
&& api_result.updatedDate.replace(/^(.*)?\s(.*)?$/, '$1'), | ||
|
||
'Expires': api_result.expiresDate | ||
&& api_result.expiresDate.replace(/^(.*)?\s(.*)?$/, '$1'), | ||
}; | ||
|
||
// return nothing if all key whois data is missing | ||
if( !normalized['Registered to'] | ||
&& !normalized['Email'] | ||
&& !normalized['Last updated'] | ||
&& !normalized['Expires']) { | ||
return; | ||
} | ||
|
||
return normalized; | ||
} | ||
|
||
// Returns whether the domain is registered to someone, based on the API result. | ||
var is_domain_available = function(api_result) { | ||
return api_result.dataError && api_result.dataError === 'MISSING_WHOIS_DATA'; | ||
}; | ||
|
||
// Searches an array of objects for the first value | ||
// at the specified key. | ||
var get_first_by_key = function(arr, key) { | ||
if(!arr || arr.length == 0) return; | ||
|
||
// find the first object in the array that has a non-empty value at the key | ||
var first; | ||
$.each(arr, function(index, obj) { | ||
// get the value at the specified key | ||
// (which could be undefined) | ||
var value = obj && obj[key]; | ||
|
||
// update the first var if the value is truthy | ||
// and first hasn't already been found | ||
if(!first && value) { | ||
first = value; | ||
} | ||
}); | ||
|
||
// return first, which could still be empty | ||
return first; | ||
} | ||
|
||
// Data that's shared between the two Spice.add calls. | ||
var get_shared_spice_data = function(api_result) { | ||
return { | ||
id: "whois", | ||
name: "Whois", | ||
meta: { | ||
sourceName: "Whois API", | ||
sourceUrl: 'http://www.whoisxmlapi.com/whois-api-doc.php#whoisserver/WhoisService?rid=2&domainName=' | ||
+ api_result.domainName | ||
+ '&outputFormat=json&target=raw' | ||
}, | ||
templates: { | ||
group: 'base', | ||
options:{ | ||
moreAt: true | ||
} | ||
} | ||
}; | ||
}; | ||
|
||
// Show message saying that the domain is available. | ||
var show_available = function(api_result) { | ||
var shared_spice_data = get_shared_spice_data(api_result); | ||
|
||
// add the attributes specific to this template | ||
shared_spice_data.data = api_result; | ||
shared_spice_data.templates.options.content = Spice.whois.available; | ||
|
||
Spice.add(shared_spice_data); | ||
}; | ||
|
||
// Show whois info for the domain using the 'record' template. | ||
var show_whois = function(api_result) { | ||
var shared_spice_data = get_shared_spice_data(api_result); | ||
|
||
// add the attributes specific to this template | ||
shared_spice_data.data = { | ||
'record_data': api_result, | ||
'record_keys': ['Status', 'Registered to', 'Email', 'Last updated', 'Expires'] | ||
}; | ||
shared_spice_data.templates.options.content = 'record'; | ||
shared_spice_data.templates.options.keySpacing = true; | ||
|
||
Spice.add(shared_spice_data); | ||
}; | ||
|
||
|
||
|
||
}(this)); |
Oops, something went wrong.