moodleNotifer

generatetoken.py
1 import re
2 import os
3 import json
4 import urllib
5 import urllib3
6 import requests
7 import logging
8
9 from http.cookiejar import MozillaCookieJar
10 from urllib.parse import urlparse
11 from getpass import getpass
12
13 moodle_domain = "moodle.iitb.ac.in"
14 moodle_path = "/"
15
16
19
20 class RequestHelper:
21 """
22 Encapsulates the recurring logic for sending out requests to the
23 Moodle-System.
24 """
25
26 stdHeader = {
27 'User-Agent': (
28 'Mozilla/5.0 (Linux; Android 7.1.1; Moto G Play Build/NPIS26.48-43-2; wv) AppleWebKit/537.36'
29 + ' (KHTML, like Gecko) Version/4.0 Chrome/71.0.3578.99 Mobile Safari/537.36 MoodleMobile'
30 ),
31 'Content-Type': 'application/x-www-form-urlencoded',
32 }
33
34 def __init__(
35 self,
36 moodle_domain: str,
37 moodle_path: str = '/',
38 token: str = '',
39 skip_cert_verify: bool = False,
40 log_responses_to: str = None,
41 ):
42 self.tokentoken = token
43 self.moodle_domainmoodle_domain = moodle_domain
44 self.moodle_pathmoodle_path = moodle_path
45
46 self.verifyverify = not skip_cert_verify
47 self.url_baseurl_base = 'https://' + moodle_domain + moodle_path
48
49 self.log_responses_tolog_responses_to = log_responses_to
50 self.log_responseslog_responses = False
51
52 if log_responses_to is not None:
53 self.log_responseslog_responses = True
54 with open(self.log_responses_tolog_responses_to, 'w') as response_log_file:
55 response_log_file.write('JSON Log:\n\n')
56
57 logging.getLogger("requests").setLevel(logging.WARNING)
58 logging.getLogger("urllib3").setLevel(logging.WARNING)
59 urllib3.disable_warnings()
60 # logging.captureWarnings(True)
61
62 def post_URL(self, url: str, data: {str: str} = None, cookie_jar_path: str = None):
63 """
64 Sends a POST request to a specific URL, including saving of cookies in cookie jar.
65 @param url: The url to which the request is sent. (the moodle base url is not added to the given URL)
66 @param data: The optional data is added to the POST body.
67 @param cookie_jar_path: Path to the cookies file.
68 @return: The resulting response object and the session object.
69 """
70
71 data_urlencoded = ""
72 if data is not None:
73 data_urlencoded = RequestHelper.recursive_urlencode(data)
74
75 session = requests.Session()
76
77 if cookie_jar_path is not None:
78 session.cookies = MozillaCookieJar(cookie_jar_path)
79
80 if os.path.exists(cookie_jar_path):
81 session.cookies.load(ignore_discard=True, ignore_expires=True)
82
83 response = session.post(url, data=data_urlencoded, headers=self.stdHeaderstdHeader, verify=self.verifyverify)
84
85 if cookie_jar_path is not None:
86 for cookie in session.cookies:
87 cookie.expires = 2147483647
88
89 session.cookies.save(ignore_discard=True, ignore_expires=True)
90
91 return response, session
92
93 def get_URL(self, url: str, cookie_jar_path: str = None):
94 """
95 Sends a GET request to a specific URL of the Moodle system, including additional cookies
96 (cookies are updated after the request)
97 @param url: The url to which the request is sent. (the moodle base url is not added to the given URL)
98 @param cookie_jar_path: The optional cookies to add to the request
99 @return: The resulting Response object.
100 """
101
102 session = requests.Session()
103
104 if cookie_jar_path is not None:
105 session.cookies = MozillaCookieJar(cookie_jar_path)
106
107 if os.path.exists(cookie_jar_path):
108 session.cookies.load(ignore_discard=True, ignore_expires=True)
109
110 response = session.get(url, headers=self.stdHeaderstdHeader, verify=self.verifyverify)
111
112 if cookie_jar_path is not None:
113 session.cookies.save(ignore_discard=True, ignore_expires=True)
114
115 return response, session
116
117 def post_REST(self, function: str, data: {str: str} = None) -> object:
118 """
119 Sends a POST request to the REST endpoint of the Moodle system
120 @param function: The Web service function to be called.
121 @param data: The optional data is added to the POST body.
122 @return: The JSON response returned by the Moodle system, already
123 checked for errors.
124 """
125
126 if self.tokentoken is None:
127 raise ValueError('The required Token is not set!')
128
129 data_urlencoded = self._get_POST_DATA_get_POST_DATA(function, self.tokentoken, data)
130 url = self._get_REST_POST_URL_get_REST_POST_URL(self.url_baseurl_base, function)
131
132 response = requests.post(url, data=data_urlencoded, headers=self.stdHeaderstdHeader, verify=self.verifyverify)
133 json_result = self._initial_parse_initial_parse(response)
134
135 if self.log_responseslog_responses and function not in ['tool_mobile_get_autologin_key']:
136 with open(self.log_responses_tolog_responses_to, 'a') as response_log_file:
137 response_log_file.write('URL: {}\n'.format(response.url))
138 response_log_file.write('Function: {}\n\n'.format(function))
139 response_log_file.write('Data: {}\n\n'.format(data))
140 response_log_file.write(json.dumps(json_result, indent=4, ensure_ascii=False))
141 response_log_file.write('\n\n\n')
142
143 return json_result
144
145 @staticmethod
146 def _get_REST_POST_URL(url_base: str, function: str) -> str:
147 """
148 Generates an URL for a REST-POST request
149 @params: The necessary parameters for a REST URL
150 @return: A formatted URL
151 """
152 url = '%swebservice/rest/server.php?moodlewsrestformat=json&wsfunction=%s' % (url_base, function)
153
154 return url
155
156 @staticmethod
157 def _get_POST_DATA(function: str, token: str, data_obj: str) -> str:
158 """
159 Generates the data for a REST-POST request
160 @params: The necessary parameters for a REST URL
161 @return: A URL-encoded data string
162 """
163 data = {'moodlewssettingfilter': 'true', 'moodlewssettingfileurl': 'true'}
164
165 if data_obj is not None:
166 data.update(data_obj)
167
168 data.update({'wsfunction': function, 'wstoken': token})
169
170 return RequestHelper.recursive_urlencode(data)
171
172 def get_login(self, data: {str: str}) -> object:
173 """
174 Sends a POST request to the login endpoint of the Moodle system to
175 obtain a token in JSON format.
176 @param data: The data is inserted into the Post-Body as arguments. This
177 should contain the login data.
178 @return: The JSON response returned by the Moodle System, already
179 checked for errors.
180 """
181
182 response = requests.post(
183 '%slogin/token.php' % (self.url_baseurl_base),
184 data=urllib.parse.urlencode(data),
185 headers=self.stdHeaderstdHeader,
186 verify=self.verifyverify,
187 )
188
189 return self._initial_parse_initial_parse(response)
190
191 @staticmethod
192 def _check_response_code(response):
193 # Normally Moodle answer with response 200
194 if response.status_code != 200:
195 raise RuntimeError(
196 'An Unexpected Error happened on side of the Moodle System!'
197 + (' Status-Code: %s' % str(response.status_code))
198 + ('\nHeader: %s' % response.headers)
199 + ('\nResponse: %s' % response.text)
200 )
201
202 def get_simple_moodle_version(self) -> float:
203 """
204 Query the version by looking up the change-log (/lib/upgrade.txt)
205 of the Moodle
206 @return: a float number representing the newest version
207 parsed from the change-log
208 """
209
210 url = '%slib/upgrade.txt' % (self.url_baseurl_base)
211 response = requests.get(url, headers=self.stdHeaderstdHeader, verify=self.verifyverify)
212
213 self._check_response_code_check_response_code(response)
214
215 changelog = str(response.text).split('\n')
216 version_string = '1'
217 for line in changelog:
218 match = re.match(r'^===\s*([\d\.]+)\s*===$', line)
219 if match:
220 version_string = match.group(1)
221 break
222
223 majorVersion = version_string.split('.')[0]
224 minorVersion = version_string[len(majorVersion) :].replace('.', '')
225
226 version = float(majorVersion + '.' + minorVersion)
227 return version
228
229 def _initial_parse(self, response) -> object:
230 """
231 The first time parsing the result of a REST request.
232 It is checked for known errors.
233 @param response: The JSON response of the Moodle system
234 @return: The parsed JSON object
235 """
236
237 self._check_response_code_check_response_code(response)
238
239 # Try to parse the JSON
240 try:
241 response_extracted = response.json()
242 #print(response_extracted)
243 except Exception as error:
244 raise RuntimeError(
245 'An Unexpected Error occurred while trying'
246 + ' to parse the json response! Moodle'
247 + ' response: %s.\nError: %s' % (response.read(), error)
248 )
249 # Check for known errors
250 if 'error' in response_extracted:
251 error = response_extracted.get('error', '')
252 errorcode = response_extracted.get('errorcode', '')
253 stacktrace = response_extracted.get('stacktrace', '')
254 debuginfo = response_extracted.get('debuginfo', '')
255 reproductionlink = response_extracted.get('reproductionlink', '')
256 raise RequestRejectedError(
257 'The Moodle System rejected the Request.'
258 + (' Details: %s (Errorcode: %s, ' % (error, errorcode))
259 + ('Stacktrace: %s, Debuginfo: %s, Reproductionlink: %s)' % (stacktrace, debuginfo, reproductionlink))
260 )
261
262 if 'exception' in response_extracted:
263 exception = response_extracted.get('exception', '')
264 errorcode = response_extracted.get('errorcode', '')
265 message = response_extracted.get('message', '')
266
267 raise RequestRejectedError(
268 'The Moodle System rejected the Request.'
269 + ' Details: %s (Errorcode: %s, Message: %s)' % (exception, errorcode, message)
270 )
271
272 return response_extracted
273
274 @staticmethod
275 def recursive_urlencode(data):
276 """URL-encode a multidimensional dictionary.
277 @param data: the data to be encoded
278 @returns: the url encoded data
279 """
280
281 def recursion(data, base=[]):
282 pairs = []
283
284 for key, value in data.items():
285 new_base = base + [key]
286 if hasattr(value, 'values'):
287 pairs += recursion(value, new_base)
288 else:
289 new_pair = None
290 if len(new_base) > 1:
291 first = urllib.parse.quote(new_base.pop(0))
292 rest = map(lambda x: urllib.parse.quote(x), new_base)
293 new_pair = '%s[%s]=%s' % (first, ']['.join(rest), urllib.parse.quote(str(value)))
294 else:
295 new_pair = '%s=%s' % (urllib.parse.quote(str(key)), urllib.parse.quote(str(value)))
296 pairs.append(new_pair)
297 return pairs
298
299 return '&'.join(recursion(data))
300
301 def obtain_login_token(
302 username: str, password: str, moodle_domain: str, moodle_path: str = '/', skip_cert_verify: bool = False
303 ) -> str:
304 """
305 Send the login credentials to the Moodle-System and extracts the resulting Login-Token.
306 @params: The necessary parameters to create a Token.
307 @return: The received token.
308 """
309 login_data = {'username': username, 'password': password, 'service': 'moodle_mobile_app'}
310
311 obj = RequestHelper(moodle_domain, moodle_path, skip_cert_verify=skip_cert_verify)
312 response = obj.get_login(login_data)
313
314 if 'token' not in response:
315 # = we didn't get an error page (checked by the RequestHelper) but
316 # somehow we don't have the needed token
317 raise RuntimeError('Invalid response received from the Moodle System! No token was received.')
318
319 if 'privatetoken' not in response:
320 return response.get('token', ''), None
321 else:
322 return response.get('token', ''), response.get('privatetoken', ''), obj
323
324 def _split_moodle_uri(moodle_uri: str):
325
326 moodle_domain = moodle_uri.netloc
327 moodle_path = moodle_uri.path
328 if not moodle_path.endswith('/'):
329 moodle_path = moodle_path + '/'
330
331 if moodle_path == '':
332 moodle_path = '/'
333
334 return moodle_domain, moodle_path
335
336 def interactively_acquire_token(
337 username: str = None, password: str = None
338 ) -> str:
339 """
340 Walks the user through executing a login into the Moodle-System to get
341 the Token and saves it.
342 @return: The Token for Moodle.
343 """
344 moodle_token = None
345 moodle_username = input("Enter Moodle Username : ")
346 moodle_password = getpass()
347 moodle_token, moodle_privatetoken, obj = obtain_login_token(
348 moodle_username, moodle_password, moodle_domain, moodle_path, False
349 )
350 print(moodle_token)
351
352 interactively_acquire_token()
generatetoken.RequestHelper
This Function is used to Generate Token to access moodle account of the student.
Definition: generatetoken.py:20
generatetoken.RequestHelper.token
token
Definition: generatetoken.py:42
generatetoken.RequestHelper.moodle_domain
moodle_domain
Definition: generatetoken.py:43
generatetoken.RequestHelper.get_login
object get_login(self, {str:str} data)
Definition: generatetoken.py:172
generatetoken.RequestHelper.post_REST
object post_REST(self, str function, {str:str} data=None)
Definition: generatetoken.py:117
generatetoken.RequestHelper.post_URL
def post_URL(self, str url, {str:str} data=None, str cookie_jar_path=None)
Definition: generatetoken.py:62
generatetoken.RequestHelper.stdHeader
dictionary stdHeader
Definition: generatetoken.py:26
generatetoken.RequestHelper._check_response_code
def _check_response_code(response)
Definition: generatetoken.py:192
generatetoken.RequestHelper.moodle_path
moodle_path
Definition: generatetoken.py:44
generatetoken.RequestHelper.recursive_urlencode
def recursive_urlencode(data)
Definition: generatetoken.py:275
generatetoken.RequestHelper.log_responses_to
log_responses_to
Definition: generatetoken.py:49
generatetoken.RequestHelper.verify
verify
Definition: generatetoken.py:46
generatetoken.RequestHelper.get_URL
def get_URL(self, str url, str cookie_jar_path=None)
Definition: generatetoken.py:93
generatetoken.RequestHelper.get_simple_moodle_version
float get_simple_moodle_version(self)
Definition: generatetoken.py:202
generatetoken.RequestHelper._get_POST_DATA
str _get_POST_DATA(str function, str token, str data_obj)
Definition: generatetoken.py:157
generatetoken.RequestHelper._get_REST_POST_URL
str _get_REST_POST_URL(str url_base, str function)
Definition: generatetoken.py:146
generatetoken.RequestHelper.log_responses
log_responses
Definition: generatetoken.py:50
generatetoken.RequestHelper._initial_parse
object _initial_parse(self, response)
Definition: generatetoken.py:229
generatetoken.RequestHelper.url_base
url_base
Definition: generatetoken.py:47