diff --git a/mycampusscript.py b/mycampusscript.py index 60ecc153c76a93a90d521938c0e114d0fd317508..21fe4d4e00c406ab485c80ec2b5241106a7ca465 100644 --- a/mycampusscript.py +++ b/mycampusscript.py @@ -1,3 +1,24 @@ +""" +Works roughly as follows: +1. Overall, we script a POST request (RQmain) to a single, complex form to create a Sakai Assignment. +2. However, the form includes one or more uploaded documents and those uploads (must) happen upfront + (RQupload). +3. Worse, the overall form must be sent upfront of even the uploads (RQmain.0)! + At least that is what happens when one opens the upload dialog. A symptom is the fact that one + cannot open the upload dialog unless one has entered a title for the Assignment. +4. This appears to be needed because the URL created for each uploaded document contains an ID + for the assignment. +5. Reverse engineering the details of this is difficult, because GETting the page on which the form resides + (RQmain.get) involves about 40 requests. +6. Therefore, we do not yet know where the identitity of the Assignment freshly created in step 3 + is to be found. Our best guess is: Somewhere in the resulting HTML page. +7. Likewise for the URLs of the uploaded documents: They have to be sent as part of the big + (>80 parameters) RQmain request, but the URL is not a main outcome of RQupload. +8. Although the upload dialog is a separate window on top of the RQmain form, RQupload does not + only close that window, it also reloads the RQmain page, presumably to include the freshly uploaded + file in the list of files shown thereon. + +""" import collections.abc import datetime as dt import mimetypes @@ -101,6 +122,10 @@ ReplacementsFunc = collections.abc.Callable[[StrAnyDict], StrStrDict] def main(scriptname: str, cmdname: str, configfile: str): + investigate_jsonlog("/c/temp/irgendwas3.json") + return + + with open(configfile, 'rt') as f: config = yaml.load(f, Loader=yaml.Loader) site_url: str = find_value_or_help(config, 'site_url') @@ -246,8 +271,8 @@ def upload_attachment(config: StrAnyDict, filepath: str): See c:/temp/a.json log.entries[164]. """ base_url = f"{config['site_url']}/{ASSIGNMENTS_CREATION_ATTACHMENT_UPLOAD_PATH}" - query_args = dict(special='upload', panel='Main', sakai_action='doAttachupload', - csrf_token=config[CSRF_TOKEN_CONFIGENTRY_NAME]) + query_args = dict(special='upload', sakai_action='doAttachupload', # panel='Main', ? + sakai_csrf_token=config[CSRF_TOKEN_CONFIGENTRY_NAME]) # TODO 1: continue here: # We next need to construct the multipart body of the POST request. # See "POST Multiple Multipart-Encoded Files" in https://requests.readthedocs.io/en/latest/user/advanced/ @@ -258,6 +283,7 @@ def upload_attachment(config: StrAnyDict, filepath: str): # Das JSON ist dafür unpraktisch, weil der ganze multipart-Rumpf nur als Text dasteht. # In der Word-Datei findet sich die aufgeschlüsselte Form, die der Browser dafür anzeigt. # Erzeugung eines solchen Requests, siehe https://stackoverflow.com/a/12385661 + headers = dict(Referer=f"{config['site_url']}/{ASSIGNMENTS_CREATION_ATTACHMENT_UPLOAD_PATH}") filename = os.path.basename(filepath) content_type, encoding = mimetypes.guess_type(filename) multipart_body = dict( @@ -273,7 +299,54 @@ def upload_attachment(config: StrAnyDict, filepath: str): sakai_csrf_token=config[CSRF_TOKEN_CONFIGENTRY_NAME] ) multipart_body['from'] = 'list' # separate because 'from' is a keyword - requests.post(...) + resp = requests.post(base_url, + params=query_args, + headers=headers, + files=multipart_body, + cookies={SESSIONCOOKIE_NAME: config['cookies'][SESSIONCOOKIE_NAME]}) + print(resp.text) + + +def investigate_jsonlog(jsonfile: str): + """ + Helper routine for reverse engineering: + We record a complete RQmain-posting episode with two attachments in Firefox + (F12->Network; cogwheel menu->Persist Logs; perform episode; SaveAs:irgendwas.json). + We pull out parts needed for understanding here. + """ + import json + import dictns + testdata = dictns.Namespace(dict( + title="irgendwas3", + a_id='dbb411b5-195d-4089-ae66-6e9c8f01b81c', + instructions="irgendwelche Anleitung", + attachments=["ajeunwuk", "akeunwuk"], + group="Übung 01", # name prefix only + )) + with open(jsonfile, 'r') as f: + log = json.load(f) + ns = dictns.Namespace(log) + # the first RQmain POST request is #111, response is 302 to panel=Main + firstrequest = ns.log.entries[111] + assert firstrequest.request.postData.params[5].value == testdata.title + # ns.log.entries[112] is another 302 to ASSIGNMENTS_CREATION_ATTACHMENT_UPLOAD_PATH + # popuprequest = ns.log.entries[113] + url = urllib.parse.urlparse("http://localhost") + for i in range(len(ns.log.entries)): # range(111, 178+1): + entry = ns.log.entries[i] + if "portal/site" not in entry.request.url: + continue + last_url = url + url = urllib.parse.urlparse(entry.request.url) + if url == last_url: + msg = "(ditto)" + elif url.path == last_url.path: + msg = url.query + else: + msg = entry.request.url + print(i, entry.response.status, msg) + + # print(popuprequest.response.content.text) def site_id(site_url: str) -> str: @@ -287,6 +360,8 @@ def slurp(filename: str) -> bytes: with open(filename, 'rb') as f: return f.read() + + if __name__ == '__main__': if len(sys.argv) != 2+1 or sys.argv[1] != 'create_multigroup_assgmt': print(usage)