#!/usr/bin/env python3 """Publication de fichiers sur Moodle@UQO en pilotant Safari (session Microsoft déjà ouverte) — recette du 2026-09-24, généralisée le 2026-10-10. Chaque étape = un seul osascript (injection JS asynchrone + delay + lecture de window.__res). Usage : moodle_publish.py add
"" "" [--before ] moodle_publish.py replace [""] [""] moodle_publish.py list Le fichier source est récupéré par fetch() depuis le site du cours (en-tête CORS Access-Control-Allow-Origin: * sur /files/). """ import json, subprocess, sys, time MOODLE = "https://moodle.uqo.ca" def osa(script): r = subprocess.run(["osascript", "-"], input=script, capture_output=True, text=True, encoding="utf-8") if r.returncode != 0: raise SystemExit("osascript: " + r.stderr.strip()) return r.stdout.strip() def js_quote(s): """Chaîne JS (JSON, UTF-8 brut).""" return json.dumps(s, ensure_ascii=False) def as_quote(s): """Chaîne AppleScript : seuls \\ et \" sont échappés (osascript lit l'UTF-8 tel quel).""" return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"' def step(url, js, delay=8, poll=12): """Navigue vers url (si non vide), injecte js (doit poser window.__res), attend, retourne window.__res.""" nav = f'set URL of current tab of front window to "{url}"\n delay 7' if url else "" script = f'''tell application "Safari" {nav} do JavaScript {as_quote("window.__res=null;" + js)} in current tab of front window delay {delay} set out to "" repeat {poll} times set out to do JavaScript "window.__res===null?'':String(window.__res)" in current tab of front window if out is not "" then exit repeat delay 2 end repeat return out end tell''' return osa(script) def upload_js(pdf_url, name, desc, submit=True): return f'''(async function(){{ try {{ var it=document.querySelector('input[name="files"]').value; var cid=document.querySelector('.filemanager').id.replace('filemanager-',''); var ctx=M.cfg.contextid, sk=M.cfg.sesskey; // 1. vider le brouillon (remplacement) var fdl=new FormData(); fdl.append('sesskey',sk); fdl.append('client_id',cid); fdl.append('itemid',it); fdl.append('filepath','/'); var lst=await (await fetch('/repository/draftfiles_ajax.php?action=list',{{method:'POST',body:fdl,credentials:'same-origin'}})).json(); 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'}}); }} // 2. téléverser var src={js_quote(pdf_url)}; var fname=decodeURIComponent(src.split('/').pop()); var blob=await (await fetch(src+(src.indexOf('?')<0?'?':'&')+'v='+Date.now(),{{mode:'cors',cache:'no-store'}})).blob(); 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'); var up=await (await fetch('/repository/repository_ajax.php?action=upload',{{method:'POST',body:fd,credentials:'same-origin'}})).json(); if (up.error) {{ window.__res='UPLOAD ERROR '+JSON.stringify(up); return; }} // 3. nom + description var nm={js_quote(name)}; if (nm) document.querySelector('#id_name').value=nm; var ds={js_quote(desc)}; if (ds && window.tinyMCE && tinyMCE.get('id_introeditor')) tinyMCE.get('id_introeditor').setContent(ds); var sd=document.querySelector('#id_showdescription'); if (sd && ds) sd.checked=true; var ss=document.querySelector('#id_showsize'); if (ss) ss.checked=true; var st=document.querySelector('#id_showtype'); if (st) st.checked=true; window.__res='UPLOADED '+JSON.stringify({{file:up.file||up.title||fname,size:blob.size}}); if ({str(submit).lower()}) setTimeout(function(){{document.querySelector('#id_submitbutton2').click();}},500); }} catch(e) {{ window.__res='ERROR '+e; }} }})();''' def list_course(course_id): 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');})();''' return step(f"{MOODLE}/course/view.php?id={course_id}", js, delay=2, poll=3) def find_cmid(course_id, name): for line in list_course(course_id).splitlines(): parts = line.split('|') if len(parts) >= 3 and parts[2].startswith(name[:60]): return int(parts[1].replace('module-', '')) return None def move_before(course_id, cmid, target_cmid): 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}}}})();''' return step(f"{MOODLE}/course/view.php?id={course_id}", js, delay=4, poll=4) def add(course_id, section, pdf_url, name, desc, before=None): url = f"{MOODLE}/course/modedit.php?add=resource&type=&course={course_id}§ion={section}&return=0&sr=0" r = step(url, upload_js(pdf_url, name, desc), delay=12, poll=10) print("add:", r or "(page soumise avant la lecture du résultat)") time.sleep(8) cmid = find_cmid(course_id, name) print("cmid:", cmid) if not cmid: raise SystemExit("échec : ressource introuvable après le téléversement") if before and cmid: print(move_before(course_id, cmid, before)) return cmid def replace(cmid, pdf_url, name="", desc=""): url = f"{MOODLE}/course/modedit.php?update={cmid}&return=1" r = step(url, upload_js(pdf_url, name, desc), delay=12, poll=10) print("replace:", r) return r if __name__ == "__main__": a = sys.argv[1:] if not a: print(__doc__); sys.exit(0) if a[0] == "list": print(list_course(int(a[1]))) elif a[0] == "add": before = int(a[a.index("--before") + 1]) if "--before" in a else None add(int(a[1]), int(a[2]), a[3], a[4], a[5] if len(a) > 5 and a[5] != "--before" else "", before) elif a[0] == "replace": replace(int(a[1]), a[2], a[3] if len(a) > 3 else "", a[4] if len(a) > 4 else "")