Greasy Fork is available in English.

tame QTKJ

at the end of with it.

Verze ze dne 25. 01. 2020. Zobrazit nejnovější verzi.

// ==UserScript==
// @name          tame QTKJ
// @namespace     Vionlentmonkey
// @version       2.13.3
// @description   at the end of with it.
// @author        someone
// @icon          http://www.moj.gov.cn/favicon.ico

// @match         http://218-94-1-181.sft.ipv6.jiangsu.gov.cn:8087/sfxzwsxy/*
// @match         http://218-94-1-179.sft.ipv6.jiangsu.gov.cn:8087/sfxzwsxy/*
// @match         http://218-94-1-175.sft.ipv6.jiangsu.gov.cn:8087/sfxzwsxy/*

// @match         http://218.94.1.181:8087/sfxzwsxy/*
// @match         http://218.94.1.179:8087/sfxzwsxy/*
// @match         http://218.94.1.175:8087/sfxzwsxy/*

// @match         http://218.94.1.181:5088/unzipapp/project/ware/attach/*

// @require       https://openuserjs.org/src/libs/sizzle/GM_config.js
// @require       https://cdn.jsdelivr.net/npm/vm.shortcut
// @require       https://greasyfork.org/scripts/381401-addstyle/code/addStyle.js
// @require       https://greasyfork.org/scripts/381403-fakenavigators/code/fakeNavigators.js

// @grant         GM_getValue
// @grant         GM_setValue
// @grant         GM_registerMenuCommand
// @grant         GM_openInTab
// @grant         GM_notification

// @run-at        document-start

// ==/UserScript==

const body = document.body || document.documentElement;

const windowCSS = `
#tameCfg {background-color: lightblue;width: auto;}
#tameCfg .reset_holder {float: left; position: relative; bottom: -1em;}
#tameCfg .saveclose_buttons {margin: 1em;}
`;

const openCfg = () => {
  // 避免在包含框架的页面打开造成多个设置界面重叠
  if (window.top === window.self) {
    GM_config.open();
  }
};

GM_registerMenuCommand('江苏省司法行政网上学院个性化设置', openCfg);

