CDNUpdates/CDNContent.py

443 lines
17 KiB
Python

"""CDNUpdates' main logic"""
import json
import re
from urllib.parse import quote
from urllib.request import Request, urlopen
from sublime import load_settings
from .CDNConstants import (
AJAX_GOOGLE_APIS_CORRESPONDENCES,
AJAX_MICROSOFT_CORRESPONDENCES,
CDN_STATIC_FILE_CORRESPONDENCES,
MAXCDN_BOOTSTRAP_CORRESPONDENCES,
OPENSOURCE_KEYCDN_CORRESPONDENCES,
SEMVER_REGEXP_OBJECT
)
from .CDNUtils import log_message
class CDNContent:
"""
This class run verifies whether found CDN are up to date.
Checks (try to) rely on the CDN provider's API, or on GitHub directly.
It also stores the results (Sublime.Region + Urllib.ParseResult) for future usages.
"""
def __init__(self, region, parsed_result):
self.sublime_region = region
self.parsed_result = parsed_result
# This variable will store a status as the ones below :
# ('up_to_date', 'to_update', 'not_found')
self.status = None
# These variables will store the final information of this CDN.
self.name = None
self.latest_version = None
# We load the settings file to retrieve a GitHub API token afterwards.
self.settings = load_settings('CDNUpdates.sublime-settings')
def handle_provider(self): # pylint: disable=too-many-statements, too-many-return-statements, too-many-branches
"""
This is the most important method of CDNUpdates.
This is where are comparing the versions in function of the provider...
... and their different formatting conventions.
"""
# CDNJS.com will be handled here.
if self.parsed_result.netloc == 'cdnjs.cloudflare.com':
# We temporally store the result of the path split around '/'.
# The library name will be in `[3]`, and its version in `[4]`.
tmp = self.parsed_result.path.split('/')
self.name = tmp[3]
self._compare_with_latest_cdnjs_version(self.name, tmp[4])
elif self.parsed_result.netloc == 'maxcdn.bootstrapcdn.com':
tmp = self.parsed_result.path.split('/')
self.name = tmp[1]
if self.name not in MAXCDN_BOOTSTRAP_CORRESPONDENCES.keys():
log_message("{0} is not known to be delivered by this provider.".format(self.name))
self.status = 'not_found'
return
self._compare_with_latest_github_tag(
MAXCDN_BOOTSTRAP_CORRESPONDENCES.get(self.name)['owner'],
MAXCDN_BOOTSTRAP_CORRESPONDENCES.get(self.name)['name'],
tmp[2]
)
# CDN from CODE.JQUERY.COM will be handled here.
elif self.parsed_result.netloc == 'code.jquery.com':
tmp = self.parsed_result.path.split('/')
if tmp[1].startswith('jquery'):
self.name = 'jquery'
version = SEMVER_REGEXP_OBJECT.search(tmp[1])
version = version and version.group(0)
elif tmp[1] in ('ui', 'mobile', 'color'):
self.name = 'jquery-' + tmp[1]
version = tmp[2]
elif tmp[1] == 'qunit':
self.name = 'qunit'
version = SEMVER_REGEXP_OBJECT.search(tmp[2])
version = version and version.group(0)
elif tmp[1] == 'pep':
self.name = 'PEP'
version = tmp[2]
else:
version = None
if not version:
self.status = 'not_found'
return
self._compare_with_latest_github_tag(
# Only `QUnit` belongs to another organization.
('qunitjs' if self.name == 'qunit' else 'jquery'),
self.name,
version
)
# CDN from AJAX.GOOGLEAPIS.COM will be handled here.
elif self.parsed_result.netloc == 'ajax.googleapis.com':
tmp = self.parsed_result.path.split('/')
self.name = tmp[3]
if self.name not in AJAX_GOOGLE_APIS_CORRESPONDENCES.keys():
self.status = 'not_found'
return
self._compare_with_latest_github_tag(
AJAX_GOOGLE_APIS_CORRESPONDENCES.get(self.name)['owner'],
AJAX_GOOGLE_APIS_CORRESPONDENCES.get(self.name)['name'],
tmp[4]
)
# CDN from CDN.JSDLIVR.NET will be handled here.
elif self.parsed_result.netloc == 'cdn.jsdelivr.net':
# The API from JSDLIVR is powerful.
# It implies we compute a "fuzzy" version checking.
# For instance : "jquery@3" is OK for '3.2.1'.
tmp = self.parsed_result.path.split('/')
try:
if tmp[1] == 'npm':
self.name, version = tmp[2].split('@')
self._compare_with_npmjs_version(self.name, version)
elif tmp[1] == 'gh':
self.name, version = tmp[3].split('@')
self._compare_with_latest_github_tag(
tmp[2], self.name, version,
fuzzy_check=True
)
elif tmp[1] == 'wp':
# This how we'll handle the latest version references, as :
# <https://cdn.jsdelivr.net/wp/wp-slimstat/trunk/wp-slimstat.js>
if len(tmp) < 6:
raise IndexError
self.name = tmp[2]
self._compare_with_latest_wpsvn_tag(self.name, tmp[4])
else:
self.status = 'not_found'
except (ValueError, IndexError):
# This statement is here to handle `split()` errors.
# This page seems using a CDN without specifying a version.
self.status = 'to_update'
# CDN from (CDN.)?RAWGIT.COM will be handled here.
elif self.parsed_result.netloc in 'cdn.rawgit.com':
tmp = self.parsed_result.path.split('/')
self.name = tmp[2]
# If no semantic version is specified in the URL, we assume either:
# * The developer uses the latest version available (`master`) [OR]
# * The developer knows what he is doing (commit hash specified)
if not SEMVER_REGEXP_OBJECT.search(tmp[3]):
self.status = 'up_to_date'
else:
# If not, we compare this version with the latest tag !
self._compare_with_latest_github_tag(tmp[1], self.name, tmp[3])
elif self.parsed_result.netloc == 'code.ionicframework.com':
tmp = self.parsed_result.path.split('/')
self.name = tmp[1]
self._compare_with_latest_github_release('ionic-team', self.name, tmp[2])
elif self.parsed_result.netloc == 'use.fontawesome.com':
self.name = 'Font Awesome'
# We assume here that FA's CDN always serves the latest version.
self.status = 'up_to_date'
elif self.parsed_result.netloc == 'opensource.keycdn.com':
tmp = self.parsed_result.path.split('/')
self.name = tmp[1]
if self.name not in OPENSOURCE_KEYCDN_CORRESPONDENCES.keys():
log_message("{0} is currently not handled for this provider.".format(self.name))
self.status = 'not_found'
return
self._compare_with_latest_github_tag(
OPENSOURCE_KEYCDN_CORRESPONDENCES.get(self.name)['owner'],
OPENSOURCE_KEYCDN_CORRESPONDENCES.get(self.name)['name'],
tmp[2]
)
elif self.parsed_result.netloc == 'cdn.staticfile.org':
tmp = self.parsed_result.path.split('/')
self.name = tmp[1]
if self.name not in CDN_STATIC_FILE_CORRESPONDENCES.keys():
log_message("{0} is currently not handled for this provider.".format(self.name))
self.status = 'not_found'
return
self._compare_with_latest_github_tag(
CDN_STATIC_FILE_CORRESPONDENCES.get(self.name)['owner'],
CDN_STATIC_FILE_CORRESPONDENCES.get(self.name)['name'],
tmp[2]
)
elif self.parsed_result.netloc in ('ajax.microsoft.com', 'ajax.aspnetcdn.com'):
tmp = self.parsed_result.path.split('/')
# Sometimes the version is in the path...
if len(tmp) == 5:
version = tmp[3]
# ... and some other times contained within the name.
else:
version = SEMVER_REGEXP_OBJECT.search(tmp[3])
version = version and version.group(0)
if tmp[2] not in AJAX_MICROSOFT_CORRESPONDENCES.keys() or not version:
self.status = 'not_found'
return
self.name = tmp[2]
self._compare_with_latest_github_tag(
AJAX_MICROSOFT_CORRESPONDENCES.get(tmp[2])['owner'],
AJAX_MICROSOFT_CORRESPONDENCES.get(tmp[2])['name'],
version,
# Microsoft has tagged some libraries very badly...
# Check `CDNConstants.AJAX_MICROSOFT_CORRESPONDENCES` for this entry.
AJAX_MICROSOFT_CORRESPONDENCES.get(tmp[2]).get('fuzzy_check', False)
)
elif self.parsed_result.netloc == 'cdn.ckeditor.com':
tmp = self.parsed_result.path.split('/')
if tmp[1] == 'ckeditor5' and \
tmp[3] in ('classic', 'inline', 'balloon'):
self.name = "{0} ({1})".format(tmp[1], tmp[3])
self._compare_with_latest_github_release('ckeditor', tmp[1], tmp[2])
else:
self.status = 'not_found'
return
# Additional CDN providers may be handled there.
else:
log_message("This statement should not be reached.")
def _compare_with_latest_cdnjs_version(self, name, version):
"""This method handles call and result comparison with the CDNJS' API"""
# We ask CDNJS API to retrieve information about this library.
request = urlopen(
"https://api.cdnjs.com/libraries?search={name}&fields=version".format(
name=quote(name)
)
)
# If the request was a success...
if request.getcode() == 200:
# ... we fetch and decode the data from the payload.
data = json.loads(request.read().decode())
# We iterate on the results until we encounter a matching name.
for result in data['results']:
if result['name'] == name:
# We set here its name and version for future usages.
self.latest_version = result['version']
# ... let's compare its version with ours !
if self.latest_version == version:
self.status = 'up_to_date'
else:
self.status = 'to_update'
break
else:
self.status = 'not_found'
else:
self.status = 'not_found'
log_message(
"The HTTP response was not successful for \"{}\" ({}).".format(
request.geturl(),
request.getcode()
)
)
def _compare_with_latest_github_tag(
self,
owner, name, version,
fuzzy_check=False):
"""
This method fetches tags from the `owner/name` repository on GitHub...
... and compares it with `version`.
`self.status` will be set according to the previous comparison.
"""
request = urlopen(Request(
"https://api.github.com/repos/{owner}/{name}/tags".format(
owner=quote(owner),
name=quote(name)),
headers={
'Authorization': "token {}".format(self.settings.get('github_api_token'))
} if self.settings.get('github_api_token') else {}
))
if request.getcode() == 200:
data = json.loads(request.read().decode())
if len(data) >= 1:
self.latest_version = data[0]['name'].lstrip('v')
if (not fuzzy_check and self.latest_version == version) \
or self.latest_version.lower().find(version.lower(), 0) == 0:
self.status = 'up_to_date'
else:
self.status = 'to_update'
else:
# Should not be reached (GitHub issue or repository moved ?).
self.status = 'not_found'
else:
self.status = 'not_found'
log_message(
"The HTTP response was not successful for \"{}\" ({}).".format(
request.geturl(),
request.getcode()
)
)
def _compare_with_latest_github_release(
self,
owner, name, version,
fuzzy_check=False):
"""
This method fetches the latest release from the `owner/name`...
... repository on GitHub and compares it with `version`.
`self.status` will be set according to the previous comparison.
"""
request = urlopen(Request(
'https://api.github.com/repos/{owner}/{name}/releases/latest'
.format(
owner=quote(owner),
name=quote(name)
),
headers={
'Authorization': 'token ' + self.settings.get('github_api_token')
} if self.settings.get('github_api_token') else {}
))
if request.getcode() == 200:
data = json.loads(request.read().decode())
self.latest_version = data['tag_name'].lstrip('v')
if (not fuzzy_check and self.latest_version == version) \
or self.latest_version.lower().find(version.lower(), 0) == 0:
self.status = 'up_to_date'
else:
self.status = 'to_update'
else:
self.status = 'not_found'
log_message(
"The HTTP response was not successful for \"{}\" ({}).".format(
request.geturl(),
request.getcode()
)
)
def _compare_with_npmjs_version(self, name, version):
"""This method handles call and result comparison with the NPMJS' API"""
request = urlopen(Request(
"https://api.npms.io/v2/search?q={name}".format(name=quote(name)),
headers={ # The API of NPMJS blocks scripts, we need to spoof a real UA.
'User-Agent': "Mozilla/5.0(X11; U; Linux i686) Gecko/20071127 Firefox/2.0.0.11"
}
))
if request.getcode() == 200:
data = json.loads(request.read().decode())
if data['total'] >= 1 and \
data['results'][0]['package']['name'] == name and \
data['results'][0]['searchScore'] >= 100000:
self.latest_version = data['results'][0]['package']['version']
# "Fuzzy" version checking below !
if self.latest_version.find(version, 0) == 0:
self.status = 'up_to_date'
else:
self.status = 'to_update'
else:
self.status = 'not_found'
else:
self.status = 'not_found'
log_message(
"The HTTP response was not successful for \"{}\" ({}).".format(
request.geturl(),
request.getcode()
)
)
def _compare_with_latest_wpsvn_tag(self, name, version):
"""This method parses HTML from WordPress' SVN plugin page to retrieve the latest tag"""
request = urlopen(
"https://plugins.svn.wordpress.org/{name}/tags/".format(
name=quote(name)
)
)
if request.getcode() == 200:
# A f*cked-up one-liner to retrieve the latest version from SVN...
data = re.findall(
r"<li><a href=\".*\">(.*)<\/a><\/li>",
request.read().decode()
)
if len(data) >= 1:
self.latest_version = data[-1].rstrip('/')
if self.latest_version == version:
self.status = 'up_to_date'
else:
self.status = 'to_update'
else:
self.status = 'not_found'
else:
self.status = 'not_found'
log_message(
"The HTTP response was not successful for \"{}\" ({}).".format(
request.geturl(),
request.getcode()
)
)