spb/uqo-imm1033
Public
1#!/usr/bin/env python32"""Publication de fichiers sur Moodle@UQO en pilotant Safari (session Microsoft déjà ouverte) — recette du 2026-09-24,3généralisée le 2026-10-10. Chaque étape = un seul osascript (injection JS asynchrone + delay + lecture de window.__res).45Usage :6 moodle_publish.py add <course_id> <section> <url_pdf> "<nom>" "<description html>" [--before <cmid>]7 moodle_publish.py replace <cmid> <url_pdf> ["<nom>"] ["<description html>"]8 moodle_publish.py list <course_id>9Le fichier source est récupéré par fetch() depuis le site du cours (en-tête CORS Access-Control-Allow-Origin: * sur /files/).10"""11import json, subprocess, sys, time1213MOODLE = "https://moodle.uqo.ca"141516def osa(script):17 r = subprocess.run(["osascript", "-"], input=script, capture_output=True, text=True, encoding="utf-8")18 if r.returncode != 0:19 raise SystemExit("osascript: " + r.stderr.strip())20 return r.stdout.strip()212223def js_quote(s):24 """Chaîne JS (JSON, UTF-8 brut)."""25 return json.dumps(s, ensure_ascii=False)262728def as_quote(s):29 """Chaîne AppleScript : seuls \\ et \" sont échappés (osascript lit l'UTF-8 tel quel)."""30 return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'313233def step(url, js, delay=8, poll=12):34 """Navigue vers url (si non vide), injecte js (doit poser window.__res), attend, retourne window.__res."""35 nav = f'set URL of current tab of front window to "{url}"\n delay 7' if url else ""36 script = f'''tell application "Safari"37 {nav}38 do JavaScript {as_quote("window.__res=null;" + js)} in current tab of front window39 delay {delay}40 set out to ""41 repeat {poll} times42 set out to do JavaScript "window.__res===null?'':String(window.__res)" in current tab of front window43 if out is not "" then exit repeat44 delay 245 end repeat46 return out47end tell'''48 return osa(script)495051def upload_js(pdf_url, name, desc, submit=True):52 return f'''(async function(){{53 try {{54 var it=document.querySelector('input[name="files"]').value;55 var cid=document.querySelector('.filemanager').id.replace('filemanager-','');56 var ctx=M.cfg.contextid, sk=M.cfg.sesskey;57 // 1. vider le brouillon (remplacement)58 var fdl=new FormData(); fdl.append('sesskey',sk); fdl.append('client_id',cid); fdl.append('itemid',it); fdl.append('filepath','/');59 var lst=await (await fetch('/repository/draftfiles_ajax.php?action=list',{{method:'POST',body:fdl,credentials:'same-origin'}})).json();60 for (var f of (lst.list||[])) {{ var fdd=new FormData(); fdd.append('sesskey',sk); fdd.append('client_id',cid); fdd.append('itemid',it); fdd.append('filepath',f.filepath||'/'); fdd.append('filename',f.filename); await fetch('/repository/draftfiles_ajax.php?action=delete',{{method:'POST',body:fdd,credentials:'same-origin'}}); }}61 // 2. téléverser62 var src={js_quote(pdf_url)}; var fname=decodeURIComponent(src.split('/').pop());63 var blob=await (await fetch(src+(src.indexOf('?')<0?'?':'&')+'v='+Date.now(),{{mode:'cors',cache:'no-store'}})).blob();64 var fd=new FormData(); fd.append('repo_upload_file',blob,fname); fd.append('sesskey',sk); fd.append('repo_id','3'); fd.append('itemid',it); fd.append('savepath','/'); fd.append('title',''); fd.append('author',''); fd.append('license','allrightsreserved'); fd.append('ctx_id',ctx); fd.append('client_id',cid); fd.append('overwrite','1');65 var up=await (await fetch('/repository/repository_ajax.php?action=upload',{{method:'POST',body:fd,credentials:'same-origin'}})).json();66 if (up.error) {{ window.__res='UPLOAD ERROR '+JSON.stringify(up); return; }}67 // 3. nom + description68 var nm={js_quote(name)}; if (nm) document.querySelector('#id_name').value=nm;69 var ds={js_quote(desc)}; if (ds && window.tinyMCE && tinyMCE.get('id_introeditor')) tinyMCE.get('id_introeditor').setContent(ds);70 var sd=document.querySelector('#id_showdescription'); if (sd && ds) sd.checked=true;71 var ss=document.querySelector('#id_showsize'); if (ss) ss.checked=true;72 var st=document.querySelector('#id_showtype'); if (st) st.checked=true;73 window.__res='UPLOADED '+JSON.stringify({{file:up.file||up.title||fname,size:blob.size}});74 if ({str(submit).lower()}) setTimeout(function(){{document.querySelector('#id_submitbutton2').click();}},500);75 }} catch(e) {{ window.__res='ERROR '+e; }}76}})();'''777879def list_course(course_id):80 js = '''(function(){var out=[];document.querySelectorAll('li.section').forEach(function(s){var name=(s.querySelector('.sectionname, h3')||{}).textContent||'';s.querySelectorAll('li.activity').forEach(function(m){out.push((s.id||'')+'|'+(m.id||'')+'|'+((m.querySelector('.activityname')||m.querySelector('.instancename')||{textContent:''}).textContent||'').trim().slice(0,90));});});window.__res=out.join('\\n');})();'''81 return step(f"{MOODLE}/course/view.php?id={course_id}", js, delay=2, poll=3)828384def find_cmid(course_id, name):85 for line in list_course(course_id).splitlines():86 parts = line.split('|')87 if len(parts) >= 3 and parts[2].startswith(name[:60]):88 return int(parts[1].replace('module-', ''))89 return None909192def move_before(course_id, cmid, target_cmid):93 js = f'''(async function(){{try{{var r=await (await fetch('/lib/ajax/service.php?sesskey='+M.cfg.sesskey+'&info=core_courseformat_update_course',{{method:'POST',credentials:'same-origin',headers:{{'Content-Type':'application/json'}},body:JSON.stringify([{{index:0,methodname:'core_courseformat_update_course',args:{{action:'cm_move',courseid:{course_id},ids:[{cmid}],targetcmid:{target_cmid}}}}}])}})).text();window.__res='MOVED '+r.slice(0,120);}}catch(e){{window.__res='ERROR '+e}}}})();'''94 return step(f"{MOODLE}/course/view.php?id={course_id}", js, delay=4, poll=4)959697def add(course_id, section, pdf_url, name, desc, before=None):98 url = f"{MOODLE}/course/modedit.php?add=resource&type=&course={course_id}§ion={section}&return=0&sr=0"99 r = step(url, upload_js(pdf_url, name, desc), delay=12, poll=10)100 print("add:", r or "(page soumise avant la lecture du résultat)")101 time.sleep(8)102 cmid = find_cmid(course_id, name)103 print("cmid:", cmid)104 if not cmid:105 raise SystemExit("échec : ressource introuvable après le téléversement")106 if before and cmid:107 print(move_before(course_id, cmid, before))108 return cmid109110111def replace(cmid, pdf_url, name="", desc=""):112 url = f"{MOODLE}/course/modedit.php?update={cmid}&return=1"113 r = step(url, upload_js(pdf_url, name, desc), delay=12, poll=10)114 print("replace:", r)115 return r116117118if __name__ == "__main__":119 a = sys.argv[1:]120 if not a:121 print(__doc__); sys.exit(0)122 if a[0] == "list":123 print(list_course(int(a[1])))124 elif a[0] == "add":125 before = int(a[a.index("--before") + 1]) if "--before" in a else None126 add(int(a[1]), int(a[2]), a[3], a[4], a[5] if len(a) > 5 and a[5] != "--before" else "", before)127 elif a[0] == "replace":128 replace(int(a[1]), a[2], a[3] if len(a) > 3 else "", a[4] if len(a) > 4 else "")129