GM_config.init({
  id: 'tameCfg',
  title: '江苏省司法行政网上学院个性化设置',
  fields: {
    loginName: {
      section: ['登录', '完整填写尝试自动登录'],
      label: '账号',
      labelPos: 'right',
      type: 'text',
      default: ''
    },
    pwd: {
      label: '密码',
      labelPos: 'right',
      type: 'password',
      default: ''
    },
    reload: {
      label: '是否无限尝试',
      labelPos: 'right',
      type: 'checkbox',
      default: false
    },
    open_unclose: {
      section: ['操作', '后台批量打开新标签页'],
      label: '快捷键',
      labelPos: 'right',
      type: 'text',
      default: 'F8'
    },
    batch: {
      label: '一次性批量打开',
      labelPos: 'right',
      type: 'int',
      default: 10
    }
  },
  css: windowCSS,
  events: {
    save: () => {
      GM_config.close();
      // 服务器选择页面或登录页面自动刷新
      if (
        location.pathname.match(/\/sfxzwsxy\/?(serverSelect.jsp)?#?$/i) ||
        location.pathname.startsWith('/sfxzwsxy/index.jsp')
      ) {
        location.reload(true);
      }
    }
  }
});

// 登录后可获取数据
// 用户学习数据:http://218.94.1.175:8087/sfxzwsxy/jypxks/ajax/index_ajax.jsp?reqType=4
const getUserData = async () => {
  let response = await fetch(`${location.origin}/sfxzwsxy/jypxks/ajax/index_ajax.jsp?reqType=4`, {
    method: 'POST'
  });
  let jsonData = await response.json();
  let jsonDataObj = jsonData[0];
  return jsonDataObj;
  /*
  let stime = jsonDataObj.stime; //学年开始时间
  let etime = jsonDataObj.etime; //学年结束时间
  let total_hour = jsonDataObj.total_hour; //规定需达到的总学时
  let required_hour = jsonDataObj.required_hour; //规定需达到的必修学时
  let required_credit = jsonDataObj.required_credit; //规定需达到的总学分
  let user_total_hour = jsonDataObj.user_total_hour || 0; //用户已获得的总学时
  let user_required_hour = jsonDataObj.user_required_hour || 0; //用户已获得的必修学时
  let user_required_credit = jsonDataObj.user_required_credit || 0; //用户已获得的总学分
  let user_integral = jsonDataObj.user_integral || 0; //用户已获得的的总积分
  let syn_total_hour = jsonDataObj.syn_total_hour || 0; //从省委组织部同步到的总学时
  let syn_required_hour = jsonDataObj.syn_required_hour || 0; //从省委组织部同步到的的必修学时
  let show_total_hour = parseFloat(user_total_hour) + parseFloat(syn_total_hour);
  let show_total_required_hour = parseFloat(user_required_hour) + parseFloat(syn_required_hour);
  */
};

/* 解决直接输出的中文乱码问题
参考:
https://segmentfault.com/q/1010000004338890
https://stackoverflow.com/questions/31433413/return-the-array-of-bytes-from-filereader
https://blog.shovonhasan.com/using-promises-with-filereader/
*/
const blob2Text = async inputBlob => {
  let reader = new FileReader();
  return new Promise((resolve, reject) => {
    reader.onerror = () => {
      reader.abort();
      reject(new DOMException('Problem parsing input file.'));
    };
    reader.onload = () => {
      resolve(reader.result);
    };
    reader.readAsText(inputBlob);
  });
};

/* 未完成学习课程数据:http://218.94.1.175:8087/sfxzwsxy/jypxks/modules/homepage/ajax/homepage_ajax.jsp?ajaxType=10
获取自 http://218.94.1.175:8087/sfxzwsxy/jypxks/modules/homepage/js/homepage.js
初始化培训课程数据方法 function initCourseData()
内含3个 Array:
exam_courses: 已听课待考试的必修课
required_courses: 待报名听课的必修课
selected_courses:选修课
各 Array 又以对象形式存储各课程,如
{"course_pk":2466,"course_name":"行政机关不履行法定职责(投诉举报)类行政复议案件审查要点","topic_name":"司法行政专业课程","apply_pk":3033781,"is_finish":1,"course_type":1,"course_rate":90,"use_flag":1}
*/
const getCourseData = async () => {
  let response = await fetch(
    `${location.origin}/sfxzwsxy/jypxks/modules/homepage/ajax/homepage_ajax.jsp?ajaxType=10`,
    {
      method: 'POST',
      body: 'blob'
    }
  );
  // https://developer.mozilla.org/zh-CN/docs/Web/API/Blob
  let blob = await response.blob();
  let utf8Text = await blob2Text(blob);
  let jsonData = JSON.parse(utf8Text);
  let jsonDataObj = jsonData[0];
  return jsonDataObj;
  /*
  let exam_courses = jsonDataObj.exam_courses;
  console.log('exam_courses:' + exam_courses);
  let required_courses = jsonDataObj.required_courses;
  console.log(required_courses);
  let selected_courses = jsonDataObj.selected_courses;
  console.log(selected_courses);
  */
};

// 课程学分学时等信息
const getCourseInfo = async csID => {
  let cUrl = `${location.origin}/sfxzwsxy//jypxks/modules/train/course/course_view.jsp?coursePk=${csID}`;
  let response = await fetch(cUrl, {
    method: 'POST',
    body: 'blob'
  });
  let blob = await response.blob();
  let csInfoHtml = await blob2Text(blob);
  /**
   * https://stackoverflow.com/a/35385518
   * @param {String} HTML representing any number of sibling elements
   * @return {NodeList}
   */
  const htmlToElements = htmlSrc => {
    let template = document.createElement('template');
    template.innerHTML = htmlSrc;
    return template.content;
  };
  let elements = htmlToElements(csInfoHtml);
  let courseCredit = Number(elements.querySelectorAll('#subjectInfo td')[7].textContent.trim());
  let courseTime = Number(elements.querySelectorAll('#extendInfo td')[3].textContent.trim());
  let courseInfo = {
    courseCredit: courseCredit,
    courseTime: courseTime
  };
  return courseInfo;
};

// 从 QQ 等打开地址后会被加上奇葩后缀 http://218.94.1.179:8087/sfxzwsxy/#?tdsourcetag=s_pctim_aiomsg
const clearURLs = () => {
  const tracks = [
    // QQ
    '?tdsourcetag=s_pctim_aiomsg',
    // Weixin
    '?from=groupmessage',
    '&from=groupmessage',
    '?from=singlemessage',
    '&from=singlemessage',
    '?from=timeline',
    '&from=timeline',
    '/type/WeixinReadCount'
  ];
  for (let track of tracks) {
    if (location.href.endsWith(track)) {
      location.href = location.href.replace(track, '');
    }
  }
};
clearURLs();

// 统一域名
if (location.host.endsWith('.sft.ipv6.jiangsu.gov.cn:8087')) {
  location.host = location.host.replace('.sft.ipv6.jiangsu.gov.cn', '').replace('-', '.');
}

// http://218.94.1.175:8087/sfxzwsxy/
if (location.pathname.match(/\/sfxzwsxy\/?(serverSelect.jsp)?#?$/i)) {
  // 自动选择最空闲的服务器
  window.onload = () => {
    const servers = document.getElementsByClassName('num');
    if (servers.length > 0) {
      let servers_str = [];
      let notificationText = '自动选择最空闲的服务器!';
      for (let s of servers) {
        servers_str.push(s.textContent);
        notificationText += `\n服务器${servers_str.length}使用程度:${s.textContent}%`;
      }
      // 将字符串元素转为数字
      let servers_num = servers_str.map(Number);
      // 全满则自动刷新,否则选择最空服务器
      let mostFree = Math.min(...servers_num);
      if (mostFree === 100) {
        if (GM_config.get('reload')) {
          // https://developer.mozilla.org/docs/Web/API/Location/reload
          location.reload(true);
        }
      } else {
        document.getElementsByClassName('entrybtn')[servers_num.indexOf(mostFree)].click();
        GM_notification(notificationText);
      }
    }
  };
}

// 去除登陆验证码校验
// 曾使用 OCR 识别法,参考了 https://www.cnblogs.com/ziyunfei/archive/2012/10/05/2710349.html 但准确度有限。
if (location.pathname.startsWith('/sfxzwsxy/index.jsp')) {
  window.onload = () => {
    // 在原函数的基础上,去掉验证码识别,去除 isBlank 函数依赖
    check = () => {
      if (document.getElementById('loginName').value === '') {
        alert('请输入用户名');
        document.getElementById('loginName').focus();
        return;
      }
      if (document.getElementById('pwd').value === '') {
        alert('请输入密码');
        document.getElementById('pwd').focus();
        return;
      }
      document.getElementById('form1').submit();
    };
    // 重新绑定点击事件
    document.getElementById('Submit').onclick = check;
    // 移除验证码并提示
    document.getElementById('verifyCode').remove();
    document.getElementById('imgCode').value = '已去除验证码可直接登录';
    // 以下尝试自动登录
    document.getElementById('loginName').value = GM_config.get('loginName'); // 写入预先设置的用户名
    document.getElementById('pwd').value = GM_config.get('pwd'); // 写入预先设置的密码
    // 自动获取用户名密码输入框焦点
    if (document.getElementById('loginName').value === '') {
      document.getElementById('loginName').focus();
    } else if (document.getElementById('pwd').value === '') {
      document.getElementById('pwd').focus();
    } else {
      // 用户名密码均已填写时才自动登录
      document.getElementById('Submit').click();
    }
  };
}

// 首页
if (
  location.href.endsWith('/sfxzwsxy/jypxks/index.html') ||
  location.href.endsWith('/sfxzwsxy/jypxks/index.html#')
) {
  // 修复页面过长无法完整显示的 Bug; 隐藏 密码修改提示 和 每日一题
  const css = `
  html[style="overflow: hidden;"] {
    overflow: visible !important;
  }
  #layui-layer1, #layui-layer-shade1, #layui-layer2, #layui-layer-shade2 {
    display: none !important;
  }
  `;
  addStyle(css);

  // 判断是否已完成任务。不等待页面载入完成就执行可能导致数据尚未成功获取而出错。
  window.onload = async () => {
    let userDataObj = await getUserData();
    let total_hour = userDataObj.total_hour; //规定需达到的总学时
    let required_hour = userDataObj.required_hour; //规定需达到的必修学时
    let required_credit = userDataObj.required_credit; //规定需达到的总学分
    let user_total_hour = userDataObj.user_total_hour || 0; //用户已获得的总学时
    let user_required_hour = userDataObj.user_required_hour || 0; //用户已获得的必修学时
    let user_required_credit = userDataObj.user_required_credit || 0; //用户已获得的总学分
    if (
      user_total_hour < total_hour ||
      user_required_hour < required_hour ||
      user_required_credit < required_credit
    ) {
      // 长时间不动会被弹出,故20分钟刷新一次
      setInterval(() => {
        location.reload(true);
      }, 1200000);
      console.log(`本学年任务尚未完成:`);
      if (user_total_hour < total_hour) {
        console.log(`总学时差:${total_hour - user_total_hour}`);
      }
      if (user_required_hour < required_hour) {
        console.log(`必修学时差:${required_hour - user_required_hour}`);
      }
      if (user_required_credit < required_credit) {
        console.log(`总学分差:${required_credit - user_required_credit}`);
      }
    } else {
      console.log(`本学年任务已经完成`);
    }
  };
}
// 自动聚焦
let document_observer = new MutationObserver(() => {
  if (document.getElementById('mainFrame')) {
    document.getElementById('mainFrame').focus();
    console.log('focus');
  }
});
document_observer.observe(body, {
  childList: true,
  subtree: true
});

// 首页培训课程 iframe
if (location.href.endsWith('/sfxzwsxy/jypxks/modules/homepage/homepage.jsp')) {
  // F8 批量后台新标签页打开已报名课程播放页面
  VM.registerShortcut(GM_config.get('open_unclose'), () => {
    let c = 0;
    const courses = document.getElementsByClassName('infomai');
    for (let course of courses) {
      // 报名后不再隐藏
      let aID = course.getElementsByClassName('applyPk')[0].textContent;
      if (aID !== '') {
        let cURL =
          location.origin +
          '/sfxzwsxy/jypxks/modules/train/ware/course_ware_view.jsp?applyPk=' +
          aID +
          '&courseType=1';
        GM_openInTab(cURL, true);
        c++;
        // 疑似超过 6 个课程同时学习后台不计数
        if (c >= GM_config.get('batch')) {
          console.log(`已批量打开${c}个课程`);
          return;
        }
      }
    }
  });
  const autoLearn = async () => {
    // 彩蛋:需要 iframe 提升才会执行
    if (window.top === window.self) {
      // 20 分钟刷新一次
      setInterval(() => {
        location.reload(true);
      }, 1200000);
      let courses = document.getElementsByClassName('course');
      console.log(courses);
      for await (let c of courses) {
        let cApplyPK = c.getElementsByClassName('applyPk')[0].textContent;
        let cID = c.getElementsByClassName('coursePk')[0].textContent;
        let cTitle = c.getElementsByClassName('title')[0].getAttribute('title');
        let cJdjs = c.getElementsByClassName('jdjs')[0].textContent;
        let csInfo = await getCourseInfo(cID);
        let csCredit = csInfo.courseCredit;
        console.log(csCredit);
        // 逻辑有待优化
        if (cJdjs === '未报名') {
          // 自动报名高学分课程
          if (csCredit > 1) {
            console.log(`${csCredit}学分:${cTitle}`);
            c.click();
            console.log('click:' + cTitle);
            const info = document.getElementsByClassName('layui-layer-btn0');
            if (info.length === 1) {
              document.getElementsByClassName('layui-layer-btn0')[0].click();
              console.log('报名:' + cTitle);
            }
          }
        } else {
          let cURL =
            location.origin +
            '/sfxzwsxy/jypxks/modules/train/ware/course_ware_view.jsp?applyPk=' +
            cApplyPK +
            '&courseType=1';
          const cOpenClose = GM_openInTab(cURL, true);
          // 15 分钟自动关闭
          setTimeout(cOpenClose.close, 900000);
          // 本段在此无意义,应作为 iframe 存在时简单处理
          c.addEventListener('click', () => {
            GM_openInTab(cURL, true);
            GM_notification('默认弹出窗口并不影响使用,暂请无视。');
          });
        }
      }
    }
  };
  // 考试提醒 - 课程考试
  const openExam = async () => {
    const csJsonDataObj = await getCourseData();
    const exam_courses = csJsonDataObj.exam_courses;
    // 第一页 #courseExam1,第二页 #courseExam2
    const exams = document.querySelectorAll('div[id^=courseExam] > a[title]');
    for await (let exam of exams) {
      const eURL = location.origin + '/sfxzwsxy/' + exam.getAttribute('onclick').split("'")[1];
      const eTitle = exam.getAttribute('title');
      //console.log(eTitle);
      exam.href = eURL;
      exam.onclick = '';
      exam.target = '_blank';
      for await (let e of exam_courses) {
        let eCourse_pk = '';
        let answerURL = '';
        if (e.course_name === eTitle) {
          //console.log(exam_courses.length);
          //console.log(e);
          eCourse_pk = e.course_pk;
          //console.log(eCourse_pk);
          answerURL =
            location.origin +
            '/sfxzwsxy//jypxks/modules/train/course/subject_list.jsp?coursePk=' +
            eCourse_pk +
            '&op=view';
          //console.log(answerURL);
          exam.addEventListener('click', () => {
            GM_openInTab(answerURL, true);
            GM_notification('答案已同步在隔壁标签页打开。\n暂请考试结束后手动关闭。');
          });
        }
      }
    }
  };
  // 最新知识库
  const openKnowledge = () => {
    const knowledges = document.querySelectorAll('#knowledgeType a[title][href="#"]');
    for (let knowledge of knowledges) {
      const kURL =
        location.origin +
        '/sfxzwsxy/jypxks/modules/learn/document/learn/document_learn_text.jsp?fkNodePk=' +
        knowledge
          .getAttribute('onclick')
          .split('(')[1]
          .split(')')[0];
      //console.log(kURL);
      knowledge.href = kURL;
      knowledge.onclick = '';
      knowledge.target = '_blank';
    }
  };
  window.onload = async () => {
    autoLearn();
    openExam();
    openKnowledge();
  };
}

// 培训课程查询 iframe
if (location.href.endsWith('/sfxzwsxy/jypxks/modules/train/query/course_query.jsp')) {
  // 清理“参加培训”和“查看”链接
  const reallinks = () => {
    let courses = document.querySelectorAll('#trainCourseList a[onclick^=bindBeginTrainEvents]');
    for (let course of courses) {
      let aID = course.getAttribute('onclick').split('"')[1];
      //console.log(aID);
      let aURL =
        location.origin +
        '/sfxzwsxy/jypxks/modules/train/ware/course_ware_view.jsp?applyPk=' +
        aID +
        '&courseType=1';
      //console.log(cURL);
      course.href = aURL;
      course.onclick = '';
      course.target = '_blank';
    }
    let answers = document.querySelectorAll(
      '#trainCourseList a[onclick^=bindViewCourseInfoEvents]'
    );
    for (let answer of answers) {
      let cID = answer.getAttribute('onclick').split('"')[1];
      //console.log(cID);
      let cURL =
        location.origin + '/sfxzwsxy/jypxks/modules/train/course/course_view.jsp?coursePk=' + cID;
      //console.log(cURL);
      answer.href = cURL;
      answer.onclick = '';
      answer.target = '_blank';
    }
  };

  let document_observer = new MutationObserver(() => {
    reallinks();
  });
  document_observer.observe(body, {
    childList: true,
    subtree: true
  });

  // F8 批量后台新标签页打开课程播放页面
  VM.registerShortcut(GM_config.get('open_unclose'), () => {
    let c = 0;
    const courses = document.querySelectorAll('#trainCourseList a[onclick^=bindBeginTrainEvents]');
    for (let course of courses) {
      let aID = course.getAttribute('onclick').split('"')[1];
      let aURL =
        location.origin +
        '/sfxzwsxy/jypxks/modules/train/ware/course_ware_view.jsp?applyPk=' +
        aID +
        '&courseType=1';
      GM_openInTab(aURL, true);
      c++;
      // 疑似超过 6 个课程同时学习后台不计数
      if (c >= GM_config.get('batch')) {
        console.log(`已批量打开${c}个课程`);
        return;
      }
    }
  });
}

// 培训课程查询 - 查看 - 题干 iframe
if (
  // 点击 “下一页” 或 “上一页” 后 iframe 实际地址会去除 .jsp 之后的尾巴
  location.href.startsWith(
    'http://218.94.1.181:8087/sfxzwsxy//jypxks/modules/train/course/subject_list.jsp'
  ) ||
  location.href.startsWith(
    'http://218.94.1.179:8087/sfxzwsxy//jypxks/modules/train/course/subject_list.jsp'
  ) ||
  location.href.startsWith(
    'http://218.94.1.175:8087/sfxzwsxy//jypxks/modules/train/course/subject_list.jsp'
  )
) {
  // 清理“题干”链接
  window.onload = () => {
    const views = document.querySelectorAll('a[onclick^=viewSubject]');
    for (let view of views) {
      const vID = view
        .getAttribute('onclick')
        .split('(')[1]
        .split(')')[0];
      //console.log(vID);
      const vURL =
        location.origin +
        '/sfxzwsxy//jypxks/modules/train/course/subject_view.jsp?subjectPk=' +
        vID;
      //console.log(vURL);
      view.href = vURL;
      view.onclick = '';
      view.target = '_blank';
    }
  };
}

// 课程视频播放 跨域 iframe
if (location.href.startsWith('http://218.94.1.181:5088/unzipapp/project/ware/attach/')) {
  // 旧播放器在 Windows 下要求 Flash,新播放器不兼容苹果系列。老版本播放器能否成功调用 HTML5 似乎是玄学问题。
  fakeUA('Linux');
  // 检测不到 HTML5 播放器则刷新
  const isFlash = () => {
    if (document.getElementById('course_player5')) {
      //console.log('html5');
      clearInterval(noFlash);
    } else {
      location.reload(true);
    }
  };
  const noFlash = setInterval(isFlash, 1000);

  const learn = () => {
    // 自动从课程封面进入播放页面
    if (document.querySelector('img[src="courseware/iconImg/z3.png"]')) {
      document.querySelector('img[src="courseware/iconImg/z3.png"]').click();
    }
    // 老播放器用的 confirm 对话框手动点击即暂停,新版的 alert 无法模拟点击但手动点击不影响后台播放
    if (document.getElementById('cancel')) {
      //document.getElementById('confirm').click(); // 继续学习;可能需要多次重复才能完成一课时。
      document.getElementById('cancel').click(); // 大侠还请重新来过
    }
  };

  const option = () => {
    // 旧播放器自动做题,找不到测试例进一步优化。
    if (document.querySelectorAll('div.option>label>input[name="que"]').length > 0) {
      document.querySelectorAll('div.option>label>input[name="que"]')[0].click();
      document.querySelectorAll('div.option>label>input[name="que"]')[1].click(); // 多选题
      document.getElementsByClassName('button')[0].click(); // 提交
      if (document.getElementsByClassName('button_xia').length === 1) {
        document.getElementsByClassName('button_xia')[0].click(); // 下一题
      }
      if (document.getElementsByClassName('button_wan').length === 1) {
        document.getElementsByClassName('button_wan')[0].click(); // 完成
      }
      document.getElementById('course_player5').click(); // 播放
    }
  };
  let document_observer = new MutationObserver(() => {
    learn();
    option();
  });
  document_observer.observe(body, {
    attributes: true,
    subtree: true
  });
}

// 必修课考试 iframe
if (
  location.href.endsWith(
    '/sfxzwsxy/jypxks/modules/examination/course_examine/course_exam_list.jsp'
  ) ||
  location.href.endsWith(
    '/sfxzwsxy/jypxks/modules/examination/course_examine/course_exam_list.jsp#'
  )
) {
  // 清理“参加考试”链接,新标签页打开考试及答案。
  window.onload = () => {
    const exams = document.querySelectorAll('a[href="#"][onclick^=openWindowFullScreen]');
    for (let exam of exams) {
      const eURL = location.origin + '/sfxzwsxy/' + exam.getAttribute('onclick').split("'")[1];
      const course_pk = eURL.split('course_pk=')[1].split('&')[0];
      //console.log(eURL);
      //console.log(course_pk);
      const answerURL =
        location.origin +
        '/sfxzwsxy//jypxks/modules/train/course/subject_list.jsp?coursePk=' +
        course_pk +
        '&op=view';
      exam.href = eURL;
      exam.onclick = '';
      exam.target = '_blank';
      exam.addEventListener('click', () => {
        GM_openInTab(answerURL, true);
        GM_notification('答案已同步在隔壁标签页打开。\n暂请考试结束后手动关闭。');
      });
    }
  };
}