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 
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
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()
This Function is used to Generate Token to access moodle account of the student.
object get_login(self, {str:str} data)
object post_REST(self, str function, {str:str} data=None)
def post_URL(self, str url, {str:str} data=None, str cookie_jar_path=None)
def _check_response_code(response)
def get_URL(self, str url, str cookie_jar_path=None)
float get_simple_moodle_version(self)
str _get_POST_DATA(str function, str token, str data_obj)
str _get_REST_POST_URL(str url_base, str function)
object _initial_parse(self, response)