diff --git a/application/api/controller/Base.php b/application/api/controller/Base.php index 380066a2..6d76cb01 100644 --- a/application/api/controller/Base.php +++ b/application/api/controller/Base.php @@ -1178,6 +1178,107 @@ class Base extends Controller return $ids; } + /** + * 解析方括号引用内层(如 1,2 / 3-5),展开为文献序号列表。 + * + * @return int[] + */ + protected function expandCitationBracketNumbers(string $referencePart): array + { + $referencePart = trim($referencePart); + if ($referencePart === '') { + return []; + } + $referencePart = str_replace( + [',', '–', '—', '−', '‐', '‑'], + [',', '-', '-', '-', '-', '-'], + $referencePart + ); + $out = []; + $segments = preg_split('/\s*,\s*/', $referencePart); + foreach ($segments as $seg) { + $seg = trim((string)$seg); + if ($seg === '') { + continue; + } + $seg = str_replace(['–', '—', '−', '‐', '‑'], '-', $seg); + if (preg_match('/^(\d+)\s*-\s*(\d+)$/', $seg, $m)) { + $a = intval($m[1]); + $b = intval($m[2]); + if ($a > $b) { + $t = $a; + $a = $b; + $b = $t; + } + for ($i = $a; $i <= $b; $i++) { + $out[] = $i; + } + } else { + $n = intval($seg); + if ($n > 0) { + $out[] = $n; + } + } + } + return $out; + } + + /** + * 从正文片段提取被引用的文献序号(reference_no = index+1)。 + * 兼容 [n] / [n] 两种形态。 + * + * @return int[] + */ + protected function extractCitationRefNosFromMainContent(string $text, int $pArticleId = 0): array + { + if ($text === '') { + return []; + } + + $nos = []; + + $pReferIds = $this->extractMyciteIds($text); + if (!empty($pReferIds) && $pArticleId > 0) { + $refers = Db::name('production_article_refer') + ->where('p_article_id', $pArticleId) + ->whereIn('p_refer_id', $pReferIds) + ->where('state', 0) + ->field('p_refer_id,index') + ->select(); + $idToNo = []; + foreach ($refers as $row) { + $idToNo[intval($row['p_refer_id'])] = intval($row['index']) + 1; + } + foreach ($pReferIds as $pid) { + if (isset($idToNo[$pid])) { + $nos[] = $idToNo[$pid]; + } + } + } + + if (preg_match_all('/(?:<\s*blue[^>]*>)?\[([^\]]+)\](?:<\/\s*blue\s*>)?/iu', $text, $m)) { + foreach ($m[1] as $inner) { + $innerNorm = str_replace( + [',', '–', '—', '−', '‐', '‑'], + [',', '-', '-', '-', '-', '-'], + trim((string)$inner) + ); + if (!preg_match('/^[\d\s,\-]+$/u', $innerNorm)) { + continue; + } + foreach ($this->expandCitationBracketNumbers($innerNorm) as $n) { + if ($n > 0) { + $nos[] = $n; + } + } + } + } + + $nos = array_values(array_unique($nos)); + sort($nos, SORT_NUMERIC); + return $nos; + } + /** * table_data:二维数组 JSON [[{text,colspan,rowspan},...],...];支持双重 JSON 字符串编码。 * diff --git a/application/api/controller/Preaccept.php b/application/api/controller/Preaccept.php index ce9e840b..0ec24534 100644 --- a/application/api/controller/Preaccept.php +++ b/application/api/controller/Preaccept.php @@ -7,7 +7,7 @@ use think\Env; use think\Queue; use think\Validate; use app\common\CrossrefService; -use app\common\ReferenceCheckService; +use app\common\ReferenceRelevanceCheckService; class Preaccept extends Base { @@ -27,7 +27,7 @@ class Preaccept extends Base return; } try { - (new ReferenceCheckService())->clearArticleChecksByPArticleId($pArticleId); + (new ReferenceRelevanceCheckService())->clearArticleChecksByPArticleId($pArticleId); } catch (\Exception $e) { \think\Log::error( 'resetArticleChecksOnReferChange[' . $sourceTag . '] p_article_id=' @@ -1221,6 +1221,14 @@ class Preaccept extends Base $insert['ctime'] = time(); $this->article_main_log_obj->insert($insert); +// $articleId = intval($am_info['article_id']); +// $amId = intval($data['am_id']); +// +// // 本段引用集合变化(如 10,11 → 11,12)时仅清空该 am_id 下的校对明细 +// if ($this->hasMainCitationChange($old_content, $new_raw_content, $articleId)) { +// $this->clearMainChecksOnCitationChange($articleId, $amId); +// } + // 判断是否存在“引用删除”(新 content 相对旧 content 缺少 ) $hasCitationDeletion = $this->hasMyciteDeletion($old_content, $new_raw_content); @@ -1246,6 +1254,39 @@ class Preaccept extends Base //返回更新数据 20260119 end } + /** + * 正文单节保存后,仅清空该 am_id 下已有的引用校对明细(按 article_id 定位)。 + */ + private function clearMainChecksOnCitationChange(int $articleId, int $amId) + { + if ($articleId <= 0 || $amId <= 0) { + return; + } + try { + (new ReferenceCheckService())->clearChecksByAmId($articleId, $amId); + } catch (\Exception $e) { + \think\Log::error( + 'clearMainChecksOnCitationChange article_id=' . $articleId + . ' am_id=' . $amId . ' ' . $e->getMessage() + ); + } + } + + /** + * 本段正文引用集合是否变化(增删改任一即 true)。 + * old 多为库内 [n],new 多为编辑器提交的 。 + */ + private function hasMainCitationChange(string $oldContent, string $newContent, int $articleId): bool + { + $pArticleId = intval(Db::name('production_article') + ->where('article_id', $articleId) + ->whereIn('state', [0, 2]) + ->value('p_article_id')); + $oldNos = $this->extractCitationRefNosFromMainContent($oldContent, $pArticleId); + $newNos = $this->extractCitationRefNosFromMainContent($newContent, $pArticleId); + return $oldNos !== $newNos; + } + /** * 是否发生 删除(new 相对 old 少了任意引用 id) */ diff --git a/application/api/controller/References.php b/application/api/controller/References.php index 331edd62..3408dc06 100644 --- a/application/api/controller/References.php +++ b/application/api/controller/References.php @@ -12,6 +12,8 @@ use think\Db; use think\Env; use think\Queue; use app\common\ReferenceCheckService; +use app\common\ReferenceRelevanceCheckService; +use app\common\DbReconnectHelper; /** * @title 参考文献 * @description 相关方法汇总 @@ -1309,38 +1311,48 @@ class References extends Base } return json_encode(['status' => 8,'msg' => 'fail']); } - /** - * 参考文献第一次校对 - * @return \think\response\Json - */ - public function allReferenceCheckAI(){ - //获取参数 - $aParam = empty($aParam) ? $this->request->post() : $aParam; + // ============================================================ + // 参考文献「主题相关性」校对(RabbitMQ 链式消费,复用 reference_check 队列) + // 表:t_article_reference_relevance_check_result / t_article_reference_relevance_check_batch + // 消费:php think reference_check:mq-consume + // ============================================================ - //必填值验证 - $iPArticleId = empty($aParam['p_article_id']) ? '' : $aParam['p_article_id']; - if(empty($iPArticleId)){ - return json_encode(array('status' => 2,'msg' => 'Please select an article' )); + /** + * 启动整篇参考文献相关性校对 + * POST: p_article_id(必填) + * + * 文献摘要/内容优先读 t_production_article_refer.abstract_text、refer_content_cleaned; + * 二者都为空时在校对执行阶段抓取并回写 refer 表,校对时始终从 refer 表读取。 + */ + public function allReferenceCheckAI() + { + $aParam = $this->request->post(); + $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']); + if ($iPArticleId <= 0) { + return jsonError('Please select an article'); } - //查询文章(p_article_id 与 article_id 都要带,下游服务方法两者都用) - $aWhere = ['p_article_id' => $iPArticleId,'state' => ['in',[0,2]]]; - $aProductionArticle = Db::name('production_article')->field('p_article_id,article_id')->where($aWhere)->find(); - if(empty($aProductionArticle)){ - return json_encode(array('status' => 3,'msg' => 'No articles found' )); + + $aProductionArticle = Db::name('production_article') + ->field('p_article_id,article_id') + ->where(['p_article_id' => $iPArticleId, 'state' => ['in', [0, 2]]]) + ->find(); + if (empty($aProductionArticle)) { + return jsonError('No articles found'); } - if($this->checkReferStatus($iPArticleId)==0){ + if ($this->checkReferStatus($iPArticleId) == 0) { return jsonError('Please correct the reference content before running the check.'); } - //已存在校对记录则禁止重复执行第一次校对,提示走重置接口 - $iExisting = Db::name('article_reference_check_result') + + $existing = Db::name('article_reference_relevance_check_result') ->where('p_article_id', $iPArticleId) ->count(); - if(intval($iExisting) > 0){ - return jsonError('This article already has a reference check record. Please use the "Reset Check" endpoint to run the check again.'); + if (intval($existing) > 0) { + return jsonError('This article already has relevance check records.'); } + try { - $svc = new ReferenceCheckService(); - $result = $svc->enqueueByPArticle($aProductionArticle); + DbReconnectHelper::ensure(); + $result = (new ReferenceRelevanceCheckService())->enqueueByPArticle($aProductionArticle); if (empty($result['check_ids'])) { return jsonError('No reference citations were found in the article.'); } @@ -1349,8 +1361,156 @@ class References extends Base return jsonError($e->getMessage()); } } + /** - * 文献校对重置:删除该文章已有的全部校对明细,并重新入队整篇校对 + * 相关性校对进度(同 referenceCheckProgressAI) + * POST/GET: p_article_id(必填) + */ + public function referenceRelevanceCheckProgressAI() + { + $aParam = $this->request->post(); + if (empty($aParam)) { + $aParam = $this->request->param(); + } + + $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']); + if ($iPArticleId <= 0) { + return json_encode(array('status' => 2, 'msg' => 'Please select an article')); + } + + try { + $result = (new ReferenceRelevanceCheckService())->getProgressByPArticleId($iPArticleId); + return jsonSuccess($result); + } catch (\Exception $e) { + return jsonError($e->getMessage()); + } + } + + /** + * 按 p_article_id 查整篇文章相关性校对总状态(同 referenceCheckArticleStatusAI) + * POST/GET: p_article_id(必填) + */ + public function referenceRelevanceCheckArticleStatusAI() + { + $aParam = $this->request->post(); + if (empty($aParam)) { + $aParam = $this->request->param(); + } + + $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']); + if ($iPArticleId <= 0) { + return json_encode(array('status' => 2, 'msg' => 'Please select an article')); + } + + try { + $result = (new ReferenceRelevanceCheckService())->getArticleProgressStatusByPArticleId($iPArticleId); + return jsonSuccess($result); + } catch (\Exception $e) { + return jsonError($e->getMessage()); + } + } + + /** + * 按 p_refer_id 查单条参考文献的相关性校对明细与进度(同 referenceCheckDetailsAI) + * POST/GET: p_refer_id(必填) + */ + public function referenceRelevanceCheckDetailsAI() + { + $aParam = $this->request->post(); + if (empty($aParam)) { + $aParam = $this->request->param(); + } + + $iPReferId = empty($aParam['p_refer_id']) ? 0 : intval($aParam['p_refer_id']); + if ($iPReferId <= 0) { + return json_encode(array('status' => 2, 'msg' => 'Please select a reference')); + } + + try { + $result = (new ReferenceRelevanceCheckService())->getDetailsByPReferId($iPReferId); + return jsonSuccess($result); + } catch (\Exception $e) { + return jsonError($e->getMessage()); + } + } + + /** + * 清空并重新执行相关性校对 + * POST: p_article_id + */ + public function referenceRelevanceCheckResetAI() + { + $aParam = $this->request->post(); + $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']); + if ($iPArticleId <= 0) { + return jsonError('Please select an article'); + } + $aProductionArticle = Db::name('production_article') + ->field('p_article_id,article_id') + ->where(['p_article_id' => $iPArticleId, 'state' => ['in', [0, 2]]]) + ->find(); + if (empty($aProductionArticle)) { + return jsonError('No articles found'); + } + if ($this->checkReferStatus($iPArticleId) == 0) { + return jsonError('Please correct the reference content before running the check.'); + } + try { + $result = (new ReferenceRelevanceCheckService())->resetAndRecheckByArticle($aProductionArticle); + return jsonSuccess($result); + } catch (\Exception $e) { + return jsonError($e->getMessage()); + } + } + + /** + * 仅清空相关性校对记录(不重跑) + * POST: p_article_id + */ + public function referenceRelevanceCheckClearAI() + { + $aParam = $this->request->post(); + $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']); + if ($iPArticleId <= 0) { + return jsonError('p_article_id is required'); + } + try { + $deleted = (new ReferenceRelevanceCheckService())->clearByPArticleId($iPArticleId); + return jsonSuccess(['p_article_id' => $iPArticleId, 'deleted' => intval($deleted)]); + } catch (\Exception $e) { + return jsonError($e->getMessage()); + } + } + + /** + * 仅重跑相关性 status=0 的记录(不清空,不抓摘要,不清洗文献内容) + * POST: p_article_id + */ + public function referenceRelevanceCheckRecheckPendingAI() + { + $aParam = $this->request->post(); + $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']); + if ($iPArticleId <= 0) { + return jsonError('p_article_id is required'); + } + try { + $result = (new ReferenceRelevanceCheckService())->recheckPendingOnlyByArticle($iPArticleId); + return jsonSuccess($result); + } catch (\Exception $e) { + return jsonError($e->getMessage()); + } + } + + /** + * @deprecated 支撑力度校对已下线,请使用 allReferenceCheckAI(主题相关性校对) + */ + public function allReferenceCheckAI2() + { + return jsonError('Support strength check is deprecated. Please use allReferenceCheckAI.'); + } + + /** + * 文献校对重置:删除该文章已有的全部相关性校对明细,并重新入队整篇校对 * POST/GET: article_id(必填) * @url /api/Article/referenceCheckReset */ @@ -1378,7 +1538,7 @@ class References extends Base return json_encode(array('status' => 4,'msg' => 'Unbound article' )); } try { - $result = (new ReferenceCheckService())->resetAndRecheckByArticle($aProductionArticle); + $result = (new ReferenceRelevanceCheckService())->resetAndRecheckByArticle($aProductionArticle); return jsonSuccess($result); } catch (\Exception $e) { return jsonError($e->getMessage()); @@ -1415,7 +1575,7 @@ class References extends Base } try { - $deleted = (new ReferenceCheckService())->clearArticleChecksByPArticleId($iPArticleId); + $deleted = (new ReferenceRelevanceCheckService())->clearByPArticleId($iPArticleId); return jsonSuccess([ 'p_article_id' => $iPArticleId, 'deleted' => intval($deleted), @@ -1426,7 +1586,7 @@ class References extends Base } /** - * 按 p_article_id 查整篇引用校对进度(按 reference_no 分组聚合) + * 按 p_article_id 查整篇相关性校对进度(按 reference_no 分组聚合) * * POST/GET: p_article_id(必填) * @@ -1439,6 +1599,8 @@ class References extends Base * records[i].status 与分组同一套数值含义(但 record 不会出现 1=校对中): * 0 = 待校验 2 = 校对完成 3 = 校对失败 * + * records[i] 含 reason(中英双语)、reason_en、combined_reason(中英双语)、combined_reason_en + * * summary 用字符串键:pending / checking / completed / failed */ public function referenceCheckProgressAI() @@ -1453,7 +1615,7 @@ class References extends Base return json_encode(array('status' => 2, 'msg' => 'Please select an article')); } try { - $result = (new ReferenceCheckService())->getProgressByPArticleId($iPArticleId); + $result = (new ReferenceRelevanceCheckService())->getProgressByPArticleId($iPArticleId); return jsonSuccess($result); } catch (\Exception $e) { return jsonError($e->getMessage()); @@ -1461,7 +1623,7 @@ class References extends Base } /** - * 按 p_article_id 查整篇文章引用校对总状态(用于前端按钮分流) + * 按 p_article_id 查整篇文章相关性校对总状态(用于前端按钮分流) * * POST/GET: p_article_id(必填) * @@ -1495,7 +1657,7 @@ class References extends Base } try { - $result = (new ReferenceCheckService())->getArticleProgressStatusByPArticleId($iPArticleId); + $result = (new ReferenceRelevanceCheckService())->getArticleProgressStatusByPArticleId($iPArticleId); return jsonSuccess($result); } catch (\Exception $e) { return jsonError($e->getMessage()); @@ -1523,7 +1685,7 @@ class References extends Base } try { - $result = (new ReferenceCheckService())->getArticleCheckQueuePositionByPArticleId($iPArticleId); + $result = (new ReferenceRelevanceCheckService())->getArticleCheckQueuePositionByPArticleId($iPArticleId); return jsonSuccess($result); } catch (\Exception $e) { return jsonError($e->getMessage()); @@ -1531,13 +1693,12 @@ class References extends Base } /** - * 某条参考文献下「校对失败」的明细重新校对(异步) + * 某条参考文献下「相关性校对失败」的明细重新校对(异步) * * POST/GET: p_refer_id(必填) * p_article_id(可选) * * 仅重跑 status=3(校对失败)的记录;不改动 refer_text,只重置结果字段后入 RabbitMQ 批次队列。 - * 返回:p_refer_id、p_article_id、reset、queued、check_ids、queue */ public function referenceCheckRecheckFailedAI() { @@ -1554,21 +1715,53 @@ class References extends Base $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']); try { - $result = (new ReferenceCheckService())->enqueueRecheckFailedByPReferId($iPReferId, $iPArticleId); - return jsonSuccess([]); + $result = (new ReferenceRelevanceCheckService())->enqueueRecheckFailedByPReferId($iPReferId, $iPArticleId); + return jsonSuccess($result); } catch (\Exception $e) { return jsonError($e->getMessage()); } } /** - * 按 p_refer_id 查单条参考文献的校对明细与进度 + * 某条参考文献下「校对失败」重跑,并联动同一引用标签分组(如 [1,2])全部重跑(异步) + * + * POST/GET: p_refer_id(必填) + * p_article_id(可选) + * + * 返回:p_refer_id、p_article_id、reset、queued、check_ids、queue + */ + public function referenceCheckRecheckFailedWithGroupAI() + { + $aParam = $this->request->post(); + if (empty($aParam)) { + $aParam = $this->request->param(); + } + + $iPReferId = empty($aParam['p_refer_id']) ? 0 : intval($aParam['p_refer_id']); + if ($iPReferId <= 0) { + return json_encode(array('status' => 2, 'msg' => 'Please select a reference')); + } + + $iPArticleId = empty($aParam['p_article_id']) ? 0 : intval($aParam['p_article_id']); + + try { + $result = (new ReferenceRelevanceCheckService())->enqueueRecheckFailedByPReferIdWithGroup($iPReferId, $iPArticleId); + return jsonSuccess($result); + } catch (\Exception $e) { + return jsonError($e->getMessage()); + } + } + + /** + * 按 p_refer_id 查单条参考文献的相关性校对明细与进度 * * POST/GET: p_refer_id(必填) * * 分组进度:progress_status(0待/1中/2完成/3失败)、pending、done、failed、pass、 * is_pass、progress_percent、last_updated_at - * list 每项:check_id、am_id、status、confidence、reason、is_match、is_pass + * list 每项:check_id、am_id、status、is_relevant、relevance_level、relevance_role、 + * relevance_score、reason(中英双语【中文】/【English】)、reason_en、 + * combined_*、combined_reason_en、cite_group_refs、cite_check_mode、is_pass */ public function referenceCheckDetailsAI() { @@ -1583,13 +1776,33 @@ class References extends Base } try { - $result = (new ReferenceCheckService())->getCheckDetailsByPReferId($iPReferId); + $result = (new ReferenceRelevanceCheckService())->getDetailsByPReferId($iPReferId); return jsonSuccess($result); } catch (\Exception $e) { return jsonError($e->getMessage()); } } + /** + * 对校对明细中从未出现过的参考文献(p_refer_id 差集)重新扫描全文并入队校对 + * + * POST/GET: p_article_id(必填) + * + * 差集:production_article_refer(state=0) 减去 article_reference_check_result 已出现的 p_refer_id。 + * 适用:首次校对漏匹配、表格后上传、正文补标等场景。不重置已有明细。 + * 前置:须已执行过第一次校对(库中已有校对记录)。 + * + * 返回:missing_p_refer_ids、matched_p_refer_ids、still_unmatched_p_refer_ids、 + * queued、new_reference_nos、check_ids、queue + */ + /** + * @deprecated 支撑力度漏匹配补扫已下线,请清空后使用 allReferenceCheckAI 重新校对 + */ + public function referenceCheckRematchNewAI() + { + return jsonError('Support strength rematch is deprecated. Please use referenceCheckResetAI or allReferenceCheckAI.'); + } + public function checkReferStatus($p_article_id){ $list = $this->production_article_refer_obj->where('p_article_id', $p_article_id)->where('state', 0)->select(); if (!$list) { @@ -1604,4 +1817,6 @@ class References extends Base } return $frag; } + + } diff --git a/application/command/ReferenceCheckMqConsume.php b/application/command/ReferenceCheckMqConsume.php index 4f2f22f7..6d0103e5 100644 --- a/application/command/ReferenceCheckMqConsume.php +++ b/application/command/ReferenceCheckMqConsume.php @@ -13,7 +13,7 @@ class ReferenceCheckMqConsume extends Command protected function configure() { $this->setName('reference_check:mq-consume') - ->setDescription('Consume RabbitMQ reference check article queue'); + ->setDescription('Consume RabbitMQ reference relevance check article queue (ref_check.article)'); } protected function execute(Input $input, Output $output) diff --git a/application/common/PubmedService.php b/application/common/PubmedService.php index ba5865ee..49aec572 100644 --- a/application/common/PubmedService.php +++ b/application/common/PubmedService.php @@ -113,6 +113,68 @@ class PubmedService return $abbr !== '' ? $abbr : null; } + /** + * 按书目信息检索 PubMed(标题 + 第一作者 + 年份) + */ + public function searchByBibliographic($title, $author = '', $year = ''): ?array + { + $title = trim((string)$title); + if ($title === '') { + return null; + } + + $terms = ['(' . $this->quoteTerm($title) . '[Title])']; + $author = trim((string)$author); + if ($author !== '') { + $parts = preg_split('/[,;]/', $author); + $first = trim((string)($parts[0] ?? '')); + if ($first !== '') { + $terms[] = '(' . $this->quoteTerm($first) . '[Author])'; + } + } + $year = trim((string)$year); + if ($year !== '' && preg_match('/^(19|20)\d{2}$/', $year)) { + $terms[] = '(' . $year . '[pdat])'; + } + + $pmid = $this->esearch(implode(' AND ', $terms)); + if (!$pmid) { + return null; + } + + $info = $this->fetchByPmid($pmid); + if (!$info) { + return null; + } + $info['pmid'] = $pmid; + $info['doi'] = $this->extractDoiFromPmidRecord($pmid); + return $info; + } + + private function quoteTerm($text) + { + return str_replace('"', '', trim((string)$text)); + } + + private function extractDoiFromPmidRecord($pmid) + { + $url = $this->base . 'efetch.fcgi?' . http_build_query([ + 'db' => 'pubmed', + 'id' => $pmid, + 'retmode' => 'xml', + 'tool' => $this->tool, + 'email' => $this->email, + ]); + $xml = $this->httpGet($url); + if ($xml === '') { + return ''; + } + if (preg_match('/([^<]+)<\/ArticleId>/i', $xml, $m)) { + return trim($m[1]); + } + return ''; + } + // ----------------- Internals ----------------- private function esearch(string $term): ?string diff --git a/application/common/ReferenceCheckService.php b/application/common/ReferenceCheckService.php index e551a482..b9ea2586 100644 --- a/application/common/ReferenceCheckService.php +++ b/application/common/ReferenceCheckService.php @@ -5,10 +5,11 @@ namespace app\common; use think\Db; use think\Env; use app\common\service\LLMService; -use app\common\mq\ReferenceCheckMqPublisher; /** * 正文 <blue>[n]</blue> 引用与 t_production_article_refer(index+1=n)相关性校对。 + * 校对上下文取 t_article_main 一条记录(正文 content 或表格 table_data 展平)。 + * 同一引用标签 [1,2]、[4-6] 联合校对,cite_group_refs 存展开序号。 * LLM 配置与 PromotionLlmService 相同;异步任务走 RabbitMQ(一篇一条消息)。 */ class ReferenceCheckService @@ -69,6 +70,9 @@ class ReferenceCheckService /** LLM 评分(confidence)通过阈值:>= 该值视为"通过" */ const PASS_CONFIDENCE_THRESHOLD = 0.65; + /** 是否启用二轮 DOI/Crossref 复核(暂时关闭时设为 false) */ + const SECOND_PASS_ENABLED = false; + /** * 正文引用标签两种排版(带 /u): * 1) [8, 9][13-15] —— 方括号在 blue 内 @@ -93,6 +97,11 @@ class ReferenceCheckService return isset($arr[$key]) ? $arr[$key] : $default; } + private function isSecondPassEnabled() + { + return self::SECOND_PASS_ENABLED; + } + /** 新建/重置校对明细时的队列初始字段 */ private function newCheckRecordFields(array $fields, $queueStatus = self::QUEUE_PENDING, $retryCount = 0) { @@ -101,8 +110,26 @@ class ReferenceCheckService return $fields; } + /** 重置校对结果时清零的字段(含支撑力度扩展字段) */ + private function referenceCheckResultResetFields(array $extra = []) + { + return array_merge([ + 'status' => self::RECORD_PENDING, + 'is_match' => 0, + 'can_support' => 0, + 'confidence' => 0, + 'reason' => '', + 'support_role' => '', + 'combined_can_support' => 0, + 'combined_confidence' => 0, + 'combined_reason' => '', + 'error_msg' => '', + ], $extra); + } + public function markQueueRuntime($checkId, $queueStatus, $retryCount = null) { + DbReconnectHelper::ensure(); $checkId = intval($checkId); if ($checkId <= 0) { return 0; @@ -113,6 +140,84 @@ class ReferenceCheckService } return Db::name('article_reference_check_result')->where('id', $checkId)->update($fields); } + public function enqueueByPArticle($prod){ + if (empty($prod)) { + throw new \RuntimeException('production_article not found'); + } + $pArticleId = intval($prod['p_article_id']); + $articleId = intval($prod['article_id']); + $referMap = $this->loadReferMapByPArticleId($pArticleId); + + $mains = Db::name('article_main') + ->field('am_id,content,article_id,type,amt_id') + ->where('article_id', $articleId) + ->whereIn('state', [0, 2]) + ->order('sort asc') + ->select(); + if (empty($mains)) { + throw new \RuntimeException('article_main is empty'); + } + $queued = 0; + $skipped = 0; + $pendingJobs = []; + $amIdsWithJobs = []; + $now = date('Y-m-d H:i:s'); + foreach ($mains as $main) { + $amId = intval($main['am_id']); + $citations = $this->extractReferencesForArticleMain($main); + if (empty($citations)) { + $this->setAmRefCheckStatus($amId, self::AM_STATUS_NONE); + continue; + } + $sectionText = $this->resolveSectionTextForArticleMain($main); + foreach ($citations as $cite) { + foreach ($cite['reference_numbers'] as $refNo) { + $referIndex = $refNo - 1; + if ($referIndex < 0 || !isset($referMap[$referIndex])) { + $skipped++; + continue; + } + $scope = [ + 'article_id' => $main['article_id'], + 'p_article_id' => $pArticleId, + 'am_id' => $amId, + 'section_text' => $sectionText, + ]; + $checkId = $this->insertCitationCheckRecord($scope, $cite, $refNo, $referMap[$referIndex], $now); + if ($checkId <= 0) { + $skipped++; + continue; + } + + $this->appendCitationPendingJob($pendingJobs, $checkId, $refNo, $amId, $cite['text_start']); + $queued++; + $amIdsWithJobs[$amId] = true; + } + } + break; + } + $checkIds = $this->enqueueChecksSortedByReferenceNo($pendingJobs, $pArticleId, 'enqueue'); + foreach (array_keys($amIdsWithJobs) as $amId) { + $this->setAmRefCheckStatus($amId, self::AM_STATUS_RUNNING); + } + + return [ + 'article_id' => $articleId, + 'p_article_id' => $pArticleId, + 'queued' => $queued, + 'skipped' => $skipped, + 'check_ids' => $checkIds, + 'queue' => self::TRANSPORT_RABBITMQ, + ]; + } + + + + + + + + /** * 合并匹配两种 blue 引用排版,按在正文中的起始位置排序。 @@ -173,6 +278,7 @@ class ReferenceCheckService 'refer_index' => intval($this->arrGet($extra, 'refer_index', 0)), 'reference_no' => intval($this->arrGet($extra, 'reference_no', 0)), 'reference_raw' => (string)$this->arrGet($extra, 'reference_raw', ''), + 'cite_group_refs' => (string)$this->arrGet($extra, 'cite_group_refs', ''), 'cite_tag_start' => intval($this->arrGet($extra, 'cite_tag_start', 0)), 'cite_tag_end' => intval($this->arrGet($extra, 'cite_tag_end', 0)), 'text_start' => intval($this->arrGet($extra, 'text_start', 0)), @@ -229,6 +335,7 @@ class ReferenceCheckService $skipped = 0; $pendingJobs = []; $now = date('Y-m-d H:i:s'); + $sectionText = $this->resolveSectionTextForArticleMain($main); foreach ($citations as $cite) { foreach ($cite['reference_numbers'] as $refNo) { $referIndex = $refNo - 1; @@ -236,30 +343,24 @@ class ReferenceCheckService $skipped++; continue; } - $refer = $referMap[$referIndex]; - $referText = $this->formatReferForLlm($refer); - - $checkId = Db::name('article_reference_check_result')->insertGetId($this->newCheckRecordFields([ - 'article_id' => $main['article_id'], - 'p_article_id' => $pArticleId, - 'am_id' => intval($main['am_id']), - 'reference_no' => $refNo, - 'refer_index' => $refNo, - 'origin_text' => $cite['original_text'], - 'refer_text' => $referText, - 'p_refer_id' => $referMap[$referIndex]['p_refer_id'], - 'text_start' => $cite['text_start'], - 'text_end' => $cite['text_end'], - 'status' => self::RECORD_PENDING, - 'created_at' => $now, - 'updated_at' => $now, - ])); - $pendingJobs[] = [ - 'check_id' => intval($checkId), - 'reference_no' => intval($refNo), + $scope = [ + 'article_id' => $main['article_id'], + 'p_article_id' => $pArticleId, 'am_id' => intval($main['am_id']), - 'text_start' => intval($cite['text_start']), + 'section_text' => $sectionText, ]; + $checkId = $this->insertCitationCheckRecord($scope, $cite, $refNo, $referMap[$referIndex], $now); + if ($checkId <= 0) { + $skipped++; + continue; + } + $this->appendCitationPendingJob( + $pendingJobs, + $checkId, + $refNo, + intval($main['am_id']), + $cite['text_start'] + ); } } @@ -276,6 +377,14 @@ class ReferenceCheckService throw new \InvalidArgumentException('article_id is required'); } + if (!$this->isSecondPassEnabled()) { + return [ + 'article_id' => $articleId, + 'check_ids2' => [], + 'queued' => 0, + ]; + } + $rows = Db::name('article_reference_check_result') ->where('article_id', $articleId) ->where('status', self::RECORD_COMPLETED) @@ -300,87 +409,6 @@ class ReferenceCheckService 'queued' => count($checkIds2), ]; } - public function enqueueByPArticle($prod){ - if (empty($prod)) { - throw new \RuntimeException('production_article not found'); - } - $pArticleId = intval($prod['p_article_id']); - $articleId = intval($prod['article_id']); - $referMap = $this->loadReferMapByPArticleId($pArticleId); - - $mains = Db::name('article_main') - ->field('am_id,content,article_id,type,amt_id') - ->where('article_id', $articleId) - ->whereIn('state', [0, 2]) - ->order('sort asc') - ->select(); - if (empty($mains)) { - throw new \RuntimeException('article_main is empty'); - } - $queued = 0; - $skipped = 0; - $pendingJobs = []; - $amIdsWithJobs = []; - $now = date('Y-m-d H:i:s'); - foreach ($mains as $main) { - $amId = intval($main['am_id']); - $citations = $this->extractReferencesForArticleMain($main); - if (empty($citations)) { - $this->setAmRefCheckStatus($amId, self::AM_STATUS_NONE); - continue; - } - foreach ($citations as $cite) { - foreach ($cite['reference_numbers'] as $refNo) { - $referIndex = $refNo - 1; - if ($referIndex < 0 || !isset($referMap[$referIndex])) { - $skipped++; - continue; - } - $refer = $referMap[$referIndex]; - $referText = $this->formatReferForLlm($refer); - - // [70-73] 展开为 reference_no=70,71,72,73 共 4 条记录;先入队表,再按文献号正序校对 - $checkId = Db::name('article_reference_check_result')->insertGetId($this->newCheckRecordFields([ - 'article_id' => $main['article_id'], - 'p_article_id' => $pArticleId, - 'am_id' => $amId, - 'reference_no' => $refNo, - 'refer_index' => $refNo, - 'origin_text' => $cite['original_text'], - 'refer_text' => $referText, - 'p_refer_id' => $referMap[$referIndex]['p_refer_id'], - 'text_start' => $cite['text_start'], - 'text_end' => $cite['text_end'], - 'status' => self::RECORD_PENDING, - 'created_at' => $now, - 'updated_at' => $now, - ])); - - $pendingJobs[] = [ - 'check_id' => intval($checkId), - 'reference_no' => intval($refNo), - 'am_id' => $amId, - 'text_start' => intval($cite['text_start']), - ]; - $queued++; - $amIdsWithJobs[$amId] = true; - } - } - } - $checkIds = $this->enqueueChecksSortedByReferenceNo($pendingJobs, $pArticleId, 'enqueue'); - foreach (array_keys($amIdsWithJobs) as $amId) { - $this->setAmRefCheckStatus($amId, self::AM_STATUS_RUNNING); - } - - return [ - 'article_id' => $articleId, - 'p_article_id' => $pArticleId, - 'queued' => $queued, - 'skipped' => $skipped, - 'check_ids' => $checkIds, - 'queue' => self::TRANSPORT_RABBITMQ, - ]; - } public function enqueueByArticle($articleId){ if ($articleId <= 0) { throw new \InvalidArgumentException('article_id is required'); @@ -416,6 +444,7 @@ class ReferenceCheckService $this->setAmRefCheckStatus($amId, self::AM_STATUS_NONE); continue; } + $sectionText = $this->resolveSectionTextForArticleMain($main); foreach ($citations as $cite) { foreach ($cite['reference_numbers'] as $refNo) { $referIndex = $refNo - 1; @@ -423,32 +452,19 @@ class ReferenceCheckService $skipped++; continue; } - $refer = $referMap[$referIndex]; - $referText = $this->formatReferForLlm($refer); - - // [70-73] 展开为 reference_no=70,71,72,73 共 4 条记录;先入队表,再按文献号正序校对 - $checkId = Db::name('article_reference_check_result')->insertGetId($this->newCheckRecordFields([ - 'article_id' => $main['article_id'], - 'p_article_id' => $pArticleId, - 'am_id' => $amId, - 'reference_no' => $refNo, - 'refer_index' => $refNo, - 'origin_text' => $cite['original_text'], - 'refer_text' => $referText, - 'p_refer_id' => $referMap[$referIndex]['p_refer_id'], - 'text_start' => $cite['text_start'], - 'text_end' => $cite['text_end'], - 'status' => self::RECORD_PENDING, - 'created_at' => $now, - 'updated_at' => $now, - ])); - - $pendingJobs[] = [ - 'check_id' => intval($checkId), - 'reference_no' => intval($refNo), + $scope = [ + 'article_id' => $main['article_id'], + 'p_article_id' => $pArticleId, 'am_id' => $amId, - 'text_start' => intval($cite['text_start']), + 'section_text' => $sectionText, ]; + $checkId = $this->insertCitationCheckRecord($scope, $cite, $refNo, $referMap[$referIndex], $now); + if ($checkId <= 0) { + $skipped++; + continue; + } + + $this->appendCitationPendingJob($pendingJobs, $checkId, $refNo, $amId, $cite['text_start']); $queued++; $amIdsWithJobs[$amId] = true; } @@ -470,6 +486,200 @@ class ReferenceCheckService ]; } + /** + * 对「参考文献表中有、校对明细中从未出现过的 p_refer_id」重新扫描全文并入队校对。 + * + * 差集:production_article_refer(state=0) 的 p_refer_id + * 减去 article_reference_check_result 中已出现过的 p_refer_id。 + * 仅对上述缺失文献在全文(含表格)中查找引用标签,命中则新增明细并入队。 + * 不删除、不重跑已有明细。 + * + * @param array $prod production_article 行(需含 p_article_id、article_id) + * @return array + */ + public function enqueueNewlyMatchedByPArticle($prod) + { + if (empty($prod) || !is_array($prod)) { + throw new \RuntimeException('production_article not found'); + } + $pArticleId = intval($this->arrGet($prod, 'p_article_id', 0)); + $articleId = intval($this->arrGet($prod, 'article_id', 0)); + if ($pArticleId <= 0 || $articleId <= 0) { + throw new \InvalidArgumentException('production_article requires both p_article_id and article_id'); + } + + $existingCount = Db::name('article_reference_check_result') + ->where('p_article_id', $pArticleId) + ->count(); + if (intval($existingCount) <= 0) { + throw new \RuntimeException('no existing reference check records for p_article_id=' . $pArticleId . '; please run the first check first'); + } + + $missingCtx = $this->loadMissingPReferIdsByPArticleId($pArticleId); + $missingPReferIds = $missingCtx['missing_p_refer_ids']; + $missingRefNos = $missingCtx['missing_reference_nos']; + + if (empty($missingPReferIds)) { + return [ + 'article_id' => $articleId, + 'p_article_id' => $pArticleId, + 'queued' => 0, + 'skipped' => 0, + 'existing' => intval($existingCount), + 'missing_p_refer_ids' => [], + 'matched_p_refer_ids' => [], + 'still_unmatched_p_refer_ids' => [], + 'new_reference_nos' => [], + 'check_ids' => [], + 'queue' => self::TRANSPORT_RABBITMQ, + ]; + } + + $referMap = $this->loadReferMapByPArticleId($pArticleId); + + $mains = Db::name('article_main') + ->field('am_id,content,article_id,type,amt_id') + ->where('article_id', $articleId) + ->whereIn('state', [0, 2]) + ->order('sort asc') + ->select(); + if (empty($mains)) { + throw new \RuntimeException('article_main is empty'); + } + + $queued = 0; + $skipped = 0; + $pendingJobs = []; + $amIdsWithJobs = []; + $newReferenceNos = []; + $matchedPReferIds = []; + $now = date('Y-m-d H:i:s'); + + foreach ($mains as $main) { + $amId = intval($main['am_id']); + $citations = $this->extractReferencesForArticleMain($main); + if (empty($citations)) { + continue; + } + $sectionText = $this->resolveSectionTextForArticleMain($main); + foreach ($citations as $cite) { + foreach ($cite['reference_numbers'] as $refNo) { + if (!isset($missingRefNos[$refNo])) { + $skipped++; + continue; + } + + $referIndex = $refNo - 1; + if ($referIndex < 0 || !isset($referMap[$referIndex])) { + $skipped++; + continue; + } + + $refer = $referMap[$referIndex]; + $pReferId = intval($this->arrGet($refer, 'p_refer_id', 0)); + $scope = [ + 'article_id' => $main['article_id'], + 'p_article_id' => $pArticleId, + 'am_id' => $amId, + 'section_text' => $sectionText, + ]; + $checkId = $this->insertCitationCheckRecord($scope, $cite, $refNo, $refer, $now); + if ($checkId <= 0) { + $skipped++; + continue; + } + + $this->appendCitationPendingJob($pendingJobs, $checkId, $refNo, $amId, $cite['text_start']); + $queued++; + $amIdsWithJobs[$amId] = true; + $newReferenceNos[$refNo] = true; + if ($pReferId > 0) { + $matchedPReferIds[$pReferId] = true; + } + } + } + } + + $checkIds = $this->enqueueChecksSortedByReferenceNo($pendingJobs, $pArticleId, 'rematch_new'); + foreach (array_keys($amIdsWithJobs) as $amId) { + $this->setAmRefCheckStatus($amId, self::AM_STATUS_RUNNING); + } + + $newRefList = array_keys($newReferenceNos); + sort($newRefList, SORT_NUMERIC); + + $matchedList = array_keys($matchedPReferIds); + sort($matchedList, SORT_NUMERIC); + + $stillUnmatched = array_values(array_diff($missingPReferIds, $matchedList)); + sort($stillUnmatched, SORT_NUMERIC); + + return [ + 'article_id' => $articleId, + 'p_article_id' => $pArticleId, + 'queued' => $queued, + 'skipped' => $skipped, + 'existing' => intval($existingCount), + 'missing_p_refer_ids' => $missingPReferIds, + 'matched_p_refer_ids' => $matchedList, + 'still_unmatched_p_refer_ids' => $stillUnmatched, + 'new_reference_nos' => $newRefList, + 'check_ids' => $checkIds, + 'queue' => self::TRANSPORT_RABBITMQ, + ]; + } + + /** + * 参考文献表(state=0) 与校对明细中已出现的 p_refer_id 做差集。 + * + * @return array{missing_p_refer_ids:int[], missing_reference_nos:array} + */ + private function loadMissingPReferIdsByPArticleId($pArticleId) + { + $pArticleId = intval($pArticleId); + $missingPReferIds = []; + $missingRefNos = []; + + if ($pArticleId <= 0) { + return [ + 'missing_p_refer_ids' => $missingPReferIds, + 'missing_reference_nos' => $missingRefNos, + ]; + } + + $refers = Db::name('production_article_refer') + ->field('p_refer_id,index') + ->where('p_article_id', $pArticleId) + ->where('state', 0) + ->order('index asc') + ->select(); + + $checkedIds = Db::name('article_reference_check_result') + ->where('p_article_id', $pArticleId) + ->where('p_refer_id', '>', 0) + ->group('p_refer_id') + ->column('p_refer_id'); + $checkedSet = []; + foreach ($checkedIds as $id) { + $checkedSet[intval($id)] = true; + } + + foreach ($refers as $refer) { + $pReferId = intval($this->arrGet($refer, 'p_refer_id', 0)); + if ($pReferId <= 0 || isset($checkedSet[$pReferId])) { + continue; + } + $refNo = intval($this->arrGet($refer, 'index', 0)) + 1; + $missingPReferIds[] = $pReferId; + $missingRefNos[$refNo] = $pReferId; + } + + return [ + 'missing_p_refer_ids' => $missingPReferIds, + 'missing_reference_nos' => $missingRefNos, + ]; + } + /** * 根据该节全部明细行汇总更新 t_article_main.ref_check_status */ @@ -553,7 +763,7 @@ class ReferenceCheckService * * @return int 被删除的明细条数 */ - public function clearArticleChecksByPArticleId($pArticleId) + public function clearArticleChecksByPArticleId($pArticleId,$articleId=0) { $pArticleId = intval($pArticleId); if ($pArticleId <= 0) { @@ -561,10 +771,12 @@ class ReferenceCheckService } // 先反查 article_id(用于重置 article_main.ref_check_status 节级状态) - $articleId = intval(Db::name('production_article') - ->where('p_article_id', $pArticleId) - ->whereIn('state', [0, 2]) - ->value('article_id')); + if($articleId==0){ + $articleId = intval(Db::name('production_article') + ->where('p_article_id', $pArticleId) + ->whereIn('state', [0, 2]) + ->value('article_id')); + } $deleted = Db::name('article_reference_check_result') ->where('p_article_id', $pArticleId) @@ -598,6 +810,29 @@ class ReferenceCheckService return intval($deleted); } + /** + * 按 article_id + am_id 删除单节正文下的引用校对明细,并同步该节 ref_check_status。 + * + * @return int 被删除的明细条数 + */ + public function clearChecksByAmId($articleId, $amId) + { + $articleId = intval($articleId); + $amId = intval($amId); + if ($articleId <= 0 || $amId <= 0) { + return 0; + } + + $deleted = Db::name('article_reference_check_result') + ->where('article_id', $articleId) + ->where('am_id', $amId) + ->delete(); + + $this->syncAmRefCheckStatus($amId); + + return intval($deleted); + } + /** * 文献列表局部挪动后,仅刷新指定 p_refer_id 对应的校对明细 reference_no / refer_index。 * @@ -850,7 +1085,7 @@ class ReferenceCheckService } $rows = Db::name('article_reference_check_result') - ->field('id,p_refer_id,reference_no,am_id,status,confidence,is_match,reason,text_start,text_end,updated_at') + ->field('id,p_refer_id,reference_no,am_id,status,confidence,is_match,reason,support_role,combined_can_support,combined_confidence,combined_reason,text_start,text_end,cite_group_refs,updated_at') ->where('p_article_id', $pArticleId) ->order('reference_no asc, id asc') ->select(); @@ -916,16 +1151,22 @@ class ReferenceCheckService } $groups[$refNo]['records'][] = [ - 'check_id' => intval($this->arrGet($row, 'id', 0)), - 'am_id' => intval($this->arrGet($row, 'am_id', 0)), - 'status' => $st, - 'confidence' => $confidence, - 'is_pass' => $isPass, - 'is_match' => intval($this->arrGet($row, 'is_match', 0)), - 'reason' => (string)$this->arrGet($row, 'reason', ''), - 'text_start' => intval($this->arrGet($row, 'text_start', 0)), - 'text_end' => intval($this->arrGet($row, 'text_end', 0)), - 'last_updated_at' => $upd, + 'check_id' => intval($this->arrGet($row, 'id', 0)), + 'am_id' => intval($this->arrGet($row, 'am_id', 0)), + 'status' => $st, + 'confidence' => $confidence, + 'is_pass' => $isPass, + 'is_match' => intval($this->arrGet($row, 'is_match', 0)), + 'reason' => (string)$this->arrGet($row, 'reason', ''), + 'support_role' => (string)$this->arrGet($row, 'support_role', ''), + 'combined_can_support' => intval($this->arrGet($row, 'combined_can_support', 0)), + 'combined_confidence' => floatval($this->arrGet($row, 'combined_confidence', 0)), + 'combined_reason' => (string)$this->arrGet($row, 'combined_reason', ''), + 'cite_group_refs' => (string)$this->arrGet($row, 'cite_group_refs', ''), + 'cite_check_mode' => $this->isJointCiteGroupRefs($this->arrGet($row, 'cite_group_refs', '')) ? 'joint' : 'single', + 'text_start' => intval($this->arrGet($row, 'text_start', 0)), + 'text_end' => intval($this->arrGet($row, 'text_end', 0)), + 'last_updated_at' => $upd, ]; } @@ -993,7 +1234,7 @@ class ReferenceCheckService } $rows = Db::name('article_reference_check_result') - ->field('id,p_article_id,reference_no,am_id,status,confidence,is_match,reason,updated_at') + ->field('id,p_article_id,reference_no,am_id,status,confidence,is_match,reason,support_role,combined_can_support,combined_confidence,combined_reason,cite_group_refs,updated_at') ->where('p_refer_id', $pReferId) ->order('id asc') ->select(); @@ -1036,13 +1277,19 @@ class ReferenceCheckService } $list[] = [ - 'check_id' => intval($this->arrGet($row, 'id', 0)), - 'am_id' => intval($this->arrGet($row, 'am_id', 0)), - 'status' => $st, - 'confidence' => $confidence, - 'reason' => (string)$this->arrGet($row, 'reason', ''), - 'is_match' => intval($this->arrGet($row, 'is_match', 0)), - 'is_pass' => $isPass, + 'check_id' => intval($this->arrGet($row, 'id', 0)), + 'am_id' => intval($this->arrGet($row, 'am_id', 0)), + 'status' => $st, + 'confidence' => $confidence, + 'reason' => (string)$this->arrGet($row, 'reason', ''), + 'is_match' => intval($this->arrGet($row, 'is_match', 0)), + 'is_pass' => $isPass, + 'support_role' => (string)$this->arrGet($row, 'support_role', ''), + 'combined_can_support' => intval($this->arrGet($row, 'combined_can_support', 0)), + 'combined_confidence' => floatval($this->arrGet($row, 'combined_confidence', 0)), + 'combined_reason' => (string)$this->arrGet($row, 'combined_reason', ''), + 'cite_group_refs' => (string)$this->arrGet($row, 'cite_group_refs', ''), + 'cite_check_mode' => $this->isJointCiteGroupRefs($this->arrGet($row, 'cite_group_refs', '')) ? 'joint' : 'single', ]; } @@ -1117,8 +1364,8 @@ class ReferenceCheckService throw new \RuntimeException('no existing reference check records for p_article_id=' . $pArticleId); } - $cleared = $this->clearArticleChecks($articleId); - $enqueueResult = $this->enqueueByArticle($articleId); + $cleared = $this->clearArticleChecksByPArticleId($aProductionArticle['p_article_id'],$aProductionArticle['article_id']); + $enqueueResult = $this->enqueueByPArticle($aProductionArticle); if (!is_array($enqueueResult)) { $enqueueResult = []; @@ -1178,6 +1425,7 @@ class ReferenceCheckService */ public function updateCheckResult($checkId, array $fields) { + DbReconnectHelper::ensure(); $checkId = intval($checkId); if ($checkId <= 0) { throw new \InvalidArgumentException('invalid check id'); @@ -1186,6 +1434,9 @@ class ReferenceCheckService if (isset($fields['reason'])) { $fields['reason'] = mb_substr(trim((string)$fields['reason']), 0, 512); } + if (isset($fields['combined_reason'])) { + $fields['combined_reason'] = mb_substr(trim((string)$fields['combined_reason']), 0, 512); + } if (isset($fields['error_msg'])) { $fields['error_msg'] = mb_substr(trim((string)$fields['error_msg']), 0, 512); } @@ -1540,6 +1791,581 @@ class ReferenceCheckService return implode("\n", $parts); } + /** + * 从 extractReferences 结果提取引用标签元数据([1,2]、[70-73] 等同标签下各明细共用) + */ + private function citationMetaFromExtract(array $cite) + { + return [ + 'reference_raw' => (string)$this->arrGet($cite, 'reference_raw', ''), + 'cite_group_refs' => $this->formatCiteGroupRefs((array)$this->arrGet($cite, 'reference_numbers', [])), + 'cite_tag_start' => intval($this->arrGet($cite, 'reference_start', 0)), + 'cite_tag_end' => intval($this->arrGet($cite, 'reference_end', 0)), + 'origin_text' => (string)$this->arrGet($cite, 'original_text', ''), + 'text_start' => intval($this->arrGet($cite, 'text_start', 0)), + 'text_end' => intval($this->arrGet($cite, 'text_end', 0)), + ]; + } + + /** + * 引用组展开序号:1,2 或 4,5,6 或 3 + */ + private function formatCiteGroupRefs(array $refNumbers) + { + $nums = []; + foreach ($refNumbers as $n) { + $n = intval($n); + if ($n > 0) { + $nums[$n] = $n; + } + } + if (empty($nums)) { + return ''; + } + $list = array_values($nums); + sort($list, SORT_NUMERIC); + + return implode(',', $list); + } + + private function isJointCiteGroupRefs($citeGroupRefs) + { + return strpos((string)$citeGroupRefs, ',') !== false; + } + + private function resolveCiteGroupRefsFromRow(array $row, array $groupRows = null) + { + $refs = trim((string)$this->arrGet($row, 'cite_group_refs', '')); + if ($refs !== '') { + return $refs; + } + if ($groupRows === null) { + $groupRows = $this->findCitationGroupRows($row); + } + $nums = []; + foreach ($groupRows as $gr) { + $n = intval($this->arrGet($gr, 'reference_no', 0)); + if ($n > 0) { + $nums[$n] = $n; + } + } + if (empty($nums)) { + $n = intval($this->arrGet($row, 'reference_no', 0)); + return $n > 0 ? (string)$n : ''; + } + $list = array_values($nums); + sort($list, SORT_NUMERIC); + + return implode(',', $list); + } + + private function hasSecondPassCompleted(array $row) + { + $reason = (string)$this->arrGet($row, 'reason', ''); + return stripos($reason, '[DOI复核') !== false || stripos($reason, '[Crossref复核') !== false; + } + + private function buildSecondPassReasonTag(array $row, array $payload, array $groupRows = null) + { + $citeGroupRefs = $this->resolveCiteGroupRefsFromRow($row, $groupRows); + $tag = '[DOI复核'; + if ($citeGroupRefs !== '') { + $tag .= ' 文献' . $citeGroupRefs; + } + if (trim((string)$this->arrGet($payload, 'doi_used', '')) !== '') { + $tag .= ' ' . trim((string)$payload['doi_used']); + } + $tag .= ']'; + + return $tag; + } + + /** + * 从 refer 抓取 DOI 真实文献块(PubMed 优先,回落 Crossref) + * + * @return array{text:string, has_abstract:bool, doi:string} + */ + private function resolveDoiRecheckFromRefer($refer) + { + if (!is_array($refer) || empty($refer)) { + return ['text' => '', 'has_abstract' => false, 'doi' => '']; + } + $text = trim($this->fetchDoiLiteratureBlock($refer)); + $hasAbstract = $text !== '' && preg_match('/Abstract:\s*\S/u', $text); + + return [ + 'text' => $text, + 'has_abstract' => $hasAbstract, + 'doi' => $this->extractDoiFromRefer($refer), + ]; + } + + /** + * 校对时使用的正文:t_article_main 一条记录(正文或表格展平文本) + */ + public function resolveParagraphContextForJob(array $row, $maxChars = 8000) + { + return $this->resolveMainContentForJob($row, $maxChars); + } + + /** + * 入队时快照引用处局部上下文,写入 origin_text(与 text_start/text_end 对应) + */ + private function resolveSectionTextForArticleMain(array $main, $maxChars = 8000) + { + $raw = trim($this->resolveArticleMainCheckContent($main)); + if ($raw === '') { + return ''; + } + + return $this->normalizeCheckContentForLlm($raw, $maxChars); + } + + /** + * 同一 blue 引用标签(如 [1,2])下为单个文献号写入校对明细 + * + * @return int|null check_id + */ + private function insertCitationCheckRecord(array $scope, array $cite, $refNo, array $refer, $now) + { + $refNo = intval($refNo); + $referIndex = $refNo - 1; + if ($referIndex < 0) { + return null; + } + + $meta = $this->citationMetaFromExtract($cite); + $referText = $this->formatReferForLlm($refer); + // origin_text 存引用处局部上下文(extractLocalCitationContext),非整节 am 正文 + $originText = trim((string)$meta['origin_text']); + if ($originText === '') { + $originText = trim((string)$this->arrGet($scope, 'section_text', '')); + } + + return intval(Db::name('article_reference_check_result')->insertGetId($this->newCheckRecordFields([ + 'article_id' => intval($this->arrGet($scope, 'article_id', 0)), + 'p_article_id' => intval($this->arrGet($scope, 'p_article_id', 0)), + 'am_id' => intval($this->arrGet($scope, 'am_id', 0)), + 'reference_no' => $refNo, + 'refer_index' => $refNo, + 'origin_text' => $originText, + 'refer_text' => $referText, + 'p_refer_id' => intval($this->arrGet($refer, 'p_refer_id', 0)), + 'reference_raw' => $meta['reference_raw'], + 'cite_group_refs' => $meta['cite_group_refs'], + 'cite_tag_start' => $meta['cite_tag_start'], + 'cite_tag_end' => $meta['cite_tag_end'], + 'text_start' => $meta['text_start'], + 'text_end' => $meta['text_end'], + 'status' => self::RECORD_PENDING, + 'created_at' => $now, + 'updated_at' => $now, + ]))); + } + + private function appendCitationPendingJob(array &$pendingJobs, $checkId, $refNo, $amId, $textStart) + { + $pendingJobs[] = [ + 'check_id' => intval($checkId), + 'reference_no' => intval($refNo), + 'am_id' => intval($amId), + 'text_start' => intval($textStart), + ]; + } + + /** + * 同一引用标签下的全部校对明细([1,2] 展开后 reference_no 不同但 cite_tag_* 相同) + * + * @return array[] + */ + private function findCitationGroupRows(array $row) + { + $amId = intval($this->arrGet($row, 'am_id', 0)); + if ($amId <= 0) { + return [$row]; + } + + $citeTagStart = intval($this->arrGet($row, 'cite_tag_start', 0)); + $citeTagEnd = intval($this->arrGet($row, 'cite_tag_end', 0)); + $q = Db::name('article_reference_check_result')->where('am_id', $amId); + + if ($citeTagStart > 0 && $citeTagEnd > $citeTagStart) { + $q->where('cite_tag_start', $citeTagStart)->where('cite_tag_end', $citeTagEnd); + } else { + $textStart = intval($this->arrGet($row, 'text_start', 0)); + $textEnd = intval($this->arrGet($row, 'text_end', 0)); + $referenceRaw = trim((string)$this->arrGet($row, 'reference_raw', '')); + $citeGroupRefs = trim((string)$this->arrGet($row, 'cite_group_refs', '')); + $q->where('text_start', $textStart)->where('text_end', $textEnd); + if ($referenceRaw !== '') { + $q->where('reference_raw', $referenceRaw); + } elseif ($citeGroupRefs !== '') { + $q->where('cite_group_refs', $citeGroupRefs); + } + } + + $rows = $q->order('reference_no asc')->select(); + return empty($rows) ? [$row] : $rows; + } + + private function resolveCitationGroupLeaderRefNo(array $groupRows) + { + $leader = PHP_INT_MAX; + foreach ($groupRows as $gr) { + $refNo = intval($this->arrGet($gr, 'reference_no', 0)); + if ($refNo > 0 && $refNo < $leader) { + $leader = $refNo; + } + } + + return $leader === PHP_INT_MAX ? 0 : $leader; + } + + private function findCitationGroupRowByRefNo(array $groupRows, $refNo) + { + $refNo = intval($refNo); + foreach ($groupRows as $gr) { + if (intval($this->arrGet($gr, 'reference_no', 0)) === $refNo) { + return $gr; + } + } + + return null; + } + + private function isCitationGroupCheck(array $groupRows) + { + return count($groupRows) > 1; + } + + private function resolveReferTextForCheckRow(array $row, $refer = null) + { + if (is_array($refer) && !empty($refer)) { + return $this->formatReferForLlm($refer); + } + + return trim((string)$this->arrGet($row, 'refer_text', '')); + } + + /** + * 将同一引用标签下多条文献书目拼成一次 LLM 校对的 refer_text + */ + private function buildCombinedReferTextForCitationGroup(array $groupRows) + { + $blocks = []; + foreach ($groupRows as $gr) { + $refNo = intval($this->arrGet($gr, 'reference_no', 0)); + if ($refNo <= 0) { + continue; + } + + $refer = null; + if (intval($this->arrGet($gr, 'p_refer_id', 0)) > 0) { + $refer = Db::name('production_article_refer') + ->where('p_refer_id', intval($gr['p_refer_id'])) + ->where('state', 0) + ->find(); + } + $text = $this->resolveReferTextForCheckRow($gr, $refer); + if ($text === '') { + continue; + } + $blocks[] = '【参考文献 ' . $refNo . '】' . "\n" . $text; + } + + return implode("\n\n", $blocks); + } + + /** + * @return array{refer_text:string, doi_block:string, has_abstract:bool, doi_used:string} + */ + private function prepareRecheckPayloadForCitationGroup(array $groupRows) + { + $referText = $this->buildCombinedReferTextForCitationGroup($groupRows); + $doiParts = []; + $doiUsed = []; + $hasAbstract = false; + + foreach ($groupRows as $gr) { + $refNo = intval($this->arrGet($gr, 'reference_no', 0)); + if ($refNo <= 0 || intval($this->arrGet($gr, 'p_refer_id', 0)) <= 0) { + continue; + } + DbReconnectHelper::ensure(); + $refer = Db::name('production_article_refer') + ->where('p_refer_id', intval($gr['p_refer_id'])) + ->where('state', 0) + ->find(); + if (empty($refer)) { + continue; + } + $bundle = (new ReferenceLiteratureFetchService())->fetchAndCleanForRefer($refer); + $checkId = intval($this->arrGet($gr, 'id', $this->arrGet($gr, 'check_id', 0))); + if ($checkId > 0) { + $this->persistLiteratureOnCheckRow($checkId, $bundle); + } + + $block = $this->buildLiteratureBlockFromBundle($refNo, $bundle); + if ($block === '') { + continue; + } + if (trim((string)($bundle['abstract_final'] ?? '')) !== '') { + $hasAbstract = true; + } + $doiParts[] = $block; + $doi = trim((string)($bundle['doi'] ?? '')); + if ($doi !== '') { + $doiUsed[] = $doi; + } + } + + return [ + 'refer_text' => $referText, + 'doi_block' => implode("\n\n", $doiParts), + 'has_abstract' => $hasAbstract, + 'doi_used' => implode(',', $doiUsed), + ]; + } + + private function applyCheckResultFromRow($checkId, array $sourceRow) + { + $this->updateCheckResult($checkId, [ + 'can_support' => intval($this->arrGet($sourceRow, 'can_support', 0)), + 'is_match' => intval($this->arrGet($sourceRow, 'is_match', 0)), + 'confidence' => floatval($this->arrGet($sourceRow, 'confidence', 0)), + 'reason' => (string)$this->arrGet($sourceRow, 'reason', ''), + 'support_role' => (string)$this->arrGet($sourceRow, 'support_role', ''), + 'combined_can_support' => intval($this->arrGet($sourceRow, 'combined_can_support', 0)), + 'combined_confidence' => floatval($this->arrGet($sourceRow, 'combined_confidence', 0)), + 'combined_reason' => (string)$this->arrGet($sourceRow, 'combined_reason', ''), + 'status' => self::RECORD_COMPLETED, + 'error_msg' => '', + ]); + } + + /** + * 将 LLM results 数组按 reference_no 写入同一引用组内的各行 + */ + private function applyCitationGroupCheckResults(array $groupRows, array $llmResponse, $reasonPrefix = '') + { + $results = isset($llmResponse['results']) && is_array($llmResponse['results']) + ? $llmResponse['results'] : []; + if (empty($results)) { + return false; + } + + $byRefNo = []; + foreach ($results as $item) { + if (!is_array($item)) { + continue; + } + $refNo = intval($this->arrGet($item, 'reference_no', 0)); + if ($refNo > 0) { + $byRefNo[$refNo] = $item; + } + } + + $applied = 0; + $expected = 0; + $reasonPrefix = trim((string)$reasonPrefix); + foreach ($groupRows as $gr) { + $refNo = intval($this->arrGet($gr, 'reference_no', 0)); + if ($refNo <= 0) { + continue; + } + $expected++; + if (!isset($byRefNo[$refNo])) { + continue; + } + $item = $byRefNo[$refNo]; + $canSupport = !empty($item['can_support']) ? 1 : 0; + $reason = trim($reasonPrefix . ' ' . (string)$this->arrGet($item, 'reason', '')); + $this->updateCheckResult($this->resolveCheckRowId($gr), [ + 'can_support' => $canSupport, + 'is_match' => array_key_exists('is_match', $item) ? (!empty($item['is_match']) ? 1 : 0) : $canSupport, + 'confidence' => floatval($this->arrGet($item, 'confidence', 0)), + 'reason' => $reason, + 'support_role' => (string)$this->arrGet($item, 'support_role', ''), + 'combined_can_support' => !empty($item['combined_can_support']) ? 1 : 0, + 'combined_confidence' => floatval($this->arrGet($item, 'combined_confidence', 0)), + 'combined_reason' => (string)$this->arrGet($item, 'combined_reason', ''), + 'status' => self::RECORD_COMPLETED, + 'error_msg' => '', + ]); + $applied++; + } + + return $expected > 0 && $applied === $expected; + } + + private function findLlmResultItemForRow(array $llmResponse, array $row) + { + $results = isset($llmResponse['results']) && is_array($llmResponse['results']) + ? $llmResponse['results'] : []; + $refNo = intval($this->arrGet($row, 'reference_no', 0)); + foreach ($results as $item) { + if (is_array($item) && intval($this->arrGet($item, 'reference_no', 0)) === $refNo) { + return $item; + } + } + + return null; + } + + private function formatCheckReturnFromRow(array $row) + { + $checkId = $this->resolveCheckRowId($row); + + return [ + 'check_id' => $checkId, + 'can_support' => intval($this->arrGet($row, 'can_support', 0)), + 'is_match' => intval($this->arrGet($row, 'is_match', 0)), + 'confidence' => floatval($this->arrGet($row, 'confidence', 0)), + 'reason' => (string)$this->arrGet($row, 'reason', ''), + 'support_role' => (string)$this->arrGet($row, 'support_role', ''), + 'combined_can_support' => intval($this->arrGet($row, 'combined_can_support', 0)), + 'combined_confidence' => floatval($this->arrGet($row, 'combined_confidence', 0)), + 'combined_reason' => (string)$this->arrGet($row, 'combined_reason', ''), + ]; + } + + private function shouldRunSecondPassForLlmResults(array $groupRows, array $llmResponse) + { + if (!$this->isSecondPassEnabled()) { + return false; + } + + $results = isset($llmResponse['results']) && is_array($llmResponse['results']) + ? $llmResponse['results'] : []; + if (empty($results)) { + return false; + } + + $byRefNo = []; + foreach ($results as $item) { + if (!is_array($item)) { + continue; + } + $refNo = intval($this->arrGet($item, 'reference_no', 0)); + if ($refNo > 0) { + $byRefNo[$refNo] = $item; + } + } + + foreach ($groupRows as $gr) { + $refNo = intval($this->arrGet($gr, 'reference_no', 0)); + if ($refNo <= 0 || !isset($byRefNo[$refNo])) { + continue; + } + $item = $byRefNo[$refNo]; + if (floatval($this->arrGet($item, 'confidence', 0)) <= self::PASS_CONFIDENCE_THRESHOLD) { + return true; + } + if (floatval($this->arrGet($item, 'combined_confidence', 0)) <= self::PASS_CONFIDENCE_THRESHOLD) { + return true; + } + } + + return false; + } + + /** + * 本引用位置附近上下文(用于 LLM 判断具体支撑哪句) + */ + public function resolveCitationLocalContextForJob(array $row, $maxChars = 3000) + { + $textStart = intval($this->arrGet($row, 'text_start', 0)); + $textEnd = intval($this->arrGet($row, 'text_end', 0)); + $amId = intval($this->arrGet($row, 'am_id', 0)); + if ($amId <= 0 || $textEnd <= $textStart) { + return ''; + } + + $main = Db::name('article_main') + ->field('content,type,amt_id,article_id') + ->where('am_id', $amId) + ->find(); + if (empty($main)) { + return ''; + } + + $raw = trim($this->resolveArticleMainCheckContent($main)); + if ($raw === '') { + return ''; + } + + $slice = $this->buildCitationContextText($raw, $textStart, $textEnd); + if (trim($slice) === '') { + return ''; + } + + return $this->normalizeCheckContentForLlm($slice, $maxChars); + } + + /** + * 相关性校对专用:段内后续引用在「本句」基础上再向前扩展 1–2 句(不早于上一引用标签), + * 以便覆盖紧邻的前置 claim,同时避免整段混用多个引用点的论述。 + */ + public function resolveCitationLocalContextForRelevanceJob(array $row, $maxChars = 3000, $extraSentences = 2) + { + DbReconnectHelper::ensure(); + $textStart = intval($this->arrGet($row, 'text_start', 0)); + $textEnd = intval($this->arrGet($row, 'text_end', 0)); + $amId = intval($this->arrGet($row, 'am_id', 0)); + $tagStart = intval($this->arrGet($row, 'cite_tag_start', 0)); + $fallback = trim((string)$this->arrGet($row, 'origin_text', '')); + + if ($amId <= 0 || $textEnd <= $textStart) { + return $fallback; + } + + $main = Db::name('article_main') + ->field('content,type,amt_id,article_id') + ->where('am_id', $amId) + ->find(); + if (empty($main)) { + return $fallback; + } + + $raw = trim($this->resolveArticleMainCheckContent($main)); + if ($raw === '') { + return $fallback; + } + + $paragraphStart = $tagStart > 0 ? $this->findParagraphStart($raw, $tagStart) : 0; + $prevTagEnd = $tagStart > 0 ? $this->resolvePriorCitationTagEnd($raw, $tagStart) : $paragraphStart; + $extendedStart = $textStart; + if ($prevTagEnd > $paragraphStart) { + $extendedStart = $this->extendContextStartBackward( + $raw, + $textStart, + max($paragraphStart, $prevTagEnd), + $extraSentences + ); + } + + $slice = $this->buildCitationContextText($raw, $extendedStart, $textEnd); + $slice = ltrim($slice, ". \t\n\r"); + if (trim($slice) === '') { + return $fallback; + } + + return $this->normalizeCheckContentForLlm($slice, $maxChars); + } + + /** + * 联合校对结果写回同一引用标签下的全部明细 + */ + private function applyCheckResultToCitationGroup(array $groupRows, array $fields) + { + foreach ($groupRows as $gr) { + $gid = $this->resolveCheckRowId($gr); + if ($gid > 0) { + $this->updateCheckResult($gid, $fields); + } + } + } + /** * 编辑某条文献内容后,按 p_refer_id 异步重新校对该文献对应的全部 check 明细 * @@ -1597,18 +2423,12 @@ class ReferenceCheckService ]; } - $resetFields = $this->newCheckRecordFields([ + $resetFields = $this->newCheckRecordFields($this->referenceCheckResultResetFields([ 'refer_text' => $referText, 'refer_index' => $referenceNo, 'reference_no' => $referenceNo, - 'status' => self::RECORD_PENDING, - 'is_match' => 0, - 'can_support' => 0, - 'confidence' => 0, - 'reason' => '', - 'error_msg' => '', 'updated_at' => $now, - ], self::QUEUE_PENDING, 0); + ]), self::QUEUE_PENDING, 0); $pendingJobs = []; $amIds = []; @@ -1686,15 +2506,9 @@ class ReferenceCheckService } $now = date('Y-m-d H:i:s'); - $resetFields = $this->newCheckRecordFields([ - 'status' => self::RECORD_PENDING, - 'is_match' => 0, - 'can_support' => 0, - 'confidence' => 0, - 'reason' => '', - 'error_msg' => '', - 'updated_at' => $now, - ], self::QUEUE_PENDING, 0); + $resetFields = $this->newCheckRecordFields($this->referenceCheckResultResetFields([ + 'updated_at' => $now, + ]), self::QUEUE_PENDING, 0); $pendingJobs = []; $amIds = []; @@ -1729,6 +2543,93 @@ class ReferenceCheckService ]; } + /** + * 某条参考文献下「校对失败」重跑,并将失败行所在同一引用标签分组(如 [1,2])全部一并重跑。 + * + * @param int $pReferId + * @param int $pArticleId + * @return array{p_refer_id:int, p_article_id:int, reset:int, queued:int, check_ids:int[], queue:string} + */ + public function enqueueRecheckFailedByPReferIdWithGroup($pReferId, $pArticleId = 0) + { + $pReferId = intval($pReferId); + if ($pReferId <= 0) { + throw new \InvalidArgumentException('p_refer_id is required'); + } + + $q = Db::name('article_reference_check_result') + ->where('p_refer_id', $pReferId) + ->where('status', self::RECORD_FAILED); + $pArticleId = intval($pArticleId); + if ($pArticleId > 0) { + $q->where('p_article_id', $pArticleId); + } + + $rows = $q->select(); + if (empty($rows)) { + return [ + 'p_refer_id' => $pReferId, + 'p_article_id' => $pArticleId, + 'reset' => 0, + 'queued' => 0, + 'check_ids' => [], + 'queue' => self::TRANSPORT_RABBITMQ, + ]; + } + + if ($pArticleId <= 0) { + $pArticleId = intval($this->arrGet($rows[0], 'p_article_id', 0)); + } + + $now = date('Y-m-d H:i:s'); + $resetFields = $this->newCheckRecordFields($this->referenceCheckResultResetFields([ + 'updated_at' => $now, + ]), self::QUEUE_PENDING, 0); + + $targetRows = []; + foreach ($rows as $row) { + $groupRows = $this->findCitationGroupRows($row); + foreach ($groupRows as $gr) { + $checkId = $this->resolveCheckRowId($gr); + if ($checkId > 0) { + $targetRows[$checkId] = $gr; + } + } + } + + $pendingJobs = []; + $amIds = []; + foreach ($targetRows as $row) { + $checkId = $this->resolveCheckRowId($row); + Db::name('article_reference_check_result')->where('id', $checkId)->update($resetFields); + $pendingJobs[] = [ + 'check_id' => $checkId, + 'reference_no' => intval($this->arrGet($row, 'reference_no', 0)), + 'am_id' => intval($this->arrGet($row, 'am_id', 0)), + 'text_start' => intval($this->arrGet($row, 'text_start', 0)), + ]; + $amId = intval($this->arrGet($row, 'am_id', 0)); + if ($amId > 0) { + $amIds[$amId] = true; + } + } + + foreach (array_keys($amIds) as $amId) { + $this->setAmRefCheckStatus($amId, self::AM_STATUS_RUNNING); + } + + $checkIds = $this->enqueueChecksSortedByReferenceNo($pendingJobs, $pArticleId, 'recheck_failed'); + + return [ + 'p_refer_id' => $pReferId, + 'p_article_id' => $pArticleId, + 'reset' => count($targetRows), + 'queued' => count($checkIds), + 'check_ids' => $checkIds, + 'queue' => self::TRANSPORT_RABBITMQ, + ]; + } + public function recheckByRefer($articleId, $pReferId = 0, $referenceNo = 0) { $articleId = intval($articleId); @@ -1763,19 +2664,13 @@ class ReferenceCheckService ]; } - $resetFields = $this->newCheckRecordFields([ + $resetFields = $this->newCheckRecordFields($this->referenceCheckResultResetFields([ 'refer_text' => $referText, 'p_refer_id' => $pReferId, 'p_article_id' => $pArticleId, 'refer_index' => $referenceNo, - 'status' => 0, - 'is_match' => 0, - 'can_support' => 0, - 'confidence' => 0, - 'reason' => '', - 'error_msg' => '', 'updated_at' => $now, - ], self::QUEUE_PENDING, 0); + ]), self::QUEUE_PENDING, 0); $pendingJobs = []; $amIds = []; @@ -1847,13 +2742,34 @@ class ReferenceCheckService */ public function runReferenceCheckOnce($checkId) { + DbReconnectHelper::ensure(); $checkId = intval($checkId); $row = Db::name('article_reference_check_result')->where('id', $checkId)->find(); if (empty($row)) { throw new \RuntimeException('article_reference_check_result not found, id=' . $checkId); } - $contentA = $this->resolveMainContentForJob($row); + if (intval($row['status']) === self::RECORD_COMPLETED) { + return $this->formatCheckReturnFromRow($row); + } + + $groupRows = $this->findCitationGroupRows($row); + $isGroup = $this->isCitationGroupCheck($groupRows); + if ($isGroup) { + $leaderRefNo = $this->resolveCitationGroupLeaderRefNo($groupRows); + $currentRefNo = intval($this->arrGet($row, 'reference_no', 0)); + if ($currentRefNo !== $leaderRefNo) { + $freshRow = Db::name('article_reference_check_result')->where('id', $checkId)->find(); + if (!empty($freshRow) && intval($freshRow['status']) === self::RECORD_COMPLETED) { + return $this->formatCheckReturnFromRow($freshRow); + } + throw new \RuntimeException('Citation group leader check not finished yet, reference_no=' . $leaderRefNo); + } + } + + $contentA = $this->resolveParagraphContextForJob($row); + $localContext = $this->resolveCitationLocalContextForJob($row); + $citeGroupRefs = $this->resolveCiteGroupRefsFromRow($row, $groupRows); $refer = null; if (intval($row['p_refer_id']) > 0) { $refer = Db::name('production_article_refer') @@ -1862,95 +2778,151 @@ class ReferenceCheckService ->find(); } - if ($refer) { - $contentB = $this->formatReferForLlm($refer); - } else { - $contentB = trim((string)$this->arrGet($row, 'refer_text', '')); - } + $contentB = $this->buildCombinedReferTextForCitationGroup($groupRows); + DbReconnectHelper::release(); + $doiPayload = $this->prepareRecheckPayloadForCitationGroup($groupRows); + $doiBlock = trim((string)$this->arrGet($doiPayload, 'doi_block', '')); + DbReconnectHelper::ensure(); if ($contentA === '' || $contentB === '') { - $this->updateCheckResult($checkId, [ + $failFields = [ 'status' => self::RECORD_FAILED, 'error_msg' => 'Missing section content (text/table) or refer_text', - ]); + ]; + if ($isGroup) { + $this->applyCheckResultToCitationGroup($groupRows, $failFields); + } else { + $this->updateCheckResult($checkId, $failFields); + } throw new \RuntimeException('Missing section content (text/table) or refer_text'); } - $llmResult = (new LLMService())->checkReference($contentA, $contentB, false); + DbReconnectHelper::release(); + $llmResult = (new LLMService())->checkReference( + $contentA, + $contentB, + false, + $doiBlock !== '' ? $doiBlock : null, + $citeGroupRefs, + $localContext + ); + DbReconnectHelper::ensure(); $requestFailed = !empty($llmResult['request_failed']); - $canSupport = $this->parseLlmCanSupport($llmResult); - $confidence = floatval(isset($llmResult['confidence']) ? $llmResult['confidence'] : 0); - $reason = isset($llmResult['reason']) ? $llmResult['reason'] : ''; - // LLM 通讯失败:写 status=RECORD_FAILED(3) + error_msg,抛异常由 MQ worker 重试 - if ($requestFailed) { - $this->updateCheckResult($checkId, [ - 'confidence' => $confidence, - 'reason' => $reason, - 'status' => self::RECORD_FAILED, - 'error_msg' => $reason, - ]); - throw new \RuntimeException($reason !== '' ? $reason : 'LLM request failed'); + if ($requestFailed || !$this->applyCitationGroupCheckResults($groupRows, $llmResult)) { + $failReason = isset($llmResult['reason']) ? (string)$llmResult['reason'] : 'LLM request failed or empty results'; + $failFields = [ + 'status' => self::RECORD_FAILED, + 'error_msg' => $failReason, + ]; + if ($isGroup) { + $this->applyCheckResultToCitationGroup($groupRows, $failFields); + } else { + $this->updateCheckResult($checkId, $failFields); + } + throw new \RuntimeException($failReason !== '' ? $failReason : 'LLM request failed'); } - $this->updateCheckResult($checkId, [ - 'can_support' => $canSupport ? 1 : 0, - 'is_match' => $canSupport ? 1 : 0, - 'confidence' => $confidence, - 'reason' => $reason, - 'status' => self::RECORD_COMPLETED, - 'error_msg' => '', - ]); - - if ($confidence <= self::PASS_CONFIDENCE_THRESHOLD) { - $this->runSecondPassBlocking($checkId, $row, $contentA, $refer, $contentB); + if ($this->shouldRunSecondPassForLlmResults($groupRows, $llmResult)) { + $this->runSecondPassBlocking($checkId, $row, $contentA, $refer, $contentB, $groupRows); } - return [ - 'check_id' => $checkId, - 'can_support' => $canSupport ? 1 : 0, - 'is_match' => $canSupport ? 1 : 0, - 'confidence' => $confidence, - 'reason' => $reason, - ]; + $freshRow = Db::name('article_reference_check_result')->where('id', $checkId)->find(); + return $this->formatCheckReturnFromRow(!empty($freshRow) ? $freshRow : $row); } /** * 低分结果的二轮 DOI 复核(同步阻塞执行;失败重试一次) */ - public function runSecondPassBlocking($checkId, array $row, $contentA, $refer, $referText) + public function runSecondPassBlocking($checkId, array $row, $contentA, $refer, $referText, array $groupRows = null) { + if (!$this->isSecondPassEnabled()) { + return false; + } + + DbReconnectHelper::ensure(); $checkId = intval($checkId); if ($checkId <= 0) { return false; } - $payload = $this->prepareRecheckPayload(is_array($refer) ? $refer : [], trim((string)$referText)); + if ($groupRows === null) { + $groupRows = $this->findCitationGroupRows($row); + } + $isGroup = $this->isCitationGroupCheck($groupRows); + + if ($isGroup) { + $leaderRefNo = $this->resolveCitationGroupLeaderRefNo($groupRows); + $currentRefNo = intval($this->arrGet($row, 'reference_no', 0)); + if ($currentRefNo !== $leaderRefNo) { + $freshRow = Db::name('article_reference_check_result')->where('id', $checkId)->find(); + if (!empty($freshRow) && $this->hasSecondPassCompleted($freshRow)) { + return true; + } + return false; + } + } + + if (trim((string)$contentA) === '') { + $contentA = $this->resolveParagraphContextForJob($row); + } + $localContext = $this->resolveCitationLocalContextForJob($row); + + DbReconnectHelper::release(); + if ($isGroup) { + $payload = $this->prepareRecheckPayloadForCitationGroup($groupRows); + $referText = trim((string)$payload['refer_text']); + } else { + if (trim((string)$referText) === '') { + $referText = $this->resolveReferTextForCheckRow($row, is_array($refer) ? $refer : null); + } + $payload = $this->prepareRecheckPayload(is_array($refer) ? $refer : [], trim((string)$referText)); + } + DbReconnectHelper::ensure(); if (empty($payload['has_abstract']) || trim((string)$payload['doi_block']) === '') { return false; } + $citeGroupRefs = $this->resolveCiteGroupRefsFromRow($row, $groupRows); $lastError = ''; for ($attempt = 0; $attempt < 2; $attempt++) { try { - $llmResult = (new LLMService())->checkReference($contentA, trim((string)$referText), true, $payload['doi_block']); + DbReconnectHelper::release(); + $llmResult = (new LLMService())->checkReference( + $contentA, + trim((string)$referText), + true, + $payload['doi_block'], + $citeGroupRefs, + $localContext + ); + DbReconnectHelper::ensure(); $requestFailed = !empty($llmResult['request_failed']); - $canSupport = $this->parseLlmCanSupport($llmResult); - $confidence = floatval(isset($llmResult['confidence']) ? $llmResult['confidence'] : 0); - $tag = '[Crossref复核' . (trim((string)$payload['doi_used']) !== '' ? (' ' . trim((string)$payload['doi_used'])) : '') . ']'; - $reason = $tag . ' ' . (isset($llmResult['reason']) ? $llmResult['reason'] : ''); + $tag = $this->buildSecondPassReasonTag($row, $payload, $groupRows); + if ($tag !== '' && !empty($llmResult['results']) && is_array($llmResult['results'])) { + foreach ($llmResult['results'] as &$one) { + if (!is_array($one)) { + continue; + } + $one['reason'] = $tag . ' ' . (isset($one['reason']) ? (string)$one['reason'] : ''); + } + unset($one); + } - if ($requestFailed) { - $lastError = isset($llmResult['reason']) ? (string)$llmResult['reason'] : 'LLM request failed'; + if ($requestFailed || !$this->applyCitationGroupCheckResults($groupRows, $llmResult)) { + $lastError = isset($llmResult['reason']) ? (string)$llmResult['reason'] : 'LLM request failed or empty results'; if ($attempt < 1) { continue; } - $this->updateCheckResult($checkId, [ - 'confidence' => $confidence, - 'reason' => $reason, - 'status' => self::RECORD_FAILED, - 'error_msg' => $lastError, - ]); + $failFields = [ + 'status' => self::RECORD_FAILED, + 'error_msg' => $lastError, + ]; + if ($isGroup) { + $this->applyCheckResultToCitationGroup($groupRows, $failFields); + } else { + $this->updateCheckResult($checkId, $failFields); + } $amId = intval(isset($row['am_id']) ? $row['am_id'] : 0); if ($amId > 0) { $this->syncAmRefCheckStatus($amId); @@ -1958,14 +2930,6 @@ class ReferenceCheckService return false; } - $this->updateCheckResult($checkId, [ - 'can_support' => $canSupport ? 1 : 0, - 'is_match' => $canSupport ? 1 : 0, - 'confidence' => $confidence, - 'reason' => $reason, - 'status' => self::RECORD_COMPLETED, - 'error_msg' => '', - ]); $amId = intval(isset($row['am_id']) ? $row['am_id'] : 0); if ($amId > 0) { $this->syncAmRefCheckStatus($amId); @@ -1976,10 +2940,15 @@ class ReferenceCheckService if ($attempt < 1) { continue; } - $this->updateCheckResult($checkId, [ + $failFields = [ 'status' => self::RECORD_FAILED, 'error_msg' => $lastError, - ]); + ]; + if ($isGroup) { + $this->applyCheckResultToCitationGroup($groupRows, $failFields); + } else { + $this->updateCheckResult($checkId, $failFields); + } $amId = intval(isset($row['am_id']) ? $row['am_id'] : 0); if ($amId > 0) { $this->syncAmRefCheckStatus($amId); @@ -2087,6 +3056,18 @@ class ReferenceCheckService if (!is_array($llmResult)) { return false; } + if (!empty($llmResult['results']) && is_array($llmResult['results'])) { + foreach ($llmResult['results'] as $item) { + if (!is_array($item)) { + continue; + } + if (!empty($item['can_support']) || !empty($item['is_match'])) { + return true; + } + } + + return false; + } if (array_key_exists('can_support', $llmResult)) { return $this->parseLlmIsMatch($llmResult['can_support']); } @@ -2098,6 +3079,7 @@ class ReferenceCheckService */ public function resolveMainContentForJob(array $row, $maxChars = 8000) { + DbReconnectHelper::ensure(); $amId = intval($this->arrGet($row, 'am_id', 0)); if ($amId <= 0) { return ''; @@ -2387,10 +3369,14 @@ class ReferenceCheckService } /** - * 引用处局部上下文(origin_text),供其它场景使用 + * 引用处局部上下文:优先按 text_start/text_end 从节正文重算,回落 origin_text 快照 */ public function resolveCitationContextForJob(array $row) { + $local = $this->resolveCitationLocalContextForJob($row); + if ($local !== '') { + return $local; + } $text = trim((string)$this->arrGet($row, 'origin_text', '')); if ($text === '') { $text = trim((string)$this->arrGet($row, 'content_a', '')); @@ -2503,6 +3489,8 @@ class ReferenceCheckService */ public function fetchDoiLiteratureBlock($refer) { + DbReconnectHelper::release(); + $candidates = $this->extractAllDoiCandidatesFromRefer($refer); if (empty($candidates)) { return ''; @@ -2670,15 +3658,80 @@ class ReferenceCheckService public function prepareRecheckPayload($refer, $referText = '') { $base = trim($referText) !== '' ? trim($referText) : $this->formatReferForLlm($refer); - $cr = $this->fetchCrossrefAbstractByReferDoi($refer); + $bundle = (new ReferenceLiteratureFetchService())->fetchAndCleanForRefer(is_array($refer) ? $refer : []); + $block = $this->buildLiteratureBlockFromBundle(0, $bundle); + if ($block === '') { + $cr = $this->resolveDoiRecheckFromRefer(is_array($refer) ? $refer : []); + return [ + 'refer_text' => $base, + 'doi_block' => $cr['text'], + 'has_abstract' => $cr['has_abstract'], + 'doi_used' => $cr['doi'], + ]; + } return [ 'refer_text' => $base, - 'doi_block' => $cr['text'], - 'has_abstract' => $cr['has_abstract'], - 'doi_used' => $cr['doi'], + 'doi_block' => $block, + 'has_abstract' => trim((string)($bundle['abstract_final'] ?? '')) !== '', + 'doi_used' => trim((string)($bundle['doi'] ?? '')), ]; } + private function buildLiteratureBlockFromBundle($refNo, array $bundle) + { + $abstract = trim((string)($bundle['abstract_final'] ?? $bundle['abstract'] ?? '')); + $cleaned = trim((string)($bundle['content_cleaned'] ?? '')); + $raw = trim((string)($bundle['raw_content'] ?? '')); + if ($cleaned === '' && $raw !== '') { + $cleaned = mb_substr($raw, 0, 6000); + } + if ($abstract === '' && $cleaned === '') { + return ''; + } + + $head = $refNo > 0 ? ('【参考文献 ' . intval($refNo) . '】') : '【文献内容】'; + $doi = trim((string)($bundle['doi'] ?? '')); + if ($doi !== '') { + $head .= ' DOI: ' . $doi; + } + $parts = [$head]; + if ($abstract !== '') { + $parts[] = '【摘要】' . "\n" . $abstract; + } + if ($cleaned !== '') { + $parts[] = '【清洗后文献内容】' . "\n" . $cleaned; + } + $sources = isset($bundle['sources']) && is_array($bundle['sources']) ? implode(',', $bundle['sources']) : ''; + if ($sources !== '') { + $parts[] = 'Sources: ' . $sources; + } + return implode("\n\n", $parts); + } + + private function persistLiteratureOnCheckRow($checkId, array $bundle) + { + $checkId = intval($checkId); + if ($checkId <= 0) { + return; + } + $abstract = trim((string)($bundle['abstract_final'] ?? $bundle['abstract'] ?? '')); + $raw = trim((string)($bundle['raw_content'] ?? '')); + $cleaned = trim((string)($bundle['content_cleaned'] ?? '')); + if ($cleaned === '' && $raw !== '') { + $cleaned = mb_substr($raw, 0, 6000); + } + try { + DbReconnectHelper::ensure(); + Db::name('article_reference_check_result')->where('id', $checkId)->update([ + 'abstract_text' => $abstract, + 'refer_content_cleaned' => $cleaned, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } catch (\Throwable $e) { + \think\Log::warning('persistLiteratureOnCheckRow: ' . $e->getMessage()); + } + } + /** * 旧接口:拼接成单块文本(向后兼容,建议调用方改用 prepareRecheckPayload) */ @@ -2710,6 +3763,24 @@ class ReferenceCheckService return false; } + if ($this->hasSecondPassCompleted($row)) { + return true; + } + + $groupRows = $this->findCitationGroupRows($row); + $isGroup = $this->isCitationGroupCheck($groupRows); + if ($isGroup) { + $leaderRefNo = $this->resolveCitationGroupLeaderRefNo($groupRows); + $currentRefNo = intval($this->arrGet($row, 'reference_no', 0)); + if ($currentRefNo !== $leaderRefNo) { + $freshRow = Db::name('article_reference_check_result')->where('id', $checkId)->find(); + if (!empty($freshRow) && $this->hasSecondPassCompleted($freshRow)) { + return true; + } + return false; + } + } + $refer = null; if (intval($row['p_refer_id']) > 0) { $refer = Db::name('production_article_refer') @@ -2717,22 +3788,15 @@ class ReferenceCheckService ->where('state', 0) ->find(); } - if (empty($refer) || $this->extractReferDoiOnly($refer) === '') { - return false; + + $contentA = $this->resolveParagraphContextForJob($row); + if ($isGroup) { + $referText = $this->buildCombinedReferTextForCitationGroup($groupRows); + } else { + $referText = $this->resolveReferTextForCheckRow($row, $refer); } - $cr = $this->fetchCrossrefAbstractByReferDoi($refer); - if (empty($cr['has_abstract'])) { - return false; - } - - $contentA = $this->resolveMainContentForJob($row); - $referText = trim((string)$this->arrGet($row, 'refer_text', '')); - if ($referText === '' && is_array($refer)) { - $referText = $this->formatReferForLlm($refer); - } - - return $this->runSecondPassBlocking($checkId, $row, $contentA, $refer, $referText); + return $this->runSecondPassBlocking($checkId, $row, $contentA, $refer, $referText, $groupRows); } /** @@ -2787,6 +3851,27 @@ class ReferenceCheckService return $result; } + /** + * 按段落截取引用上下文:同一段落内各处引用共用段落文本,分别按 cite_group_refs 校对。 + */ + private function extractParagraphCitationContext($content, $tagStart, $tagEnd, array $tagSpans) + { + $paragraphStart = $this->findParagraphStart($content, $tagStart); + $paragraphEnd = $this->findParagraphEnd($content, $tagEnd); + $originalText = $this->buildCitationContextText($content, $paragraphStart, $paragraphEnd); + + if (!$this->isMeaningfulCitationContext($originalText)) { + return $this->extractLocalCitationContext( + $content, + $tagStart, + $tagEnd, + $tagSpans + ); + } + + return [$paragraphStart, $paragraphEnd, $originalText]; + } + /** * 按引用位置截取局部上下文:优先取标签前叙述;同句多引时后续引用从上一标签后开始。 */ @@ -2929,6 +4014,89 @@ class ReferenceCheckService return $text; } + /** + * 同段落内、当前引用标签之前最近一个引用标签的结束字节位置 + */ + private function resolvePriorCitationTagEnd($content, $tagStart) + { + $tagStart = intval($tagStart); + $paragraphStart = $this->findParagraphStart($content, $tagStart); + $prevTagEnd = $paragraphStart; + + $matches = $this->collectBlueTagMatches($content); + if (empty($matches[0])) { + return $paragraphStart; + } + + foreach ($matches[0] as $match) { + $end = intval($match[1]) + strlen($match[0]); + if ($end <= $tagStart && $end > $prevTagEnd) { + $prevTagEnd = $end; + } + } + + return $prevTagEnd; + } + + /** + * 给定当前句起点,返回上一句起点 + */ + private function findPreviousSentenceStart($content, $sentenceStart) + { + $sentenceStart = intval($sentenceStart); + if ($sentenceStart <= 0) { + return 0; + } + + $pos = $sentenceStart - 1; + while ($pos > 0 && isset($content[$pos]) && ctype_space($content[$pos])) { + $pos--; + } + if ($pos <= 0) { + return 0; + } + + $prev = $this->findSentenceStart($content, $pos); + if ($prev >= $sentenceStart) { + $pos--; + while ($pos > 0 && isset($content[$pos]) && ctype_space($content[$pos])) { + $pos--; + } + if ($pos <= 0) { + return 0; + } + $prev = $this->findSentenceStart($content, $pos); + } + + return max(0, $prev); + } + + /** + * 从当前句起点向前扩展若干完整句子,但不早于 $minStart + */ + private function extendContextStartBackward($content, $start, $minStart, $extraSentences = 2) + { + $start = intval($start); + $minStart = max(0, intval($minStart)); + $extraSentences = max(0, intval($extraSentences)); + if ($extraSentences === 0 || $start <= $minStart) { + return max($minStart, $start); + } + + for ($i = 0; $i < $extraSentences; $i++) { + if ($start <= $minStart) { + break; + } + $prev = $this->findPreviousSentenceStart($content, $start); + if ($prev >= $start) { + break; + } + $start = max($minStart, $prev); + } + + return $start; + } + /** * 过滤仅标点、过短或无字母/汉字的上下文(如去掉标签后只剩 ".") */ @@ -3049,6 +4217,36 @@ class ReferenceCheckService return $best; } + /** + * 段落结束(

、双换行、下一段

之前) + */ + private function findParagraphEnd($content, $tagEnd) + { + $length = strlen($content); + $pos = max(0, intval($tagEnd)); + if ($pos >= $length) { + return $length; + } + + $candidates = [$length]; + + if (preg_match('/<\/p>/i', $content, $m, PREG_OFFSET_CAPTURE, $pos)) { + $candidates[] = intval($m[0][1]) + strlen($m[0][0]); + } + if (preg_match('/]*>/i', $content, $m, PREG_OFFSET_CAPTURE, $pos + 1)) { + $candidates[] = intval($m[0][1]); + } + if (preg_match('/\n\n/', $content, $m, PREG_OFFSET_CAPTURE, $pos)) { + $candidates[] = intval($m[0][1]); + } + if (preg_match('/\s*/i', $content, $m, PREG_OFFSET_CAPTURE, $pos)) { + $candidates[] = intval($m[0][1]) + strlen($m[0][0]); + } + + $end = min($candidates); + return max($pos, $end); + } + /** * 段落过长时从引用处向前截取上限,避免单次 LLM 上下文过大 */ @@ -3151,41 +4349,7 @@ class ReferenceCheckService */ public function startArticleCheckQueue(array $checkIds, $pArticleId = 0, $trigger = 'enqueue') { - $checkIds = array_values(array_filter(array_map('intval', $checkIds))); - if (empty($checkIds)) { - return []; - } - - $pArticleId = intval($pArticleId); - if ($pArticleId <= 0) { - $firstRow = Db::name('article_reference_check_result')->where('id', $checkIds[0])->find(); - $pArticleId = empty($firstRow) ? 0 : intval($this->arrGet($firstRow, 'p_article_id', 0)); - } - if ($pArticleId <= 0) { - throw new \RuntimeException('p_article_id is required for reference check queue'); - } - - $now = date('Y-m-d H:i:s'); - $batchId = Db::name('article_reference_check_batch')->insertGetId([ - 'p_article_id' => $pArticleId, - 'batch_status' => 0, - 'total_count' => count($checkIds), - 'done_count' => 0, - 'failed_count' => 0, - 'trigger' => (string)$trigger, - 'created_at' => $now, - 'updated_at' => $now, - ]); - - $shouldPublish = !$this->hasEarlierWaitingBatch($batchId) && !$this->hasRunningReferenceCheckBatch(); - if ($shouldPublish) { - (new ReferenceCheckMqPublisher())->publishArticleStart($pArticleId, intval($batchId), $trigger); - $this->log('startArticleCheckQueue publish p_article_id=' . $pArticleId . ' batch_id=' . $batchId); - } else { - $this->log('startArticleCheckQueue queued batch_id=' . $batchId . ' p_article_id=' . $pArticleId); - } - - return $checkIds; + throw new \RuntimeException('Support strength check queue is deprecated. Use ReferenceRelevanceCheckService.'); } private function hasRunningReferenceCheckBatch() diff --git a/application/common/mq/ReferenceCheckArticleWorker.php b/application/common/mq/ReferenceCheckArticleWorker.php index e71da22d..5c2701db 100644 --- a/application/common/mq/ReferenceCheckArticleWorker.php +++ b/application/common/mq/ReferenceCheckArticleWorker.php @@ -3,10 +3,12 @@ namespace app\common\mq; use think\Db; -use app\common\ReferenceCheckService; +use app\common\DbReconnectHelper; +use app\common\ReferenceRelevanceCheckService; /** - * RabbitMQ 消费:按文章串行,文章内 reference_no 升序逐条校对(含低分同步二轮) + * RabbitMQ 消费(队列 reference_check / ref_check.article): + * 全局文章串行,文章内 reference_no 升序链式逐条「主题相关性」校对。 */ class ReferenceCheckArticleWorker { @@ -15,18 +17,20 @@ class ReferenceCheckArticleWorker const BATCH_DONE = 2; const BATCH_PARTIAL_FAILED = 3; - /** @var ReferenceCheckService */ + /** @var ReferenceRelevanceCheckService */ private $svc; public function __construct() { - $this->svc = new ReferenceCheckService(); + $this->svc = new ReferenceRelevanceCheckService(); } public function handleMessage(array $payload) { + DbReconnectHelper::ensure(); $pArticleId = intval(isset($payload['p_article_id']) ? $payload['p_article_id'] : 0); $batchId = intval(isset($payload['batch_id']) ? $payload['batch_id'] : 0); + $trigger = isset($payload['trigger']) ? (string)$payload['trigger'] : 'enqueue'; if ($pArticleId <= 0 || $batchId <= 0) { $this->svc->log('ReferenceCheckArticleWorker invalid payload'); return; @@ -34,7 +38,11 @@ class ReferenceCheckArticleWorker if (!$this->canStartArticleWork($batchId)) { $this->svc->log('ReferenceCheckArticleWorker defer batch_id=' . $batchId . ' other article running'); - (new ReferenceCheckMqPublisher())->publishArticleStart($pArticleId, $batchId, isset($payload['trigger']) ? $payload['trigger'] : 'enqueue'); + (new ReferenceCheckMqPublisher())->publishArticleStart( + $pArticleId, + $batchId, + isset($payload['trigger']) ? $payload['trigger'] : 'enqueue' + ); sleep(3); return; } @@ -46,6 +54,11 @@ class ReferenceCheckArticleWorker } } + $this->svc->recoverQueueRowsForArticle($pArticleId); + if ($trigger !== 'recheck_pending_only' + && ReferenceRelevanceCheckService::PREPARE_LITERATURE_BEFORE_CHECK) { + $this->svc->prepareLiteratureContentByArticle($pArticleId); + } $this->svc->log('ReferenceCheckArticleWorker start p_article_id=' . $pArticleId . ' batch_id=' . $batchId); $done = 0; @@ -59,7 +72,7 @@ class ReferenceCheckArticleWorker if ($checkId <= 0) { continue; } - $result = $this->processOneRow($checkId, $row); + $result = $this->processOneRow($checkId, $row, $trigger === 'recheck_pending_only'); if ($result === 'ok') { $done++; } elseif ($result === 'failed') { @@ -75,7 +88,7 @@ class ReferenceCheckArticleWorker private function canStartArticleWork($batchId) { - $running = Db::name('article_reference_check_batch') + $running = Db::name('article_reference_relevance_check_batch') ->where('batch_status', self::BATCH_RUNNING) ->where('id', '<>', intval($batchId)) ->count(); @@ -85,7 +98,7 @@ class ReferenceCheckArticleWorker private function claimBatch($batchId) { $now = date('Y-m-d H:i:s'); - $affected = Db::name('article_reference_check_batch') + $affected = Db::name('article_reference_relevance_check_batch') ->where('id', intval($batchId)) ->whereIn('batch_status', [self::BATCH_WAITING, self::BATCH_RUNNING]) ->update([ @@ -97,15 +110,15 @@ class ReferenceCheckArticleWorker private function getBatch($batchId) { - return Db::name('article_reference_check_batch')->where('id', intval($batchId))->find(); + return Db::name('article_reference_relevance_check_batch')->where('id', intval($batchId))->find(); } private function fetchNextPendingRow($pArticleId) { - return Db::name('article_reference_check_result') + return Db::name('article_reference_relevance_check_result') ->where('p_article_id', intval($pArticleId)) - ->where('queue_status', ReferenceCheckService::QUEUE_PENDING) - ->where('status', ReferenceCheckService::RECORD_PENDING) + ->where('queue_status', ReferenceRelevanceCheckService::QUEUE_PENDING) + ->where('status', ReferenceRelevanceCheckService::RECORD_PENDING) ->order('reference_no asc,am_id asc,text_start asc,id asc') ->find(); } @@ -113,44 +126,44 @@ class ReferenceCheckArticleWorker /** * @return string ok|failed|skip */ - private function processOneRow($checkId, array $row) + private function processOneRow($checkId, array $row, $skipLiteratureFetch = false) { - $claimed = Db::name('article_reference_check_result') + DbReconnectHelper::ensure(); + $claimed = Db::name('article_reference_relevance_check_result') ->where('id', intval($checkId)) - ->where('queue_status', ReferenceCheckService::QUEUE_PENDING) - ->update(['queue_status' => ReferenceCheckService::QUEUE_RUNNING]); + ->where('queue_status', ReferenceRelevanceCheckService::QUEUE_PENDING) + ->update(['queue_status' => ReferenceRelevanceCheckService::QUEUE_RUNNING]); if (intval($claimed) <= 0) { return 'skip'; } $retryCount = intval(isset($row['retry_count']) ? $row['retry_count'] : 0); try { - $this->svc->runReferenceCheckOnce($checkId); - $amId = intval(isset($row['am_id']) ? $row['am_id'] : 0); - if ($amId > 0) { - $this->svc->syncAmRefCheckStatus($amId); - } - $this->svc->markQueueRuntime($checkId, ReferenceCheckService::QUEUE_COMPLETED, $retryCount); + $this->svc->runCheckOnce($checkId, $skipLiteratureFetch); + $this->svc->markQueueRuntime($checkId, ReferenceRelevanceCheckService::QUEUE_COMPLETED, $retryCount); return 'ok'; } catch (\Exception $e) { $this->svc->log('ReferenceCheckArticleWorker check_id=' . $checkId . ' err=' . $e->getMessage()); - if ($retryCount < ReferenceCheckService::QUEUE_MAX_RETRY) { - $this->svc->markQueueRuntime($checkId, ReferenceCheckService::QUEUE_PENDING, $retryCount + 1); - return $this->processOneRow($checkId, array_merge($row, ['retry_count' => $retryCount + 1])); + DbReconnectHelper::ensure(); + if ($retryCount < ReferenceRelevanceCheckService::QUEUE_MAX_RETRY) { + $this->svc->markQueueRuntime($checkId, ReferenceRelevanceCheckService::QUEUE_PENDING, $retryCount + 1); + return $this->processOneRow($checkId, array_merge($row, ['retry_count' => $retryCount + 1]), $skipLiteratureFetch); } try { - $this->svc->updateCheckResult($checkId, [ - 'status' => ReferenceCheckService::RECORD_FAILED, - 'error_msg' => $e->getMessage(), - ]); - $this->svc->markQueueRuntime($checkId, ReferenceCheckService::QUEUE_FAILED, $retryCount); + $fresh = Db::name('article_reference_relevance_check_result')->where('id', intval($checkId))->find(); + $groupRows = !empty($fresh) ? $this->svc->findCitationGroupRowsForWorker($fresh) : []; + if (!empty($groupRows)) { + $this->svc->failGroupWithQueue($groupRows, $e->getMessage(), $retryCount); + } else { + $this->svc->updateCheckResult($checkId, [ + 'status' => ReferenceRelevanceCheckService::RECORD_FAILED, + 'error_msg' => $e->getMessage(), + ]); + $this->svc->markQueueRuntime($checkId, ReferenceRelevanceCheckService::QUEUE_FAILED, $retryCount); + } } catch (\Exception $e2) { \think\Log::error('ReferenceCheckArticleWorker markFailed: ' . $e2->getMessage()); } - $amId = intval(isset($row['am_id']) ? $row['am_id'] : 0); - if ($amId > 0) { - $this->svc->syncAmRefCheckStatus($amId); - } return 'failed'; } } @@ -166,7 +179,7 @@ class ReferenceCheckArticleWorker if ($failed > 0) { $status = self::BATCH_PARTIAL_FAILED; } - Db::name('article_reference_check_batch')->where('id', intval($batchId))->update([ + Db::name('article_reference_relevance_check_batch')->where('id', intval($batchId))->update([ 'batch_status' => $status, 'done_count' => intval($done), 'failed_count' => intval($failed), @@ -179,7 +192,7 @@ class ReferenceCheckArticleWorker private function publishNextWaitingBatch() { - $next = Db::name('article_reference_check_batch') + $next = Db::name('article_reference_relevance_check_batch') ->where('batch_status', self::BATCH_WAITING) ->order('id asc') ->find(); @@ -193,8 +206,8 @@ class ReferenceCheckArticleWorker isset($next['trigger']) ? $next['trigger'] : 'enqueue' ); } catch (\Exception $e) { - $this->svc->log('publishNextWaitingBatch failed: ' . $e->getMessage()); - \think\Log::error('publishNextWaitingBatch: ' . $e->getMessage()); + $this->svc->log('ReferenceCheck publishNextWaitingBatch failed: ' . $e->getMessage()); + \think\Log::error('ReferenceCheck publishNextWaitingBatch: ' . $e->getMessage()); } } } diff --git a/application/common/service/LLMService.php b/application/common/service/LLMService.php index 20e25fc1..3daca96a 100644 --- a/application/common/service/LLMService.php +++ b/application/common/service/LLMService.php @@ -28,18 +28,17 @@ class LLMService * @param string $contextText 正文引用处句子 * @param string $referText 参考文献条目(或 refer 格式化文本) * @param bool $isAgain 是否为 DOI 二次复核 - * @param string|null $doiBlock 可选:系统抓取到的 DOI 真实文献内容(仅二次复核使用) + * @param string|null $doiBlock 可选:系统抓取到的 DOI 真实文献内容(仅二次复核使用) + * @param string $citeGroupRefs 引用文献组,如 1,2 或 4,5,6 + * @param string $localContext 本引用位置附近上下文(可选) + * @return array{results:array,request_failed?:bool} */ - public function checkReference($contextText, $referText, $isAgain = false, $doiBlock = null) + public function checkReference($contextText, $referText, $isAgain = false, $doiBlock = null, $citeGroupRefs = '', $localContext = '') { - // request_failed=true 表示"LLM 通讯/解析层面的失败"(可重试,区别于业务上的"未命中"); - // 上游 runReferenceCheckOnce 会据此把 DB.status 置为 3(失败) 并抛异常触发 MQ worker 重试 $fallback = [ - 'can_support' => false, - 'is_match' => false, - 'confidence' => 0.0, - 'reason' => 'LLM not configured or request failed', + 'results' => [], 'request_failed' => true, + 'reason' => 'LLM not configured or request failed', ]; if ($this->url === '' || $this->model === '') { \think\Log::warning('ReferenceCheck LLM: url or model not configured'); @@ -47,15 +46,16 @@ class LLMService } $contextText = trim($contextText); + \think\Log::info('llm checkReference:' . $contextText); $referText = trim($referText); + \think\Log::info('llm referText:' . $referText); $doiBlock = trim((string)$doiBlock); + $citeGroupRefs = trim((string)$citeGroupRefs); + $localContext = trim((string)$localContext); if ($contextText === '' || $referText === '') { - // 空文本是入参问题,不是 LLM 故障,不需要重试 return [ - 'can_support' => false, - 'is_match' => false, - 'confidence' => 0.0, - 'reason' => 'Empty citation context or reference text', + 'results' => [], + 'reason' => 'Empty citation context or reference text', ]; } @@ -63,27 +63,30 @@ class LLMService if (mb_strlen($contextText) > $maxContextLen) { $contextText = mb_substr($contextText, 0, $maxContextLen); } - if (mb_strlen($referText) > 4000) { - $referText = mb_substr($referText, 0, 4000); + if (mb_strlen($localContext) > 3000) { + $localContext = mb_substr($localContext, 0, 3000); } - if (mb_strlen($doiBlock) > 4000) { - $doiBlock = mb_substr($doiBlock, 0, 4000); + if (mb_strlen($referText) > 6000) { + $referText = mb_substr($referText, 0, 6000); + } + if (mb_strlen($doiBlock) > 8000) { + $doiBlock = mb_substr($doiBlock, 0, 8000); } if ($isAgain) { $system = $this->buildReferenceCheckSecondPassPrompt(); - $user = $this->buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock); + $user = $this->buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock, $citeGroupRefs, $localContext); } else { $system = $this->buildReferenceCheckFirstPassPrompt(); - $user = $this->buildReferenceCheckFirstPassUserPrompt($contextText, $referText); + $user = $this->buildReferenceCheckFirstPassUserPrompt($contextText, $referText, $citeGroupRefs, $localContext, $doiBlock); } - \think\Log::info('ReferenceCheck system head: ' . mb_substr($system, 0, 200)); - \think\Log::info('ReferenceCheck user head: ' . mb_substr($user, 0, 600)); +// \think\Log::info('ReferenceCheck system head: ' . mb_substr($system, 0, 200)); +// \think\Log::info('ReferenceCheck user head: ' . mb_substr($user, 0, 600)); $payload = [ - 'model' => $this->model, + 'model' => $this->model, 'temperature' => 0, - 'messages' => [ + 'messages' => [ ['role' => 'system', 'content' => $system], ['role' => 'user', 'content' => $user], ], @@ -101,23 +104,14 @@ class LLMService return $fallback; } - $canSupport = $this->parseCanSupportFromParsed($parsed); - $confidence = $this->snapReferenceCheckConfidence( - $this->normalizeConfidence(isset($parsed['confidence']) ? $parsed['confidence'] : 0), - $canSupport - ); - $reason = $this->cleanReason((string)(isset($parsed['reason']) ? $parsed['reason'] : '')); - \think\Log::info( - 'ReferenceCheck result: can_support=' . ($canSupport ? '1' : '0') - . ', confidence=' . $confidence - . ', reason=' . $reason - ); - return [ - 'can_support' => $canSupport, - 'is_match' => $canSupport, - 'confidence' => $confidence, - 'reason' => $reason, - ]; + $results = $this->parseReferenceCheckResultsFromParsed($parsed, $citeGroupRefs, $localContext, $doiBlock); + if (empty($results)) { + \think\Log::warning('ReferenceCheck LLM: empty results array'); + return $fallback; + } + + \think\Log::info($results); + return ['results' => $results]; } /** @@ -174,83 +168,541 @@ class LLMService $s = strtolower(trim((string)$value)); return in_array($s, ['1', 'true', 'yes', 'support', 'supported'], true); } + private function bulidReferenceCheckFirstPassPrompt(){ + return <<<'PROMPT' +你是一名护理、医学与科研期刊的资深文献编辑,专门校对「正文引用句」与「对应参考文献条目」是否匹配。 - /** 第一次校对:书目条目 vs 正文全文 */ +你的目标是严格识别错引、张冠李戴、方法不符、对象不符、结论不成立的问题。 + +宁可少判 true,也不要漏掉错引。 + +你只能依据用户提供的内容判断: +1. 正文引用句 +2. 当前对应参考文献条目 + +禁止假设已阅读全文。 +禁止联网。 +禁止脑补文献内容。 +禁止根据学科常识推断研究结果。 + +==================== +【核心任务】 + +判断: + +正文在该引用位置表达的核心观点、结论、方法、数据、定义、模型、研究发现、指南依据等, + +是否能够被该条参考文献合理支撑。 + +你判断的是: + +“引用是否成立” + +不是: + +“正文是否正确”。 + +==================== +【总原则(最高优先级)】 + +采用严格审稿标准: + +边界不清时,一律判 false。 + +宁可误杀(人工复核),不要漏掉错引。 + +同领域 ≠ 匹配。 + +同关键词 ≠ 匹配。 + +相关 ≠ 能支撑。 + +==================== +【强制规则】 + +1. 严禁关键词硬匹配 + +不能因为出现: +患者、护理、治疗、研究、模型、算法、深度学习、机器学习、焦虑、效果 + +等泛化词汇就判定匹配。 + +必须看: + +- 核心对象 +- 研究问题 +- 方法 +- 场景 +- 结局指标 +- 核心论点 + +是否一致。 + +==================== +2. 方法学必须严格一致(极重要) + +若正文明确提到: + +- 算法 +- 模型 +- 聚类方法 +- 深度学习架构 +- 统计方法 +- 数学模型 +- 评价指标 + +必须要求文献与其存在明确关联。 + +例如: + +不匹配: +- fuzzy clustering ≠ deep learning +- CNN ≠ LSTM +- random forest ≠ SVM +- 聚类 ≠ 分类 +- 特征选择 ≠ 分类预测 +- 风险因素分析 ≠ 干预研究 + +仅属于同一“大领域(AI/ML)” +不能判定匹配。 + +若方法体系不同: + +优先判 false + 0.10。 + +==================== +3. 医学护理引用严格一致 + +若正文涉及: + +- 疾病 +- 人群 +- 护理场景 +- 干预措施 +- 结局指标 + +必须基本一致。 + +例如: + +不匹配: +- ICU ≠ 普通病房 +- 老年人 ≠ 儿童 +- 糖尿病 ≠ 高血压 +- 心理护理 ≠ 运动干预 +- 焦虑改善 ≠ 生存率提高 + +==================== +4. 强结论必须强证据 + +正文若出现: + +- 显著改善 +- 明显降低 +- 证实 +- 优于 +- 有效预测 +- 危险因素 +- 因果关系 + +文献必须能合理支撑该强结论。 + +仅“应用研究”“相关研究”“观察研究” +不能自动支持强结论。 + +否则 false。 + +==================== +5. 特定证据类型必须一致 + +正文若明确写: + +- RCT/randomized trial +- Meta-analysis +- Guideline +- Systematic review +- Expert consensus + +而参考文献类型明显不符: + +直接 false。 + +==================== +6. 信息不足从严 + +若参考文献只有: + +作者 + 年份 + +或信息过少, + +无法建立明确关联: + +false + 0.30 + +==================== +【判定逻辑】 + +只有同时满足以下条件,才能 true: + +1. 主题一致 +2. 核心对象一致 +3. 核心论点一致 +4. 方法/研究方向一致 +5. 无明显错引风险 + +任意一点明显不符: + +false。 + +==================== +【评分(只能四选一)】 + +只能输出: + +0.90 +0.75 +0.30 +0.10 + +禁止任何其他分数。 + +评分规则: + +0.90 +明确匹配: +主题、对象、方法、核心论点均明显一致。 + +0.75 +基本匹配: +整体支撑成立,但存在轻微概括或小范围表述差异。 + +0.30 +存疑: +同领域但支撑不足; +信息不足; +需人工复核。 + +0.10 +明确错引: +主题、对象、方法或核心论点明显不符。 + +硬规则: + +is_match=true +只能: +0.75 或 0.90 + +is_match=false +只能: +0.10 或 0.30 + +==================== +【reason 要求】 + +仅说明: + +1. 是否主题一致; +2. 核心论点/方法是否能支撑。 + +禁止模糊措辞: +“可能” +“看起来” +“应该” +“疑似” + +长度: + +20~60字。 + +==================== +【输出要求】 + +仅输出一行 minified JSON。 + +禁止 markdown。 +禁止解释。 +禁止换行。 +禁止任何额外内容。 + +格式: + +{"is_match":true|false,"confidence":0.10|0.30|0.75|0.90,"reason":"简体中文说明"} +PROMPT; + + } + /** 第一次校对:参考文献真实性与支撑力度 */ private function buildReferenceCheckFirstPassPrompt() { - return <<<'PROMPT' -你是文献引用校对助手。判断【正文全文】与【参考文献书目】是否相关、能否用于支撑正文中的引用。 - -【核心原则:从宽判断,避免误杀】 -默认倾向 can_support=true。只要文献与正文不是「风马牛不相及」,即判为相关、能支撑。 -不要求变量一致、不要求结论逐条对应、不要求研究设计相同。 - -【仅当以下情况才判 can_support=false(与正文明显无关)】 -- 学科/主题完全无关(如正文讲深度学习聚类,文献是糖尿病步态检测)。 -- 明显张冠李戴(正文断言 A 疗法的效果,文献研究的是完全不同的 B 问题且无关联)。 -- 文献条目与正文讨论的对象/场景毫无交集,且无法作背景或理论引用。 - -【以下情况均应 can_support=true】 -- 同一大领域或相邻方向(如护理、心理、管理、医学、统计、AI 等相近子领域)。 -- 可作背景文献、综述性引用、理论或方法的一般性依据。 -- 表述略宽、略有概括、变量名不完全一致,但大方向说得通。 - -【confidence 固定档位(禁止其它小数)】 -can_support=true:0.65(有关联但较泛)/ 0.78 / 0.85 / 0.92 / 0.98(非常确定相关) -can_support=false:0.15(明确风马牛不相及)/ 0.25 / 0.35 / 0.45(仅当实在无法建立任何合理关联) - -【输出】仅一行 minified JSON,无 markdown: -{"can_support":true|false,"is_match":true|false,"confidence":0.15|0.25|0.35|0.45|0.65|0.78|0.85|0.92|0.98,"reason":"30-80字简体中文"} -is_match 必须与 can_support 相同。 -PROMPT; + return $this->buildReferenceCheckSupportSystemPrompt(false); } - private function buildReferenceCheckFirstPassUserPrompt($contextText, $referText) + private function buildReferenceCheckSupportSystemPrompt($isSecondPass = false) { - return "【正文全文 article_main.content】\n" . $contextText - . "\n\n【参考文献书目 refer_text】\n" . $referText - . "\n\n请从宽判断:文献与正文非风马牛不相即可判 can_support=true,只返回 JSON。"; + $prompt = <<<'PROMPT' +你是一名护理、医学、生物医学与科研期刊的资深学术编辑,正在执行“参考文献真实性与支撑力度校对”。 + +你的任务不是判断“主题是否相关”,而是判断: +【稿件正文中某段被引用内容】是否真的能被【对应编号的参考文献】直接或充分支撑。 + +你必须严格基于用户提供的材料作出判断,不得凭常识、不得脑补、不得假设参考文献中“可能写过但未提供”的内容。 + +================================================== +【一、任务目标】 +你需要判断: +“正文引用位置的核心论点、结论、背景陈述、机制解释、疗效描述、数据表达或因果表述, +是否能被对应参考文献真实支持。” + +这里的“支持”不是指“文献主题相关”或“研究领域接近”,而是指: +参考文献中确实包含足以支持正文该处表述的内容。 + +================================================== +【二、输出原则:结果必须直接对应数据库行】 + +你输出的结果将直接写入数据库表 t_article_reference_check_result。 + +因此: +## 输出必须是 results 数组,数组中的每一个对象对应数据库中的一行,也就是“一个引用位置中的一条参考文献结果”。 + +换句话说: +- 如果某个引用位置是 [3],则输出 1 条 result(reference_no=3) +- 如果某个引用位置是 [1,2],则输出 2 条 result: + - 一条对应 reference_no=1 + - 一条对应 reference_no=2 + +每条 result 都必须给出该参考文献“单独”对正文引用句的支撑判断。 +如果该引用位置是联合引用(citation group 中有多篇文献),则除了单条判断外,还必须给出该引用组整体的联合判断(combined_* 字段)。 + +================================================== +【三、最重要原则:只看“是否支撑正文核心断言”,不是看“主题是否沾边”】 + +以下情况不能判为强支撑: +1. 参考文献只和主题大致相关,但没有明确支持正文中的关键表述 +2. 正文说的是“疗效提升/死亡率下降/全球高发/耐药/多通路机制”等明确论点,而文献只是在背景里泛泛提到疾病 +3. 正文是多层复合句,文献只支撑其中一小部分 +4. 正文有因果、比较、趋势、机制、疗效强度等强表述,而文献没有明确证据 +5. 文献是基础机制研究,但正文引用它来支撑宏观流行病学、临床治疗现状或指南式结论 +6. 文献可以“推测支持”但不是“直接/明确支持” + +================================================== +【三b、多 claim 复合句 → 0.78 部分支撑(勿误降到 0.45)】 + +正文常为 2~4 个连续 claim 的复合句。须逐 claim 比对后综合给分: + +- 若文献(含 DOI 摘要)能**明确支撑多数关键概念**(如遗传异质性/多基因改变、多 survival pathway 并存、耐药或治疗挑战), + 但**未逐字写出**正文完整因果链(如「异质性→多通路→单靶点疗效下降」), + → 应判 **partial_support**,confidence 通常 **0.78**(边界情况 0.65),**不得**仅因文献主标题聚焦某化合物/干预就降到 0.45。 + +- 0.45 仅用于:文献与 claim 方向明显不符、仅同病沾边、或几乎无可用证据。 + +**校准样例(单条 [4],须接近此逻辑):** + +引用句: +Furthermore, the genomic heterogeneity of colorectal cancer (CRC) presents additional difficulties because tumors frequently make use of several survival pathways at once, which reduces the efficacy of single-target treatments [4]. + +文献4(Sheikhnia et al., thymoquinone CRC 机制综述): +- Claim1 遗传异质性/多基因改变:文献有 APC/KRAS/TP53、MSI/CIN 等 → 支撑较强 +- Claim2 多 survival pathway:文献列举 PI3K/Akt、Wnt、STAT3、NF-κB 等多通路 → 支撑较强 +- Claim3 单靶点疗效下降:文献有 drug resistance/治疗挑战,但未直述因果链 → 部分支撑 +- **输出**:can_support=1, confidence=**0.78**, support_role=supplementary_support(**不是 0.45**) + +用户消息中若提供【DOI 真实文献内容】,**必须结合摘要判断**,不得仅凭书目标题给分。 + +================================================== +【四、评分规则】 + +你必须使用以下 8 个固定分值之一: +0.98 / 0.92 / 0.85 / 0.78 / 0.65 / 0.45 / 0.25 / 0.15 + +判定含义: +- 0.98 / 0.92 / 0.85 => 强支撑(strong_support) +- 0.78 / 0.65 => 部分支撑(partial_support) +- 0.45 / 0.25 => 支撑不足(insufficient_support) +- 0.15 => 不支撑(not_support) + +can_support 取值规则: +- 若该文献/联合引文整体可判为 strong_support 或 partial_support,则 can_support = 1 +- 若判为 insufficient_support 或 not_support,则 can_support = 0 + +================================================== +【五、单条文献结果如何判断】 + +对于每一条参考文献,你必须判断它“单独”能否支撑该引用位置的正文内容,并输出: +- can_support +- confidence +- reason +- support_role + +其中: +### support_role 只能取以下值之一 +- primary_support:该文献本身就是主要证据来源,能支撑引用句核心内容 +- supplementary_support:能支撑部分重要内容,但不是主要来源 +- minimal_support:只提供少量背景或边缘支撑 +- no_meaningful_support:几乎不能支撑该引用句 + +### reason 的写法要求 +必须使用中文,明确写出: +1. 这篇文献具体支撑正文的哪一部分 +2. 哪些部分没有支撑到 +3. 是否存在文献类型与引用用途不匹配的问题 +4. 为什么给这个分值,而不是更高或更低 + +================================================== +【六、联合引用的判断规则】 + +当同一个引用位置包含多篇参考文献时(例如 [1,2] / [4,5,6]),除了逐条给单条结果外,还要额外判断: +“这些文献合起来,是否足以支撑该引用位置的正文内容?” + +联合结论输出到: +- combined_can_support +- combined_confidence +- combined_reason + +规则: +1. 联合评分不是单条评分平均值 +2. 如果其中一篇文献已强支撑,其他文献只是补充,则联合评分可接近主支撑文献 +3. 如果多篇文献分别覆盖不同部分,合起来能较完整支撑正文,则联合评分可以高于某些单条评分 +4. 但如果最关键的核心断言没有被任何文献明确支撑,则联合评分不能虚高 +5. 如果多篇文献都只是零散相关,需要大量推断才能拼出正文结论,则联合评分通常不应过高 + +================================================== +【七、单引文的 combined_* 字段处理规则】 + +即使某个引用位置只有 1 条参考文献,也仍然必须输出 combined_* 字段。 +此时: +- combined_can_support = can_support +- combined_confidence = confidence +- combined_reason = “该引用位置仅包含单条文献,联合结论等同于该文献的单条结论。” 或等价表述 + +这样可以保证输出结构统一,便于数据库写入。 + +================================================== +【八、输出 JSON 结构】 + +你必须输出合法 JSON,且只能输出以下结构: + +{ + "results": [ + { + "reference_no": 1, + "cite_group_refs": "1,2", + "can_support": 0, + "confidence": 0.65, + "reason": "中文,单条文献结论", + "support_role": "supplementary_support", + "combined_can_support": 1, + "combined_confidence": 0.85, + "combined_reason": "中文,联合引用整体结论" + } + ] +} + +================================================== +【九、字段约束】 + +### 1)results 中每个对象都必须包含以下字段: +- reference_no +- cite_group_refs +- can_support +- confidence +- reason +- support_role +- combined_can_support +- combined_confidence +- combined_reason + +### 2)reference_no +必须对应当前引用位置中的某一条参考文献编号。 + +### 3)cite_group_refs +必须是该引用位置的完整引文组,格式如: +- "3" +- "1,2" +- "4,5,6" + +### 4)同一引用位置若包含多条参考文献,则必须输出多条 result +例如 cite_group_refs = "1,2" 时,必须输出: +- 一条 reference_no=1 +- 一条 reference_no=2 + +### 5)同一引用位置下的 combined_* 必须一致 +例如同属 "1,2" 的两条 result,它们的: +- combined_can_support +- combined_confidence +- combined_reason +必须完全一致。 + +================================================== +【十、禁止事项】 +你绝对不能: +- 杜撰文献中不存在的结论 +- 把“主题相关”当作“内容支撑” +- 因为是同一疾病就默认支持 +- 输出 JSON 以外的任何内容 + +现在开始,读取用户提供的引用位置正文、参考文献信息和文献内容,输出结果。 +PROMPT; + + if ($isSecondPass) { + $prompt .= <<<'PROMPT' + + +================================================== +【二次校对补充(DOI 真实文献内容)】 +用户消息中会提供【DOI 真实文献内容(PubMed/Crossref)】。 +必须以 DOI 真实内容为准复核支撑力度;书目信息与 DOI 冲突时以 DOI 为准。 +仍须输出完整 results 数组,逐条给出单文献判断与联合判断。 +PROMPT; + } + + return $prompt; } - /** 第二次校对:Crossref 摘要(Refer_doi) */ + private function buildReferenceCheckFirstPassUserPrompt($contextText, $referText, $citeGroupRefs = '', $localContext = '', $doiBlock = '') + { + return $this->buildReferenceCheckSupportUserPrompt($contextText, $referText, $citeGroupRefs, $localContext, $doiBlock); + } + + private function buildReferenceCheckSupportUserPrompt($contextText, $referText, $citeGroupRefs, $localContext, $doiBlock) + { + $citeGroupRefs = trim((string)$citeGroupRefs); + $localContext = trim((string)$localContext); + $doiBlock = trim((string)$doiBlock); + + $parts = [ + "【正文节 t_article_main】\n" . $contextText, + ]; + if ($citeGroupRefs !== '') { + $mode = strpos($citeGroupRefs, ',') !== false ? '联合引用' : '单独引用'; + $parts[] = "【引用文献组 cite_group_refs】{$citeGroupRefs}({$mode})"; + } + if ($localContext !== '') { + $parts[] = "【本引用位置附近上下文】\n" . $localContext; + } + $parts[] = "【参考文献书目(按编号列出)】\n" . $referText; + if ($doiBlock !== '') { + $parts[] = "【DOI 真实文献内容(PubMed/Crossref,一轮校对已提供)】\n" . $doiBlock; + } + $parts[] = '请严格按 system 要求输出 results 数组 JSON,每条 result 对应一个 reference_no,并包含 combined_* 字段。'; + + return implode("\n\n", $parts); + } + + /** 第二次校对:DOI 真实文献内容复核 */ private function buildReferenceCheckSecondPassPrompt() { - return <<<'PROMPT' -你是文献引用二次校对助手。已根据 Refer_doi 从 Crossref(https://api.crossref.org/works/)获取摘要,请结合【正文全文】复核该文献是否相关。 - -【核心原则:与第一次相同,从宽判断】 -默认倾向 can_support=true。只要 Crossref 摘要(或书目)与正文不是风马牛不相及,即判相关、能支撑。 -以【Crossref 摘要】为准;摘要与书目冲突时以摘要为准。 - -【仅当以下情况才判 can_support=false】 -- 摘要显示的研究主题/对象/方法与正文讨论内容完全风马牛不相及。 -- 典型风马牛不相及、张冠李戴,且无法解释为背景或泛化引用。 - -【以下情况均应 can_support=true】 -- 摘要与正文属同领域或相近方向,能作背景、理论或方向性支撑。 -- 细节不完全一致,但不存在明显矛盾。 - -【无 Crossref 摘要时】 -结合 refer_text 从宽判断;非明显无关仍可 can_support=true,confidence 建议 0.65。 - -【confidence 固定档位(禁止其它小数)】 -can_support=true:0.65 / 0.78 / 0.85 / 0.92 / 0.98 -can_support=false:0.15 / 0.25 / 0.35 / 0.45 - -【输出】仅一行 minified JSON: -{"can_support":true|false,"is_match":true|false,"confidence":0.15|0.25|0.35|0.45|0.65|0.78|0.85|0.92|0.98,"reason":"30-80字简体中文"} -is_match 必须与 can_support 相同。 -PROMPT; + return $this->buildReferenceCheckSupportSystemPrompt(true); } - private function buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock) + private function buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock, $citeGroupRefs = '', $localContext = '') { - $doiBlock = trim((string)$doiBlock); - return "【正文全文 article_main.content】\n" . $contextText - . "\n\n【参考文献书目 refer_text】\n" . $referText - . "\n\n【Crossref 摘要】(Refer_doi → api.crossref.org/works/)\n" - . ($doiBlock !== '' ? $doiBlock : '(未获取到摘要,请结合 refer_text 从宽判断)') - . "\n\n文献与正文非风马牛不相即可判 can_support=true,只返回 JSON。"; + return $this->buildReferenceCheckSupportUserPrompt( + $contextText, + $referText, + $citeGroupRefs, + $localContext, + $doiBlock !== '' ? $doiBlock : '(未获取到 DOI 摘要或元数据,请结合书目条目从严判断)' + ); } private function buildReferenceCheckSystemPrompt3() { @@ -1169,13 +1621,174 @@ PROMPT; private function buildReferenceCheckRecheckUserPrompt($contextText, $referText, $doiBlock) { - return $this->buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock); + return $this->buildReferenceCheckSecondPassUserPrompt($contextText, $referText, $doiBlock, '', ''); } /** - * 与 buildReferenceCheckSystemPrompt3 一致的 confidence 档位 + * @return array */ - private function getReferenceCheckConfidenceBands($isMatch) + private function parseReferenceCheckResultsFromParsed(array $parsed, $defaultCiteGroupRefs = '', $localContext = '', $doiBlock = '') + { + $rows = []; + if (isset($parsed['results']) && is_array($parsed['results'])) { + $rows = $parsed['results']; + } elseif (isset($parsed['reference_no']) || isset($parsed['confidence'])) { + $rows = [$parsed]; + } + + $normalized = []; + foreach ($rows as $item) { + if (!is_array($item)) { + continue; + } + $refNo = intval(isset($item['reference_no']) ? $item['reference_no'] : 0); + if ($refNo <= 0) { + continue; + } + + $confidence = $this->snapReferenceCheckConfidenceValue( + $this->normalizeConfidence(isset($item['confidence']) ? $item['confidence'] : 0) + ); + $canSupport = $this->canSupportFromConfidence($confidence); + if (array_key_exists('can_support', $item)) { + $canSupport = $this->boolFromLlmValue($item['can_support']); + } elseif (array_key_exists('is_match', $item)) { + $canSupport = $this->boolFromLlmValue($item['is_match']); + } + + $reason = $this->cleanReason((string)(isset($item['reason']) ? $item['reason'] : '')); + $supportRole = $this->normalizeSupportRole(isset($item['support_role']) ? $item['support_role'] : ''); + list($confidence, $canSupport, $supportRole) = $this->applyMultiClaimPartialSupportFloor( + $localContext, + $doiBlock, + $confidence, + $canSupport, + $supportRole, + $reason + ); + + $combinedConfidence = $this->snapReferenceCheckConfidenceValue( + $this->normalizeConfidence(isset($item['combined_confidence']) ? $item['combined_confidence'] : $confidence) + ); + $combinedCanSupport = $this->canSupportFromConfidence($combinedConfidence); + if (array_key_exists('combined_can_support', $item)) { + $combinedCanSupport = $this->boolFromLlmValue($item['combined_can_support']); + } + + $citeGroupRefs = trim((string)(isset($item['cite_group_refs']) ? $item['cite_group_refs'] : $defaultCiteGroupRefs)); + if ($citeGroupRefs === '' && $defaultCiteGroupRefs !== '') { + $citeGroupRefs = trim((string)$defaultCiteGroupRefs); + } + + $normalized[] = [ + 'reference_no' => $refNo, + 'cite_group_refs' => $citeGroupRefs, + 'can_support' => $canSupport, + 'is_match' => $canSupport, + 'confidence' => $confidence, + 'reason' => $reason, + 'support_role' => $supportRole, + 'combined_can_support' => $combinedCanSupport, + 'combined_confidence' => $combinedConfidence, + 'combined_reason' => $this->cleanReason((string)(isset($item['combined_reason']) ? $item['combined_reason'] : '')), + ]; + } + + return $normalized; + } + + private function normalizeSupportRole($role) + { + $role = strtolower(trim((string)$role)); + $allowed = [ + 'primary_support', + 'supplementary_support', + 'minimal_support', + 'no_meaningful_support', + ]; + return in_array($role, $allowed, true) ? $role : 'no_meaningful_support'; + } + + private function canSupportFromConfidence($confidence) + { + return floatval($confidence) >= 0.65 - 0.001; + } + + /** + * 多通路/异质性 claim + DOI 有多通路证据时,防止误打 0.45(应对齐 0.78 部分支撑) + */ + private function applyMultiClaimPartialSupportFloor($localContext, $doiBlock, $confidence, $canSupport, $supportRole, $reason) + { + $confidence = floatval($confidence); + if ($confidence > 0.45) { + return [$confidence, $canSupport, $supportRole]; + } + + $claimText = trim((string)$localContext); + if ($claimText === '') { + return [$confidence, $canSupport, $supportRole]; + } + + $claimIsMechanism = (bool)preg_match( + '/\b(genomic heterogeneity|heterogeneity|survival pathway|pathways at once|single-target|multi.?pathway|genetic alteration|drug resistance|异质性|生存通路|多.*通路|单靶点|耐药)\b/ui', + $claimText + ); + if (!$claimIsMechanism) { + return [$confidence, $canSupport, $supportRole]; + } + + $corpus = trim((string)$doiBlock) . ' ' . trim((string)$reason); + if ($corpus === '') { + return [$confidence, $canSupport, $supportRole]; + } + + $refHasPathwayEvidence = (bool)preg_match( + '/\b(pathway|PI3K|Akt|mTOR|Wnt|STAT3|NF-κB|NF-kB|genetic alteration|MSI|CIN|drug resistance|signaling|multiple|APC|KRAS|TP53|通路|耐药|信号)\b/ui', + $corpus + ); + if (!$refHasPathwayEvidence) { + return [$confidence, $canSupport, $supportRole]; + } + + $confidence = 0.78; + $canSupport = true; + if ($supportRole === 'no_meaningful_support' || $supportRole === 'minimal_support') { + $supportRole = 'supplementary_support'; + } + + return [$confidence, $canSupport, $supportRole]; + } + + private function getReferenceCheckConfidenceBands() + { + return [0.15, 0.25, 0.45, 0.65, 0.78, 0.85, 0.92, 0.98]; + } + + private function snapReferenceCheckConfidenceValue($confidence) + { + $bands = $this->getReferenceCheckConfidenceBands(); + foreach ($bands as $band) { + if (abs($confidence - $band) < 0.001) { + return $band; + } + } + $nearest = $bands[0]; + $minDiff = abs($confidence - $nearest); + foreach ($bands as $band) { + $diff = abs($confidence - $band); + if ($diff < $minDiff) { + $minDiff = $diff; + $nearest = $band; + } + } + + return $nearest; + } + + /** + * @deprecated 兼容旧逻辑 + */ + private function getReferenceCheckConfidenceBandsLegacy($isMatch) { return $isMatch ? [0.65, 0.78, 0.85, 0.92, 0.98] @@ -1183,22 +1796,24 @@ PROMPT; } /** - * 将模型输出的 confidence 吸附到合法档位(如 0.95 → 0.92,0.75 → 0.78) + * 将模型输出的 confidence 吸附到合法档位 */ private function snapReferenceCheckConfidence($confidence, $isMatch) { - $bands = $this->getReferenceCheckConfidenceBands($isMatch); - + $snapped = $this->snapReferenceCheckConfidenceValue($confidence); + $bands = $this->getReferenceCheckConfidenceBandsLegacy($isMatch); + if (in_array($snapped, $bands, true)) { + return $snapped; + } foreach ($bands as $band) { - if (abs($confidence - $band) < 0.001) { + if (abs($snapped - $band) < 0.001) { return $band; } } - $nearest = $bands[0]; - $minDiff = abs($confidence - $nearest); + $minDiff = abs($snapped - $nearest); foreach ($bands as $band) { - $diff = abs($confidence - $band); + $diff = abs($snapped - $band); if ($diff < $minDiff) { $minDiff = $diff; $nearest = $band; diff --git a/application/common/service/ReferenceRelevanceLlmService.php b/application/common/service/ReferenceRelevanceLlmService.php new file mode 100644 index 00000000..975ead5f --- /dev/null +++ b/application/common/service/ReferenceRelevanceLlmService.php @@ -0,0 +1,670 @@ +url = trim((string)Env::get('promotion.promotion_llm_url', '')); + $this->model = trim((string)Env::get('promotion.promotion_llm_model', '')); + $this->apiKey = trim((string)Env::get('promotion.promotion_llm_api_key', '')); + $this->timeout = max(180, intval(Env::get('promotion.promotion_llm_timeout', 180))); + // 控制发送给 LLM 的上下文长度,降低单次推理耗时(可通过 env 覆盖) + $this->maxSectionChars = max(1500, intval(Env::get('promotion.relevance_llm_max_section_chars', 4500))); + $this->maxLocalContextChars = max(600, intval(Env::get('promotion.relevance_llm_max_local_context_chars', 1800))); + $this->maxReferChars = max(1500, intval(Env::get('promotion.relevance_llm_max_refer_chars', 3500))); + $this->maxAbstractChars = max(1500, intval(Env::get('promotion.relevance_llm_max_abstract_chars', 3500))); + } + + /** + * @return array{results:array,request_failed?:bool,reason?:string} + */ + public function checkRelevance($sectionText, $localContext, $referText, $abstractText = '', $citeGroupRefs = '') + { + $fallback = [ + 'results' => [], + 'request_failed' => true, + 'reason' => 'LLM not configured or request failed', + ]; + if ($this->url === '' || $this->model === '') { + return $fallback; + } + + $sectionText = trim((string)$sectionText); + $localContext = trim((string)$localContext); + $referText = trim((string)$referText); + $abstractText = trim((string)$abstractText); + if ($sectionText === '' || $referText === '') { + return ['results' => [], 'reason' => 'Empty section or reference text']; + } + + if (mb_strlen($sectionText) > $this->maxSectionChars) { + $sectionText = mb_substr($sectionText, 0, $this->maxSectionChars); + } + if (mb_strlen($localContext) > $this->maxLocalContextChars) { + $localContext = mb_substr($localContext, 0, $this->maxLocalContextChars); + } + if (mb_strlen($referText) > $this->maxReferChars) { + $referText = mb_substr($referText, 0, $this->maxReferChars); + } + if (mb_strlen($abstractText) > $this->maxAbstractChars) { + $abstractText = mb_substr($abstractText, 0, $this->maxAbstractChars); + } + + $payload = [ + 'model' => $this->model, + 'temperature' => 0, + 'messages' => [ + ['role' => 'system', 'content' => $this->buildSystemPrompt()], + ['role' => 'user', 'content' => $this->buildUserPrompt($sectionText, $localContext, $referText, $abstractText, $citeGroupRefs)], + ], + ]; + + $content = $this->postChat($payload); + if ($content === null) { + $reason = $this->lastPostError !== '' ? $this->lastPostError : 'LLM request failed'; + return array_merge($fallback, ['reason' => $reason]); + } + + $parsed = $this->parseJson($content); + if ($parsed === null) { + return array_merge($fallback, ['reason' => 'LLM response JSON parse failed']); + } + + $results = $this->normalizeResults($parsed, $citeGroupRefs, $localContext, $referText, $abstractText); + if (empty($results)) { + return array_merge($fallback, ['reason' => 'LLM returned empty or invalid results']); + } + + return ['results' => $results]; + } + + private function buildSystemPrompt() + { + return <<<'PROMPT' +你是一名护理、医学、生物医学与科研期刊的资深学术编辑,正在执行「参考文献主题相关性校对」。 + +你的任务:判断【引用位置正文表述】与【对应编号参考文献】在主题、研究对象、疾病/场景/结局方向上是否相关,能否作为该处引用的合理来源。 + +注意:这是「相关性」校对,侧重引用处具体 claim 与文献内容是否匹配;**不是**判断「是否同一疾病/同一领域」。 + +================================================== +【零、最硬规则(违反则输出无效)】 +1. **单条 relevance_score 只评价该编号文献单独**与引用处的关系;不得因联合组整体合理而抬高弱相关文献的单条分。 +2. **禁止「同病高分」**:正文与文献都涉及 CRC,不等于单条可给 0.85~0.92。 + **但若引用处 claim 本身就是机制/通路/异质性/耐药/治疗挑战**,且**研究主语一致**(同一疾病/同一化合物/同一干预对象),文献(含摘要/清洗内容)讨论同病多通路、遗传改变、耐药等,应给 **0.65~0.78**,不得误降到 0.45。 + **主语不一致时仍适用本条禁止高分**:引用处主语为化合物 X,文献却是其他植物/提取物/计算预测,即使提到 X 或相同通路名,也不得因此给 0.78+。 +3. 引用处若为**流行病学/负担类 claim**(most common、incidence、mortality、burden、全球高发等): + - 机制研究、分子通路、细胞增殖/迁移、血管生成等**原始研究** → 单条通常 **0.45 或更低**,`is_relevant=0`,`minimal_relevance` + - 不得因摘要提到 colorectal cancer 就给 0.92 + - 仅当文献为流行病学综述/公共卫生研究,或明确讨论发病率、死亡率、疾病负担时,单条才可 **0.85~0.92** +4. **联合分写在 combined_relevance_score**,与单条分必须可分离(例如 [1,2] 时文献1=0.45、文献2=0.92、联合=0.92)。 +5. **「来源/化学分类」型句子**(naturally occurring、pentacyclic triterpenoid、found in fruits/vegetables/medicinal plants、并列举具体植物学名): + - 先判文献类型:来源综述 / 生物活性综述 最适合;**抗癌治疗综述**对「来源分布」claim 通常仅 **0.65** + - 单篇可差异化打分(如 0.92 / 0.92 / 0.65),**不得**因联合而三篇都给高分 + - 若原句含**具体列举项**(如多个植物学名),而材料未逐一核实全部学名,联合分通常 **≤0.85**(不得给 0.98) +6. **多要素综括句**(一句同时塞入:药学/研究兴趣 + 大量前临床研究 + 多种活性[抗炎/抗氧化/抗癌等] + 多个癌种/对象列举): + - 单篇即使是综述,通常仅 partially_related ~ near-direct(**0.78~0.86**),**不轻易给 0.92**(单篇难逐项覆盖全部要素) + - **联合分是整句覆盖度评估,可低于最高单条分**:若整句要素需多篇拼合、且含作者整合概括,联合通常 **0.72~0.78(partially_related)**,不给 0.85+ +7. **联合分不是「取最高单条分」**:当各单篇都只覆盖整句一部分、需互补拼合时,联合分应反映「整句作为一个整体被支撑的完整度」,**允许低于任何一篇单条分**。 +8. **主语/研究对象层级必须对齐**:引用处主语为某化合物/分子(如「X has been demonstrated…」)时,文献核心对象须为 **X 本身**或以 X 为核心的实验/综述。 + - **植物提取物/混合物**研究、**其他物种/其他植物**的计算预测、成分表中顺带出现 X → 通常 **0.45 或更低** + - **关键:提取物即使 X 含量很高(如 50%+)且显示了抗癌/凋亡活性,活性归因于提取物整体而非 X 单体单独验证 → 仍属 weakly_related(≤0.45)、`minimal_relevance`**,不得评为 supplementary_relevance/0.78 + - 只有当文献**针对 X 单体单独做了验证**(X monomer 处理、X 单独剂量效应等)时,主语才算对齐,方可进入 0.65+ + - **不得**因摘要/讨论出现与引用句相同的通路名、凋亡、抗癌等词就给 0.78+ +9. **证据层级与 demonstrated / mechanistically**: + - 本文实验结果或针对 X 的系统综述 > 计算预测/混合成分推测 + - **讨论(Discussion)转引他人关于 X 的机制总结 ≠ 该文自身证据**;据此最多 **0.45~0.65**,不得评为 highly_related + - in silico / computational prediction 不足以支撑「has been demonstrated to mechanistically…」式强语气 claim 的高分 +10. **点名通路/功能结局须逐项核对**:原句逐条列举通路(如 PI3K/AKT、MAPK、NF-κB)或结局(增殖、凋亡、血管生成、炎症信号等)时,**每一项单独核对是否在本文证据中成立**(非仅背景提及)。 + - 讨论转述既往文献 ≠ 本文证明该项 + - 缺原句任一点名项(如 angiogenesis)→ 单条通常 **不得 0.78+** + - **「覆盖部分结局」不足以进入 0.78**:原句点名了多条通路 + 多个结局,文献仅命中其中 1~2 个结局(如仅凋亡/增殖),且**点名通路在本文结果中全部缺失(仅讨论转引)**或主语层级不对 → 单条 **限 0.45(weakly_related / minimal_relevance)**,不得给 0.65~0.78 + - 仅同领域沾边 1–2 项、主语或机制层级不对 → **0.45** + - **进入 0.65~0.78 的前提**:主语对齐(X 单体)+ 本文自身结果命中原句点名通路/结局的多数项;几乎全部明确对应 → **0.85+** +11. **文献「主题粒度」必须匹配 claim「主题粒度」**:引用处为**疾病总论型 claim**(流行病学负担、标准/多模态治疗现状与局限、基因组异质性、单靶点治疗受限、亟需新策略等总体背景)时: + - 最适合的来源是**疾病总体综述 / 分子病理综述 / 精准肿瘤学 / 耐药综述**;此类文献正面、系统地为该总论 claim 提供依据 → 可 **0.85+** + - **单一药物 / 单一成分 / 单一通路的专题综述**(如「某化合物抗某癌:A review」),即使同病、同大方向,也只是专题视角、并非为该总论 claim 做系统总结 → 通常 **partially_related(0.72~0.78)**,**不得给 0.85+** + - **单基因 / 单通路的机制原始研究**对纯流行病学负担 claim → 仍按规则 3 给 **0.45** + - 判断要点:文献类型是否「为该总论 claim 本身做系统综述/总论」;仅同病同方向、或只支撑整段中某一两句(如「需要更安全的新策略」),不足以进入 highly_related + +================================================== +【一、必须先拆解 claim】 +从【本引用位置附近上下文】中提炼最小主张单元(Claim A, Claim B…),**不要**把整句笼统归为「大概讲抗癌」。例如: +- **主语/研究对象**(化合物单体 vs 植物提取物 vs 其他物种;是否「X has been demonstrated」) +- **证据语气与层级**(demonstrated / mechanistically vs predict / suggest;本文结果 vs 讨论转引) +- **claim 主题粒度**:是否为疾病总论型(流行病学负担 / 治疗现状与局限 / 基因组异质性 / 单靶点受限 / 亟需新策略);若是,要求「总体综述 / 分子病理 / 精准肿瘤学 / 耐药综述」类来源,单一药物专题综述只算 partially_related +- 疾病流行病学(高发、死亡率) +- **点名通路/分子机制**(PI3K/AKT、MAPK、NF-κB 等,须逐项) +- **点名功能结局**(抑制增殖、凋亡、血管生成、炎症信号等,须逐项) +- 治疗/干预现状 +- **化合物化学类别**(如 pentacyclic triterpenoid) +- **天然来源分布**(fruits / vegetables / medicinal plants) +- **具体列举项**(植物学名、药名、基因名等,须逐项核对) + +================================================== +【二、逐篇文献单独判断(每条 result 对应一个 reference_no)】 +对 cite_group_refs 中的每一篇文献,单独输出: +- 该文献与引用处哪些 claim 主题相关、哪些不相关(含具体列举项是否覆盖) +- 文献类型是否匹配引用用途(来源综述 / 生物活性综述 / 机制研究 / 流行病学综述 / 抗癌治疗综述等) +- relevance_score:只能使用 0.98 / 0.92 / 0.85 / 0.78 / 0.65 / 0.45 / 0.25 / 0.15 +- relevance_level:highly_related | partially_related | weakly_related | unrelated +- is_relevant:score>=0.65 为 1,否则 0 +- relevance_role: + - primary_relevance:该文献是引用处主题的主要相关来源 + - supplementary_relevance:部分相关、补充性 + - minimal_relevance:仅边缘/背景沾边 + - no_meaningful_relevance:与引用处核心表述基本无关 +- reason:中英双语结论,格式固定为两行: + 【中文】(中文结论,须写明:①文献类型与**核心研究对象** ②**本文自身证据**覆盖了哪些 claim / 哪些未覆盖 ③主语/claim 不匹配须明确写出 ④为何此分值) + 【English】(与中文对应的英文结论,语义一致) +- reason_en:仅英文结论(与 reason 中【English】段相同,勿留空) + +主语/层级不对 → 单条 **0.45**,不得因讨论提及相同通路给 0.78: +引用处 claim 为「化合物 X 经 PI3K/AKT 等机制 demonstrated…」,文献为其他植物提取物或计算预测、仅在讨论转引他人 X 机制 → 0.45,weakly_related,is_relevant=0。 + +机制文引用流行病学句 → 单条 **0.45**,不得 0.92: +文献为 CRC 机制研究,引用处 claim 为全球高发/死亡率,文献无流行病学数据 → 0.45,minimal_relevance,is_relevant=0。 + +================================================== +【三、联合引用 combined_*(同一 cite_group_refs 内各行必须一致)】 +当 cite_group_refs 为 "1,2" 等多篇时,除逐篇判断外,必须给出引用组整体结论: +- 这些文献合起来,是否足以支撑/匹配该引用位置的整体表述? +- combined_relevance_score:八档固定分值之一,**不是单条平均分** +- 若一篇已强相关、其余仅弱补充,联合分可接近主相关文献,但**不必等于最高单条分** +- 若原句含具体列举项(学名等)且材料未逐一核实,联合分通常 **0.85**,不给 0.98 +- 若核心 claim 无任何文献明确覆盖,联合分不能虚高 +- 多篇联合仍缺主语对齐、缺原句点名通路/结局、或主要靠讨论转引 → 联合分通常 **≤0.45~0.65**,不得因单篇讨论出现相同关键词给到 0.78+ +- combined_is_relevant:combined_relevance_score>=0.65 为 1 +- combined_relevance_level:与 combined 分数对应的等级 +- combined_reason:中英双语综合结论,格式同 reason(【中文】/【English】),说明各文献分工及最终分值理由 +- combined_reason_en:仅英文综合结论(与 combined_reason 中【English】段相同) + +单条引用时:combined_* 与单条一致;combined_reason / combined_reason_en 可与 reason / reason_en 相同。 + +================================================== +【四、评分与等级对照】 +0.98 / 0.92 / 0.85 = highly_related +文献直接支持整句主旨,大部分关键要素都在文中明确出现 +0.78 / 0.65 = partially_related +文献只支撑其中一部分,或支撑方式偏间接 +0.45 = weakly_related +只是同领域文献,但与句子事实对应很弱 +0.25 / 0.15 = unrelated +基本不支撑该句 +≤0.15 = not_support +不支撑 + +================================================== +【五、输出 JSON(仅 JSON,无 markdown)】 +{ + "results": [ + { + "reference_no": 1, + "cite_group_refs": "1,2", + "is_relevant": 0, + "relevance_score": 0.45, + "relevance_level": "weakly_related", + "relevance_role": "minimal_relevance", + "reason": "【中文】中文单条结论\n【English】English single-reference conclusion", + "reason_en": "English single-reference conclusion", + "combined_is_relevant": 1, + "combined_relevance_score": 0.92, + "combined_relevance_level": "highly_related", + "combined_reason": "【中文】中文联合结论\n【English】English combined conclusion", + "combined_reason_en": "English combined conclusion" + }, + { + "reference_no": 2, + "cite_group_refs": "1,2", + ... + } + ] +} +PROMPT; + } + + private function buildUserPrompt($sectionText, $localContext, $referText, $abstractText, $citeGroupRefs) + { + $parts = ["【正文节 t_article_main】\n" . $sectionText]; + if (trim((string)$citeGroupRefs) !== '') { + $mode = strpos($citeGroupRefs, ',') !== false ? '联合引用' : '单独引用'; + $parts[] = "【引用文献组 cite_group_refs】{$citeGroupRefs}({$mode})"; + } + if ($localContext !== '') { + $parts[] = "【本引用位置附近上下文(优先据此拆解 claim)】\n" . $localContext; + } + $parts[] = "【参考文献书目(按编号)】\n" . $referText; + if ($abstractText !== '') { + $parts[] = "【文献摘要/清洗后内容(Europe PMC·PubMed·Crossref·PDF)】\n" . $abstractText; + } + $parts[] = '请先拆解最小主张单元(主语层级、证据来源、点名通路/结局逐项核对),判断每篇文献类型与**本文自身证据**,再**逐篇独立**给出单条 relevance_score(讨论转引、提取物/计算预测不得抬高;弱相关文献不得因联合而高分),最后给出 combined_*。reason / combined_reason 必须中英双语(【中文】/【English】),并分别填写 reason_en / combined_reason_en。仅输出 results 数组 JSON。'; + + return implode("\n\n", $parts); + } + + private function normalizeResults(array $parsed, $defaultCiteGroupRefs, $localContext = '', $referText = '', $abstractText = '') + { + $rows = []; + if (isset($parsed['results']) && is_array($parsed['results'])) { + $rows = $parsed['results']; + } elseif (isset($parsed['reference_no']) || isset($parsed['relevance_score'])) { + $rows = [$parsed]; + } + + $bands = $this->getScoreBands(); + $localContext = trim((string)$localContext); + $referText = trim((string)$referText); + $abstractText = trim((string)$abstractText); + + $out = []; + foreach ($rows as $item) { + if (!is_array($item)) { + continue; + } + $refNo = intval(isset($item['reference_no']) ? $item['reference_no'] : 0); + if ($refNo <= 0) { + continue; + } + + $score = $this->snapScore(floatval(isset($item['relevance_score']) ? $item['relevance_score'] : 0), $bands); + $isRelevant = $score >= 0.65 - 0.001; + if (array_key_exists('is_relevant', $item)) { + $isRelevant = $this->boolVal($item['is_relevant']); + } + + $level = $this->levelFromScore($score, isset($item['relevance_level']) ? $item['relevance_level'] : ''); + $role = $this->normalizeRelevanceRole(isset($item['relevance_role']) ? $item['relevance_role'] : ''); + list($reason, $reasonEn) = $this->normalizeBilingualReason( + isset($item['reason']) ? $item['reason'] : '', + isset($item['reason_en']) ? $item['reason_en'] : '' + ); + + list($score, $level, $isRelevant, $role) = $this->enforceSingleReferenceConsistency( + $score, + $level, + $isRelevant, + $role, + $bands + ); + + $combinedScore = $this->snapScore( + floatval(isset($item['combined_relevance_score']) ? $item['combined_relevance_score'] : $score), + $bands + ); + $combinedRelevant = $combinedScore >= 0.65 - 0.001; + if (array_key_exists('combined_is_relevant', $item)) { + $combinedRelevant = $this->boolVal($item['combined_is_relevant']); + } + + $combinedLevel = $this->levelFromScore( + $combinedScore, + isset($item['combined_relevance_level']) ? $item['combined_relevance_level'] : '' + ); + list($combinedScore, $combinedLevel, $combinedRelevant) = $this->enforceCombinedConsistency( + $combinedScore, + $combinedLevel, + $combinedRelevant, + $bands + ); + + $citeGroupRefs = trim((string)(isset($item['cite_group_refs']) ? $item['cite_group_refs'] : $defaultCiteGroupRefs)); + if ($citeGroupRefs === '' && $defaultCiteGroupRefs !== '') { + $citeGroupRefs = trim((string)$defaultCiteGroupRefs); + } + + list($combinedReason, $combinedReasonEn) = $this->normalizeBilingualReason( + isset($item['combined_reason']) ? $item['combined_reason'] : '', + isset($item['combined_reason_en']) ? $item['combined_reason_en'] : '' + ); + if ($combinedReason === '' && $combinedReasonEn === '') { + list($combinedReason, $combinedReasonEn) = [$reason, $reasonEn]; + } + + $out[] = [ + 'reference_no' => $refNo, + 'cite_group_refs' => $citeGroupRefs, + 'is_relevant' => $isRelevant ? 1 : 0, + 'relevance_score' => $score, + 'relevance_level' => $level, + 'relevance_role' => $role, + 'reason' => $reason, + 'reason_en' => $reasonEn, + 'combined_is_relevant' => $combinedRelevant ? 1 : 0, + 'combined_relevance_score' => $combinedScore, + 'combined_relevance_level' => $combinedLevel, + 'combined_reason' => $combinedReason, + 'combined_reason_en' => $combinedReasonEn, + ]; + } + + $out = $this->syncCombinedFieldsAcrossGroup($out); + + return $out; + } + + private function enforceSingleReferenceConsistency($score, $level, $isRelevant, $role, array $bands) + { + $score = floatval($score); + if ($role === 'no_meaningful_relevance') { + if ($score > 0.25) { + $score = 0.25; + } + $level = 'unrelated'; + $isRelevant = false; + } elseif ($role === 'minimal_relevance') { + if ($score > 0.45) { + $score = 0.45; + } + $level = 'weakly_related'; + $isRelevant = false; + } elseif ($role === 'supplementary_relevance') { + if ($score > 0.78) { + $score = 0.78; + } + $level = $this->levelFromScore($score, $level); + } elseif ($role === 'primary_relevance') { + if ($score < 0.85) { + $score = 0.85; + } + $isRelevant = true; + $level = $this->levelFromScore($score, $level); + } + + if ($level === 'weakly_related' && $score > 0.45) { + $score = 0.45; + $isRelevant = false; + } elseif ($level === 'unrelated' && $score > 0.25) { + $score = 0.25; + $isRelevant = false; + } elseif ($level === 'highly_related' && $score < 0.85) { + $score = 0.85; + $isRelevant = true; + } elseif ($level === 'partially_related') { + if ($score > 0.78) { + $score = 0.78; + } + if ($score < 0.65) { + $score = 0.65; + } + $isRelevant = true; + } + + if (!$isRelevant && $score >= 0.65) { + $score = 0.45; + $level = 'weakly_related'; + } + if ($isRelevant && $score < 0.65) { + $score = 0.65; + $level = 'partially_related'; + } + + $score = $this->snapScore($score, $bands); + $level = $this->levelFromScore($score, $level); + + return [$score, $level, $isRelevant, $role]; + } + + private function enforceCombinedConsistency($combinedScore, $combinedLevel, $combinedRelevant, array $bands) + { + $combinedScore = $this->snapScore(floatval($combinedScore), $bands); + $combinedLevel = $this->levelFromScore($combinedScore, $combinedLevel); + $combinedRelevant = $combinedScore >= 0.65 - 0.001; + + return [$combinedScore, $combinedLevel, $combinedRelevant]; + } + + private function syncCombinedFieldsAcrossGroup(array $out) + { + $groups = []; + foreach ($out as $idx => $row) { + $key = (string)$row['cite_group_refs']; + if ($key === '') { + $key = 'ref:' . $row['reference_no']; + } + $groups[$key][] = $idx; + } + + foreach ($groups as $indices) { + if (count($indices) <= 1) { + continue; + } + $bestIdx = $indices[0]; + $bestScore = floatval($out[$bestIdx]['combined_relevance_score']); + foreach ($indices as $idx) { + $s = floatval($out[$idx]['combined_relevance_score']); + if ($s >= $bestScore) { + $bestScore = $s; + $bestIdx = $idx; + } + } + $src = $out[$bestIdx]; + foreach ($indices as $idx) { + $out[$idx]['combined_is_relevant'] = intval($src['combined_is_relevant']); + $out[$idx]['combined_relevance_score'] = floatval($src['combined_relevance_score']); + $out[$idx]['combined_relevance_level'] = (string)$src['combined_relevance_level']; + $out[$idx]['combined_reason'] = (string)$src['combined_reason']; + $out[$idx]['combined_reason_en'] = (string)$src['combined_reason_en']; + } + } + + return $out; + } + + private function getScoreBands() + { + return [0.15, 0.25, 0.45, 0.65, 0.78, 0.85, 0.92, 0.98]; + } + + private function snapScore($score, array $bands) + { + foreach ($bands as $band) { + if (abs($score - $band) < 0.001) { + return $band; + } + } + $nearest = $bands[0]; + $minDiff = abs($score - $nearest); + foreach ($bands as $band) { + $diff = abs($score - $band); + if ($diff < $minDiff) { + $minDiff = $diff; + $nearest = $band; + } + } + + return $nearest; + } + + private function levelFromScore($score, $levelHint = '') + { + $levelHint = strtolower(trim((string)$levelHint)); + $allowed = ['highly_related', 'partially_related', 'weakly_related', 'unrelated']; + if (in_array($levelHint, $allowed, true)) { + return $levelHint; + } + $aliases = [ + 'highly_related' => ['highly_related', 'high_related', 'strong_related', 'strong_relevance'], + 'partially_related' => ['partially_related', 'partial_related', 'moderate_related'], + 'weakly_related' => ['weakly_related', 'weak_related', 'low_related', 'insufficient'], + 'unrelated' => ['unrelated', 'not_related', 'irrelevant', 'no_meaningful_relevance'], + ]; + foreach ($aliases as $canonical => $list) { + if (in_array($levelHint, $list, true)) { + return $canonical; + } + } + $score = floatval($score); + if ($score >= 0.85) { + return 'highly_related'; + } + if ($score >= 0.65) { + return 'partially_related'; + } + if ($score >= 0.45) { + return 'weakly_related'; + } + + return 'unrelated'; + } + + private function normalizeRelevanceRole($role) + { + $role = strtolower(trim((string)$role)); + $map = [ + 'primary_relevance' => ['primary_relevance', 'primary_support', 'primary'], + 'supplementary_relevance' => ['supplementary_relevance', 'supplementary_support', 'supplementary'], + 'minimal_relevance' => ['minimal_relevance', 'minimal_support', 'minimal'], + 'no_meaningful_relevance' => ['no_meaningful_relevance', 'no_meaningful_support', 'none'], + ]; + foreach ($map as $canonical => $aliases) { + if ($role === $canonical || in_array($role, $aliases, true)) { + return $canonical; + } + } + + return 'no_meaningful_relevance'; + } + + private function cleanReason($reason) + { + $reason = trim(preg_replace('/[ \t]+/u', ' ', (string)$reason)); + $reason = trim(preg_replace("/\n{3,}/u", "\n\n", $reason)); + return mb_substr($reason, 0, 2000); + } + + /** + * @return array{0:string,1:string} [bilingual reason, english only] + */ + private function normalizeBilingualReason($reason, $reasonEn) + { + $reason = trim((string)$reason); + $reasonEn = $this->cleanReason($reasonEn); + + if ($reasonEn === '' && preg_match('/【English】\s*(.+)$/us', $reason, $m)) { + $reasonEn = $this->cleanReason($m[1]); + } + + $zh = ''; + if (preg_match('/【中文】\s*(.*?)(?:\n【English】|$)/us', $reason, $m)) { + $zh = trim($m[1]); + } elseif ($reason !== '' && strpos($reason, '【English】') === false) { + $zh = trim($reason); + } + + if ($zh !== '' && $reasonEn !== '' && strpos($reason, '【English】') === false) { + $reason = "【中文】{$zh}\n【English】{$reasonEn}"; + } elseif ($zh !== '' && $reasonEn !== '' && strpos($reason, '【中文】') === false) { + $reason = "【中文】{$zh}\n【English】{$reasonEn}"; + } else { + $reason = $this->cleanReason($reason); + } + + if ($reasonEn === '' && $zh !== '') { + $reasonEn = ''; + } + + return [$reason, $reasonEn]; + } + + private function boolVal($v) + { + if (is_bool($v)) { + return $v; + } + if (is_numeric($v)) { + return intval($v) !== 0; + } + $s = strtolower(trim((string)$v)); + return in_array($s, ['1', 'true', 'yes', 'y'], true); + } + + private function postChat(array $payload) + { + $this->lastPostError = ''; + try { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->url); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE)); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, min(15, $this->timeout)); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); + $headers = ['Content-Type: application/json']; + if ($this->apiKey !== '') { + $headers[] = 'Authorization: Bearer ' . $this->apiKey; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + $raw = curl_exec($ch); + if ($raw === false) { + $this->lastPostError = 'LLM curl error: ' . curl_error($ch); + \think\Log::warning('ReferenceRelevanceLlm: ' . $this->lastPostError); + curl_close($ch); + return null; + } + $httpCode = intval(curl_getinfo($ch, CURLINFO_HTTP_CODE)); + curl_close($ch); + if ($httpCode < 200 || $httpCode >= 300) { + $snippet = mb_substr(trim((string)$raw), 0, 200); + $this->lastPostError = 'LLM HTTP ' . $httpCode . ($snippet !== '' ? ': ' . $snippet : ''); + \think\Log::warning('ReferenceRelevanceLlm: ' . $this->lastPostError); + return null; + } + $data = json_decode($raw, true); + if (!is_array($data)) { + $this->lastPostError = 'LLM response is not valid JSON'; + return null; + } + if (isset($data['choices'][0]['message']['content'])) { + return (string)$data['choices'][0]['message']['content']; + } + if (isset($data['content'])) { + return (string)$data['content']; + } + $this->lastPostError = 'LLM response missing content field'; + } catch (\Exception $e) { + $this->lastPostError = 'LLM exception: ' . $e->getMessage(); + \think\Log::warning('ReferenceRelevanceLlm: ' . $this->lastPostError); + } + + return null; + } + + private function parseJson($raw) + { + $raw = trim((string)$raw); + if ($raw === '') { + return null; + } + $raw = preg_replace('/^```[a-zA-Z]*\s*|```$/m', '', $raw); + $raw = trim($raw); + $decoded = json_decode($raw, true); + if (is_array($decoded)) { + return $decoded; + } + if (preg_match('/\{[\s\S]*\}/', $raw, $m)) { + $decoded = json_decode($m[0], true); + if (is_array($decoded)) { + return $decoded; + } + } + + return null; + } +} diff --git a/composer.json b/composer.json index a0a034eb..e8b8d190 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,8 @@ "paypal/paypal-server-sdk": "^0.6.1", "guzzlehttp/guzzle": "^7.9", "php-amqplib/php-amqplib": "^2.12", - "tectalic/openai": "^1.6" + "tectalic/openai": "^1.6", + "smalot/pdfparser": "^2.0" }, "autoload": { "psr-4": { diff --git a/sql/reference_check_result_alter_positions.sql b/sql/reference_check_result_alter_positions.sql index 688dd47e..db6d65b3 100644 --- a/sql/reference_check_result_alter_positions.sql +++ b/sql/reference_check_result_alter_positions.sql @@ -1,5 +1,6 @@ -- 为预览标记增加原文位置字段([70-73] 展开为 4 条时共用 cite_tag_* / text_*) -ALTER TABLE `t_reference_check_result` +-- 已合并到 article_reference_check_result_alter_columns.sql(表名 t_article_reference_check_result) +ALTER TABLE `t_article_reference_check_result` ADD COLUMN `cite_tag_start` int(11) NOT NULL DEFAULT 0 COMMENT 'blue标签起始字节偏移' AFTER `reference_raw`, ADD COLUMN `cite_tag_end` int(11) NOT NULL DEFAULT 0 COMMENT 'blue标签结束字节偏移' AFTER `cite_tag_start`, ADD COLUMN `text_start` int(11) NOT NULL DEFAULT 0 COMMENT '引用句起始字节偏移' AFTER `cite_tag_end`, diff --git a/vendor/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php index 0fb0a2c1..730bac18 100644 --- a/vendor/composer/autoload_classmap.php +++ b/vendor/composer/autoload_classmap.php @@ -6,5 +6,10 @@ $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( + 'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Stringable' => $vendorDir . '/myclabs/php-enum/stubs/Stringable.php', + 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', ); diff --git a/vendor/composer/autoload_files.php b/vendor/composer/autoload_files.php index 5db964a2..355b401b 100644 --- a/vendor/composer/autoload_files.php +++ b/vendor/composer/autoload_files.php @@ -7,12 +7,13 @@ $baseDir = dirname($vendorDir); return array( '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', + '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', + 'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', '9c67151ae59aff4788964ce8eb2a0f43' => $vendorDir . '/clue/stream-filter/src/functions_include.php', '7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', '2cffec82183ee1cea088009cef9a6fc3' => $vendorDir . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', '8cff32064859f4559445b89279f3199c' => $vendorDir . '/php-http/message/src/filters.php', 'decc78cc4436b1292c6c0d151b19445c' => $vendorDir . '/phpseclib/phpseclib/phpseclib/bootstrap.php', - '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', '9b552a3cc426e3287cc811caefa3cf53' => $vendorDir . '/topthink/think-helper/src/helper.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', '1cfd2761b63b0a29ed23657ea394cb2d' => $vendorDir . '/topthink/think-captcha/src/helper.php', diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php index 990c758f..eb4850fe 100644 --- a/vendor/composer/autoload_namespaces.php +++ b/vendor/composer/autoload_namespaces.php @@ -6,6 +6,7 @@ $vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( + 'Smalot\\PdfParser\\' => array($vendorDir . '/smalot/pdfparser/src'), 'Rs\\Json' => array($vendorDir . '/php-jsonpointer/php-jsonpointer/src'), 'PHPExcel' => array($vendorDir . '/phpoffice/phpexcel/Classes'), 'HTMLPurifier' => array($vendorDir . '/ezyang/htmlpurifier/library'), diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php index 6ce0181d..6db206ef 100644 --- a/vendor/composer/autoload_psr4.php +++ b/vendor/composer/autoload_psr4.php @@ -15,11 +15,12 @@ return array( 'ZipStream\\' => array($vendorDir . '/maennchen/zipstream-php/src'), 'Unirest\\' => array($vendorDir . '/apimatic/unirest-php/src'), 'Tectalic\\OpenAi\\' => array($vendorDir . '/tectalic/openai/src'), + 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Component\\HttpFoundation\\' => array($vendorDir . '/symfony/http-foundation'), 'Spatie\\DataTransferObject\\' => array($vendorDir . '/spatie/data-transfer-object/src'), 'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'), - 'Psr\\Log\\' => array($vendorDir . '/psr/log/src'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), 'Psr\\Http\\Message\\' => array($vendorDir . '/psr/http-factory/src', $vendorDir . '/psr/http-message/src'), 'Psr\\Http\\Client\\' => array($vendorDir . '/psr/http-client/src'), 'PhpOffice\\PhpWord\\' => array($vendorDir . '/phpoffice/phpword/src/PhpWord'), @@ -30,6 +31,7 @@ return array( 'ParagonIE\\ConstantTime\\' => array($vendorDir . '/paragonie/constant_time_encoding/src'), 'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'), 'Nyholm\\Psr7\\' => array($vendorDir . '/nyholm/psr7/src'), + 'MyCLabs\\Enum\\' => array($vendorDir . '/myclabs/php-enum/src'), 'Matrix\\' => array($vendorDir . '/markbaker/matrix/classes/src'), 'Http\\Message\\MultipartStream\\' => array($vendorDir . '/php-http/multipart-stream-builder/src'), 'Http\\Message\\' => array($vendorDir . '/php-http/message/src'), @@ -39,7 +41,6 @@ return array( 'GuzzleHttp\\' => array($vendorDir . '/guzzlehttp/guzzle/src'), 'Core\\' => array($vendorDir . '/apimatic/core/src'), 'CoreInterfaces\\' => array($vendorDir . '/apimatic/core-interfaces/src'), - 'Composer\\Pcre\\' => array($vendorDir . '/composer/pcre/src'), 'Complex\\' => array($vendorDir . '/markbaker/complex/classes/src'), 'Clue\\StreamFilter\\' => array($vendorDir . '/clue/stream-filter/src'), ); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php index e9626643..b341152f 100644 --- a/vendor/composer/autoload_real.php +++ b/vendor/composer/autoload_real.php @@ -22,6 +22,8 @@ class ComposerAutoloaderInit2bc4f313dba415539e266f7ac2c87dcd return self::$loader; } + require __DIR__ . '/platform_check.php'; + spl_autoload_register(array('ComposerAutoloaderInit2bc4f313dba415539e266f7ac2c87dcd', 'loadClassLoader'), true, true); self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); spl_autoload_unregister(array('ComposerAutoloaderInit2bc4f313dba415539e266f7ac2c87dcd', 'loadClassLoader')); diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php index 38ba10b9..b2e237ca 100644 --- a/vendor/composer/autoload_static.php +++ b/vendor/composer/autoload_static.php @@ -8,12 +8,13 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd { public static $files = array ( '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', + '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', + 'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', '9c67151ae59aff4788964ce8eb2a0f43' => __DIR__ . '/..' . '/clue/stream-filter/src/functions_include.php', '7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', '2cffec82183ee1cea088009cef9a6fc3' => __DIR__ . '/..' . '/ezyang/htmlpurifier/library/HTMLPurifier.composer.php', '8cff32064859f4559445b89279f3199c' => __DIR__ . '/..' . '/php-http/message/src/filters.php', 'decc78cc4436b1292c6c0d151b19445c' => __DIR__ . '/..' . '/phpseclib/phpseclib/phpseclib/bootstrap.php', - '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', '9b552a3cc426e3287cc811caefa3cf53' => __DIR__ . '/..' . '/topthink/think-helper/src/helper.php', '37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', '1cfd2761b63b0a29ed23657ea394cb2d' => __DIR__ . '/..' . '/topthink/think-captcha/src/helper.php', @@ -50,6 +51,7 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd ), 'S' => array ( + 'Symfony\\Polyfill\\Php80\\' => 23, 'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Component\\HttpFoundation\\' => 33, 'Spatie\\DataTransferObject\\' => 26, @@ -74,6 +76,7 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd ), 'M' => array ( + 'MyCLabs\\Enum\\' => 13, 'Matrix\\' => 7, ), 'H' => @@ -92,7 +95,6 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd array ( 'Core\\' => 5, 'CoreInterfaces\\' => 15, - 'Composer\\Pcre\\' => 14, 'Complex\\' => 8, 'Clue\\StreamFilter\\' => 18, ), @@ -138,6 +140,10 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd array ( 0 => __DIR__ . '/..' . '/tectalic/openai/src', ), + 'Symfony\\Polyfill\\Php80\\' => + array ( + 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', + ), 'Symfony\\Polyfill\\Mbstring\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-mbstring', @@ -156,7 +162,7 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd ), 'Psr\\Log\\' => array ( - 0 => __DIR__ . '/..' . '/psr/log/src', + 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', ), 'Psr\\Http\\Message\\' => array ( @@ -199,6 +205,10 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd array ( 0 => __DIR__ . '/..' . '/nyholm/psr7/src', ), + 'MyCLabs\\Enum\\' => + array ( + 0 => __DIR__ . '/..' . '/myclabs/php-enum/src', + ), 'Matrix\\' => array ( 0 => __DIR__ . '/..' . '/markbaker/matrix/classes/src', @@ -235,10 +245,6 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd array ( 0 => __DIR__ . '/..' . '/apimatic/core-interfaces/src', ), - 'Composer\\Pcre\\' => - array ( - 0 => __DIR__ . '/..' . '/composer/pcre/src', - ), 'Complex\\' => array ( 0 => __DIR__ . '/..' . '/markbaker/complex/classes/src', @@ -250,6 +256,13 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd ); public static $prefixesPsr0 = array ( + 'S' => + array ( + 'Smalot\\PdfParser\\' => + array ( + 0 => __DIR__ . '/..' . '/smalot/pdfparser/src', + ), + ), 'R' => array ( 'Rs\\Json' => @@ -274,7 +287,12 @@ class ComposerStaticInit2bc4f313dba415539e266f7ac2c87dcd ); public static $classMap = array ( + 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', + 'Stringable' => __DIR__ . '/..' . '/myclabs/php-enum/stubs/Stringable.php', + 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', + 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', ); public static function getInitializer(ClassLoader $loader) diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json index 360dacfc..68c16be7 100644 --- a/vendor/composer/installed.json +++ b/vendor/composer/installed.json @@ -2,24 +2,18 @@ "packages": [ { "name": "apimatic/core", - "version": "0.3.16", - "version_normalized": "0.3.16.0", + "version": "0.3.17", + "version_normalized": "0.3.17.0", "source": { "type": "git", "url": "https://github.com/apimatic/core-lib-php.git", - "reference": "ae4ab4ca26a41be41718f33c703d67b7a767c07b" + "reference": "a48a583f686ee3786432b976c795a2817ec095b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/apimatic/core-lib-php/zipball/ae4ab4ca26a41be41718f33c703d67b7a767c07b", - "reference": "ae4ab4ca26a41be41718f33c703d67b7a767c07b", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/apimatic/core-lib-php/zipball/a48a583f686ee3786432b976c795a2817ec095b3", + "reference": "a48a583f686ee3786432b976c795a2817ec095b3", + "shasum": "" }, "require": { "apimatic/core-interfaces": "~0.1.5", @@ -38,7 +32,7 @@ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5", "squizlabs/php_codesniffer": "^3.5" }, - "time": "2025-11-25T04:42:27+00:00", + "time": "2026-01-27T05:14:10+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -60,7 +54,7 @@ ], "support": { "issues": "https://github.com/apimatic/core-lib-php/issues", - "source": "https://github.com/apimatic/core-lib-php/tree/0.3.16" + "source": "https://github.com/apimatic/core-lib-php/tree/0.3.17" }, "install-path": "../apimatic/core" }, @@ -294,94 +288,6 @@ }, "install-path": "../clue/stream-filter" }, - { - "name": "composer/pcre", - "version": "3.3.2", - "version_normalized": "3.3.2.0", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<1.11.10" - }, - "require-dev": { - "phpstan/phpstan": "^1.12 || ^2", - "phpstan/phpstan-strict-rules": "^1 || ^2", - "phpunit/phpunit": "^8 || ^9" - }, - "time": "2024-11-12T16:29:46+00:00", - "type": "library", - "extra": { - "branch-alias": { - "dev-main": "3.x-dev" - }, - "phpstan": { - "includes": [ - "extension.neon" - ] - } - }, - "installation-source": "dist", - "autoload": { - "psr-4": { - "Composer\\Pcre\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jordi Boggiano", - "email": "j.boggiano@seld.be", - "homepage": "http://seld.be" - } - ], - "description": "PCRE wrapping library that offers type-safe preg_* replacements.", - "keywords": [ - "PCRE", - "preg", - "regex", - "regular expression" - ], - "support": { - "issues": "https://github.com/composer/pcre/issues", - "source": "https://github.com/composer/pcre/tree/3.3.2" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/composer/composer", - "type": "tidelift" - } - ], - "install-path": "./pcre" - }, { "name": "ezyang/htmlpurifier", "version": "v4.19.0", @@ -635,24 +541,18 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.8.0", - "version_normalized": "2.8.0.0", + "version": "2.9.0", + "version_normalized": "2.9.0.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "21dc724a0583619cd1652f673303492272778051" + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/21dc724a0583619cd1652f673303492272778051", - "reference": "21dc724a0583619cd1652f673303492272778051", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/guzzle/psr7/zipball/7d0ed42f28e42d61352a7a79de682e5e67fec884", + "reference": "7d0ed42f28e42d61352a7a79de682e5e67fec884", + "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", @@ -667,12 +567,13 @@ "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", "http-interop/http-factory-tests": "0.9.0", + "jshttp/mime-db": "1.54.0.1", "phpunit/phpunit": "^8.5.44 || ^9.6.25" }, "suggest": { "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, - "time": "2025-08-23T21:21:41+00:00", + "time": "2026-03-10T16:41:02+00:00", "type": "library", "extra": { "bamarni-bin": { @@ -740,7 +641,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.8.0" + "source": "https://github.com/guzzle/psr7/tree/2.9.0" }, "funding": [ { @@ -760,45 +661,32 @@ }, { "name": "maennchen/zipstream-php", - "version": "3.1.2", - "version_normalized": "3.1.2.0", + "version": "2.1.0", + "version_normalized": "2.1.0.0", "source": { "type": "git", "url": "https://github.com/maennchen/ZipStream-PHP.git", - "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f" + "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/aeadcf5c412332eb426c0f9b4485f6accba2a99f", - "reference": "aeadcf5c412332eb426c0f9b4485f6accba2a99f", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58", + "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58", + "shasum": "" }, "require": { - "ext-mbstring": "*", - "ext-zlib": "*", - "php-64bit": "^8.2" + "myclabs/php-enum": "^1.5", + "php": ">= 7.1", + "psr/http-message": "^1.0", + "symfony/polyfill-mbstring": "^1.0" }, "require-dev": { - "brianium/paratest": "^7.7", "ext-zip": "*", - "friendsofphp/php-cs-fixer": "^3.16", - "guzzlehttp/guzzle": "^7.5", + "guzzlehttp/guzzle": ">= 6.3", "mikey179/vfsstream": "^1.6", - "php-coveralls/php-coveralls": "^2.5", - "phpunit/phpunit": "^11.0", - "vimeo/psalm": "^6.0" + "phpunit/phpunit": ">= 7.5" }, - "suggest": { - "guzzlehttp/psr7": "^2.4", - "psr/http-message": "^2.0" - }, - "time": "2025-01-27T12:07:53+00:00", + "time": "2020-05-30T13:11:16+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -835,12 +723,16 @@ ], "support": { "issues": "https://github.com/maennchen/ZipStream-PHP/issues", - "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.2" + "source": "https://github.com/maennchen/ZipStream-PHP/tree/2.1.0" }, "funding": [ { "url": "https://github.com/maennchen", "type": "github" + }, + { + "url": "https://opencollective.com/zipstream", + "type": "open_collective" } ], "install-path": "../maennchen/zipstream-php" @@ -958,6 +850,72 @@ }, "install-path": "../markbaker/matrix" }, + { + "name": "myclabs/php-enum", + "version": "1.8.5", + "version_normalized": "1.8.5.0", + "source": { + "type": "git", + "url": "https://github.com/myclabs/php-enum.git", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/php-enum/zipball/e7be26966b7398204a234f8673fdad5ac6277802", + "reference": "e7be26966b7398204a234f8673fdad5ac6277802", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5", + "squizlabs/php_codesniffer": "1.*", + "vimeo/psalm": "^4.6.2 || ^5.2" + }, + "time": "2025-01-14T11:49:03+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "MyCLabs\\Enum\\": "src/" + }, + "classmap": [ + "stubs/Stringable.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP Enum contributors", + "homepage": "https://github.com/myclabs/php-enum/graphs/contributors" + } + ], + "description": "PHP Enum implementation", + "homepage": "https://github.com/myclabs/php-enum", + "keywords": [ + "enum" + ], + "support": { + "issues": "https://github.com/myclabs/php-enum/issues", + "source": "https://github.com/myclabs/php-enum/tree/1.8.5" + }, + "funding": [ + { + "url": "https://github.com/mnapoli", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum", + "type": "tidelift" + } + ], + "install-path": "../myclabs/php-enum" + }, { "name": "nyholm/psr7", "version": "1.8.2", @@ -1031,17 +989,17 @@ }, { "name": "paragonie/constant_time_encoding", - "version": "v3.1.3", - "version_normalized": "3.1.3.0", + "version": "v2.8.2", + "version_normalized": "2.8.2.0", "source": { "type": "git", "url": "https://github.com/paragonie/constant_time_encoding.git", - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77" + "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", - "reference": "d5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77", + "url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/e30811f7bc69e4b5b6d5783e712c06c8eabf0226", + "reference": "e30811f7bc69e4b5b6d5783e712c06c8eabf0226", "shasum": "", "mirrors": [ { @@ -1051,15 +1009,13 @@ ] }, "require": { - "php": "^8" + "php": "^7|^8" }, "require-dev": { - "infection/infection": "^0", - "nikic/php-fuzzer": "^0", - "phpunit/phpunit": "^9|^10|^11", - "vimeo/psalm": "^4|^5|^6" + "phpunit/phpunit": "^6|^7|^8|^9", + "vimeo/psalm": "^1|^2|^3|^4" }, - "time": "2025-09-24T15:06:41+00:00", + "time": "2025-09-24T15:12:37+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1386,24 +1342,18 @@ }, { "name": "php-http/message", - "version": "1.16.1", - "version_normalized": "1.16.1.0", + "version": "1.16.2", + "version_normalized": "1.16.2.0", "source": { "type": "git", "url": "https://github.com/php-http/message.git", - "reference": "5997f3289332c699fa2545c427826272498a2088" + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/message/zipball/5997f3289332c699fa2545c427826272498a2088", - "reference": "5997f3289332c699fa2545c427826272498a2088", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/php-http/message/zipball/06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "reference": "06dd5e8562f84e641bf929bfe699ee0f5ce8080a", + "shasum": "" }, "require": { "clue/stream-filter": "^1.5", @@ -1428,7 +1378,7 @@ "laminas/laminas-diactoros": "Used with Diactoros Factories", "slim/slim": "Used with Slim Framework PSR-7 implementation" }, - "time": "2024-03-07T13:22:09+00:00", + "time": "2024-10-02T11:34:13+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1458,30 +1408,24 @@ ], "support": { "issues": "https://github.com/php-http/message/issues", - "source": "https://github.com/php-http/message/tree/1.16.1" + "source": "https://github.com/php-http/message/tree/1.16.2" }, "install-path": "../php-http/message" }, { "name": "php-http/multipart-stream-builder", - "version": "1.3.1", - "version_normalized": "1.3.1.0", + "version": "1.4.2", + "version_normalized": "1.4.2.0", "source": { "type": "git", "url": "https://github.com/php-http/multipart-stream-builder.git", - "reference": "ed56da23b95949ae4747378bed8a5b61a2fdae24" + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/ed56da23b95949ae4747378bed8a5b61a2fdae24", - "reference": "ed56da23b95949ae4747378bed8a5b61a2fdae24", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/php-http/multipart-stream-builder/zipball/10086e6de6f53489cca5ecc45b6f468604d3460e", + "reference": "10086e6de6f53489cca5ecc45b6f468604d3460e", + "shasum": "" }, "require": { "php": "^7.1 || ^8.0", @@ -1494,7 +1438,7 @@ "php-http/message-factory": "^1.0.2", "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.3" }, - "time": "2024-06-10T14:51:55+00:00", + "time": "2024-09-04T13:22:54+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1523,7 +1467,7 @@ ], "support": { "issues": "https://github.com/php-http/multipart-stream-builder/issues", - "source": "https://github.com/php-http/multipart-stream-builder/tree/1.3.1" + "source": "https://github.com/php-http/multipart-stream-builder/tree/1.4.2" }, "install-path": "../php-http/multipart-stream-builder" }, @@ -1588,24 +1532,18 @@ }, { "name": "phpmailer/phpmailer", - "version": "v6.11.1", - "version_normalized": "6.11.1.0", + "version": "v6.12.0", + "version_normalized": "6.12.0.0", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284" + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d9e3b36b47f04b497a0164c5a20f92acb4593284", - "reference": "d9e3b36b47f04b497a0164c5a20f92acb4593284", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/d1ac35d784bf9f5e61b424901d5a014967f15b12", + "reference": "d1ac35d784bf9f5e61b424901d5a014967f15b12", + "shasum": "" }, "require": { "ext-ctype": "*", @@ -1625,7 +1563,6 @@ }, "suggest": { "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", - "ext-imap": "Needed to support advanced email address parsing according to RFC822", "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", "ext-openssl": "Needed for secure SMTP sending and DKIM signing", "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", @@ -1635,7 +1572,7 @@ "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" }, - "time": "2025-09-30T11:54:53+00:00", + "time": "2025-10-15T16:49:08+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1667,7 +1604,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.11.1" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.12.0" }, "funding": [ { @@ -1679,24 +1616,18 @@ }, { "name": "phpoffice/math", - "version": "0.2.0", - "version_normalized": "0.2.0.0", + "version": "0.3.0", + "version_normalized": "0.3.0.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/Math.git", - "reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329" + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc2eb6d1a61b058d5dac77197059db30ee3c8329", - "reference": "fc2eb6d1a61b058d5dac77197059db30ee3c8329", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/PHPOffice/Math/zipball/fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "reference": "fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a", + "shasum": "" }, "require": { "ext-dom": "*", @@ -1707,7 +1638,7 @@ "phpstan/phpstan": "^0.12.88 || ^1.0.0", "phpunit/phpunit": "^7.0 || ^9.0" }, - "time": "2024-08-12T07:30:45+00:00", + "time": "2025-05-29T08:31:49+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1734,7 +1665,7 @@ ], "support": { "issues": "https://github.com/PHPOffice/Math/issues", - "source": "https://github.com/PHPOffice/Math/tree/0.2.0" + "source": "https://github.com/PHPOffice/Math/tree/0.3.0" }, "install-path": "../phpoffice/math" }, @@ -1810,27 +1741,20 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "1.30.1", - "version_normalized": "1.30.1.0", + "version": "1.25.2", + "version_normalized": "1.25.2.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "fa8257a579ec623473eabfe49731de5967306c4c" + "reference": "a317a09e7def49852400a4b3eca4a4b0790ceeb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fa8257a579ec623473eabfe49731de5967306c4c", - "reference": "fa8257a579ec623473eabfe49731de5967306c4c", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/a317a09e7def49852400a4b3eca4a4b0790ceeb5", + "reference": "a317a09e7def49852400a4b3eca4a4b0790ceeb5", + "shasum": "" }, "require": { - "composer/pcre": "^1||^2||^3", "ext-ctype": "*", "ext-dom": "*", "ext-fileinfo": "*", @@ -1845,26 +1769,26 @@ "ext-zip": "*", "ext-zlib": "*", "ezyang/htmlpurifier": "^4.15", - "maennchen/zipstream-php": "^2.1 || ^3.0", + "maennchen/zipstream-php": "^2.1", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", - "php": ">=7.4.0 <8.5.0", + "php": "^7.3 || ^8.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "dev-main", - "dompdf/dompdf": "^1.0 || ^2.0 || ^3.0", + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "mitoteam/jpgraph": "^10.3", - "mpdf/mpdf": "^8.1.1", + "mitoteam/jpgraph": "10.2.4", + "mpdf/mpdf": "8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5 || ^9.0", "squizlabs/php_codesniffer": "^3.7", - "tecnickcom/tcpdf": "^6.5" + "tecnickcom/tcpdf": "6.5" }, "suggest": { "dompdf/dompdf": "Option for rendering PDF with PDF Writer", @@ -1873,7 +1797,7 @@ "mpdf/mpdf": "Option for rendering PDF with PDF Writer", "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" }, - "time": "2025-10-26T16:01:04+00:00", + "time": "2022-09-25T17:21:01+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1919,59 +1843,52 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.30.1" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.25.2" }, "install-path": "../phpoffice/phpspreadsheet" }, { "name": "phpoffice/phpword", - "version": "1.3.0", - "version_normalized": "1.3.0.0", + "version": "1.4.0", + "version_normalized": "1.4.0.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PHPWord.git", - "reference": "8392134ce4b5dba65130ba956231a1602b848b7f" + "reference": "6d75328229bc93790b37e93741adf70646cea958" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/8392134ce4b5dba65130ba956231a1602b848b7f", - "reference": "8392134ce4b5dba65130ba956231a1602b848b7f", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/PHPOffice/PHPWord/zipball/6d75328229bc93790b37e93741adf70646cea958", + "reference": "6d75328229bc93790b37e93741adf70646cea958", + "shasum": "" }, "require": { "ext-dom": "*", + "ext-gd": "*", "ext-json": "*", "ext-xml": "*", + "ext-zip": "*", "php": "^7.1|^8.0", - "phpoffice/math": "^0.2" + "phpoffice/math": "^0.3" }, "require-dev": { - "dompdf/dompdf": "^2.0", - "ext-gd": "*", + "dompdf/dompdf": "^2.0 || ^3.0", "ext-libxml": "*", - "ext-zip": "*", "friendsofphp/php-cs-fixer": "^3.3", - "mpdf/mpdf": "^8.1", + "mpdf/mpdf": "^7.0 || ^8.0", "phpmd/phpmd": "^2.13", - "phpstan/phpstan-phpunit": "@stable", + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", "phpunit/phpunit": ">=7.0", "symfony/process": "^4.4 || ^5.0", "tecnickcom/tcpdf": "^6.5" }, "suggest": { "dompdf/dompdf": "Allows writing PDF", - "ext-gd2": "Allows adding images", "ext-xmlwriter": "Allows writing OOXML and ODF", - "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template", - "ext-zip": "Allows writing OOXML and ODF" + "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template" }, - "time": "2024-08-30T18:03:42+00:00", + "time": "2025-06-05T10:32:36+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -1981,7 +1898,7 @@ }, "notification-url": "https://packagist.org/downloads/", "license": [ - "LGPL-3.0" + "LGPL-3.0-only" ], "authors": [ { @@ -2037,7 +1954,7 @@ ], "support": { "issues": "https://github.com/PHPOffice/PHPWord/issues", - "source": "https://github.com/PHPOffice/PHPWord/tree/1.3.0" + "source": "https://github.com/PHPOffice/PHPWord/tree/1.4.0" }, "install-path": "../phpoffice/phpword" }, @@ -2217,30 +2134,24 @@ }, { "name": "psr/http-factory", - "version": "1.0.2", - "version_normalized": "1.0.2.0", + "version": "1.1.0", + "version_normalized": "1.1.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-factory.git", - "reference": "e616d01114759c4c489f93b099585439f795fe35" + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35", - "reference": "e616d01114759c4c489f93b099585439f795fe35", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a", + "shasum": "" }, "require": { - "php": ">=7.0.0", + "php": ">=7.1", "psr/http-message": "^1.0 || ^2.0" }, - "time": "2023-04-10T20:10:41+00:00", + "time": "2024-04-15T12:06:14+00:00", "type": "library", "extra": { "branch-alias": { @@ -2263,7 +2174,7 @@ "homepage": "https://www.php-fig.org/" } ], - "description": "Common interfaces for PSR-7 HTTP message factories", + "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories", "keywords": [ "factory", "http", @@ -2275,39 +2186,33 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-factory/tree/1.0.2" + "source": "https://github.com/php-fig/http-factory" }, "install-path": "../psr/http-factory" }, { "name": "psr/http-message", - "version": "2.0", - "version_normalized": "2.0.0.0", + "version": "1.1", + "version_normalized": "1.1.0.0", "source": { "type": "git", "url": "https://github.com/php-fig/http-message.git", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71" + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/php-fig/http-message/zipball/cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "reference": "cb6ce4845ce34a8ad9e68117c10ee90a29919eba", + "shasum": "" }, "require": { "php": "^7.2 || ^8.0" }, - "time": "2023-04-04T09:54:51+00:00", + "time": "2023-04-04T09:50:52+00:00", "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.1.x-dev" } }, "installation-source": "dist", @@ -2323,7 +2228,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Common interface for HTTP messages", @@ -2337,45 +2242,39 @@ "response" ], "support": { - "source": "https://github.com/php-fig/http-message/tree/2.0" + "source": "https://github.com/php-fig/http-message/tree/1.1" }, "install-path": "../psr/http-message" }, { "name": "psr/log", - "version": "3.0.1", - "version_normalized": "3.0.1.0", + "version": "1.1.4", + "version_normalized": "1.1.4.0", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "79dff0b268932c640297f5208d6298f71855c03e" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/79dff0b268932c640297f5208d6298f71855c03e", - "reference": "79dff0b268932c640297f5208d6298f71855c03e", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=5.3.0" }, - "time": "2024-08-21T13:31:24+00:00", + "time": "2021-05-03T11:20:27+00:00", "type": "library", "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "1.1.x-dev" } }, "installation-source": "dist", "autoload": { "psr-4": { - "Psr\\Log\\": "src" + "Psr\\Log\\": "Psr/Log/" } }, "notification-url": "https://packagist.org/downloads/", @@ -2396,39 +2295,33 @@ "psr-3" ], "support": { - "source": "https://github.com/php-fig/log/tree/3.0.1" + "source": "https://github.com/php-fig/log/tree/1.1.4" }, "install-path": "../psr/log" }, { "name": "psr/simple-cache", - "version": "3.0.0", - "version_normalized": "3.0.0.0", + "version": "1.0.1", + "version_normalized": "1.0.1.0", "source": { "type": "git", "url": "https://github.com/php-fig/simple-cache.git", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", - "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b", + "shasum": "" }, "require": { - "php": ">=8.0.0" + "php": ">=5.3.0" }, - "time": "2021-10-29T13:26:27+00:00", + "time": "2017-10-23T01:57:42+00:00", "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0.x-dev" + "dev-master": "1.0.x-dev" } }, "installation-source": "dist", @@ -2444,7 +2337,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "https://www.php-fig.org/" + "homepage": "http://www.php-fig.org/" } ], "description": "Common interfaces for simple caching", @@ -2456,7 +2349,7 @@ "simple-cache" ], "support": { - "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + "source": "https://github.com/php-fig/simple-cache/tree/master" }, "install-path": "../psr/simple-cache" }, @@ -2507,6 +2400,66 @@ }, "install-path": "../ralouphie/getallheaders" }, + { + "name": "smalot/pdfparser", + "version": "v2.9.0", + "version_normalized": "2.9.0.0", + "source": { + "type": "git", + "url": "https://github.com/smalot/pdfparser.git", + "reference": "6b53144fcb24af77093d4150dd7d0dd571f25761" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/smalot/pdfparser/zipball/6b53144fcb24af77093d4150dd7d0dd571f25761", + "reference": "6b53144fcb24af77093d4150dd7d0dd571f25761", + "shasum": "", + "mirrors": [ + { + "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-iconv": "*", + "ext-zlib": "*", + "php": ">=7.1", + "symfony/polyfill-mbstring": "^1.18" + }, + "time": "2024-03-01T09:51:10+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-0": { + "Smalot\\PdfParser\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0" + ], + "authors": [ + { + "name": "Sebastien MALOT", + "email": "sebastien@malot.fr" + } + ], + "description": "Pdf parser library. Can read and extract information from pdf file.", + "homepage": "https://www.pdfparser.org", + "keywords": [ + "extract", + "parse", + "parser", + "pdf", + "text" + ], + "support": { + "issues": "https://github.com/smalot/pdfparser/issues", + "source": "https://github.com/smalot/pdfparser/tree/v2.9.0" + }, + "install-path": "../smalot/pdfparser" + }, { "name": "spatie/data-transfer-object", "version": "1.14.1", @@ -2564,29 +2517,23 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.6.0", - "version_normalized": "3.6.0.0", + "version": "v2.5.4", + "version_normalized": "2.5.4.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", - "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/605389f2a7e5625f273b53960dc46aeaf9c62918", + "reference": "605389f2a7e5625f273b53960dc46aeaf9c62918", + "shasum": "" }, "require": { - "php": ">=8.1" + "php": ">=7.1" }, - "time": "2024-09-25T14:21:43+00:00", + "time": "2024-09-25T14:11:13+00:00", "type": "library", "extra": { "thanks": { @@ -2594,7 +2541,7 @@ "name": "symfony/contracts" }, "branch-alias": { - "dev-main": "3.6-dev" + "dev-main": "2.5-dev" } }, "installation-source": "dist", @@ -2620,7 +2567,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.4" }, "funding": [ { @@ -2640,44 +2587,38 @@ }, { "name": "symfony/http-foundation", - "version": "v8.0.1", - "version_normalized": "8.0.1.0", + "version": "v5.4.50", + "version_normalized": "5.4.50.0", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "3690740e2e8b19d877f20d4f10b7a489cddf0fe2" + "reference": "1a0706e8b8041046052ea2695eb8aeee04f97609" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/3690740e2e8b19d877f20d4f10b7a489cddf0fe2", - "reference": "3690740e2e8b19d877f20d4f10b7a489cddf0fe2", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/1a0706e8b8041046052ea2695eb8aeee04f97609", + "reference": "1a0706e8b8041046052ea2695eb8aeee04f97609", + "shasum": "" }, "require": { - "php": ">=8.4", - "symfony/polyfill-mbstring": "^1.1" - }, - "conflict": { - "doctrine/dbal": "<4.3" + "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", + "symfony/polyfill-mbstring": "~1.1", + "symfony/polyfill-php80": "^1.16" }, "require-dev": { - "doctrine/dbal": "^4.3", - "predis/predis": "^1.1|^2.0", - "symfony/cache": "^7.4|^8.0", - "symfony/clock": "^7.4|^8.0", - "symfony/dependency-injection": "^7.4|^8.0", - "symfony/expression-language": "^7.4|^8.0", - "symfony/http-kernel": "^7.4|^8.0", - "symfony/mime": "^7.4|^8.0", - "symfony/rate-limiter": "^7.4|^8.0" + "predis/predis": "^1.0|^2.0", + "symfony/cache": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^5.4|^6.0", + "symfony/expression-language": "^4.4|^5.0|^6.0", + "symfony/http-kernel": "^5.4.12|^6.0.12|^6.1.4", + "symfony/mime": "^4.4|^5.0|^6.0", + "symfony/rate-limiter": "^5.2|^6.0" }, - "time": "2025-12-07T11:23:24+00:00", + "suggest": { + "symfony/mime": "To use the file extension guesser" + }, + "time": "2025-11-03T12:58:48+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -2705,7 +2646,7 @@ "description": "Defines an object-oriented layer for the HTTP specification", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/http-foundation/tree/v8.0.1" + "source": "https://github.com/symfony/http-foundation/tree/v5.4.50" }, "funding": [ { @@ -2729,24 +2670,18 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.32.0", - "version_normalized": "1.32.0.0", + "version": "v1.37.0", + "version_normalized": "1.37.0.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", - "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6a21eb99c6973357967f6ce3708cd55a6bec6315", + "reference": "6a21eb99c6973357967f6ce3708cd55a6bec6315", + "shasum": "" }, "require": { "ext-iconv": "*", @@ -2758,7 +2693,7 @@ "suggest": { "ext-mbstring": "For best performance" }, - "time": "2024-12-23T08:48:59+00:00", + "time": "2026-04-10T17:25:58+00:00", "type": "library", "extra": { "thanks": { @@ -2799,7 +2734,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" }, "funding": [ { @@ -2810,6 +2745,10 @@ "url": "https://github.com/fabpot", "type": "github" }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, { "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", "type": "tidelift" @@ -2817,6 +2756,93 @@ ], "install-path": "../symfony/polyfill-mbstring" }, + { + "name": "symfony/polyfill-php80", + "version": "v1.37.0", + "version_normalized": "1.37.0.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php80.git", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411", + "shasum": "" + }, + "require": { + "php": ">=7.2" + }, + "time": "2026-04-10T16:19:22+00:00", + "type": "library", + "extra": { + "thanks": { + "url": "https://github.com/symfony/polyfill", + "name": "symfony/polyfill" + } + }, + "installation-source": "dist", + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ion Bazan", + "email": "ion.bazan@gmail.com" + }, + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "install-path": "../symfony/polyfill-php80" + }, { "name": "tectalic/openai", "version": "v1.6.0", @@ -3003,24 +3029,18 @@ }, { "name": "topthink/think-helper", - "version": "v3.1.11", - "version_normalized": "3.1.11.0", + "version": "v3.1.12", + "version_normalized": "3.1.12.0", "source": { "type": "git", "url": "https://github.com/top-think/think-helper.git", - "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c" + "reference": "fe277121112a8f1c872e169a733ca80bb11c4acb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/top-think/think-helper/zipball/1d6ada9b9f3130046bf6922fe1bd159c8d88a33c", - "reference": "1d6ada9b9f3130046bf6922fe1bd159c8d88a33c", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/top-think/think-helper/zipball/fe277121112a8f1c872e169a733ca80bb11c4acb", + "reference": "fe277121112a8f1c872e169a733ca80bb11c4acb", + "shasum": "" }, "require": { "php": ">=7.1.0" @@ -3028,7 +3048,7 @@ "require-dev": { "phpunit/phpunit": "^9.5" }, - "time": "2025-04-07T06:55:59+00:00", + "time": "2025-12-26T09:58:29+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -3052,30 +3072,24 @@ "description": "The ThinkPHP6 Helper Package", "support": { "issues": "https://github.com/top-think/think-helper/issues", - "source": "https://github.com/top-think/think-helper/tree/v3.1.11" + "source": "https://github.com/top-think/think-helper/tree/v3.1.12" }, "install-path": "../topthink/think-helper" }, { "name": "topthink/think-image", - "version": "v1.0.7", - "version_normalized": "1.0.7.0", + "version": "v1.0.8", + "version_normalized": "1.0.8.0", "source": { "type": "git", "url": "https://github.com/top-think/think-image.git", - "reference": "8586cf47f117481c6d415b20f7dedf62e79d5512" + "reference": "d1d748cbb2fe2f29fca6138cf96cb8b5113892f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/top-think/think-image/zipball/8586cf47f117481c6d415b20f7dedf62e79d5512", - "reference": "8586cf47f117481c6d415b20f7dedf62e79d5512", - "shasum": "", - "mirrors": [ - { - "url": "https://mirrors.aliyun.com/composer/dists/%package%/%reference%.%type%", - "preferred": true - } - ] + "url": "https://api.github.com/repos/top-think/think-image/zipball/d1d748cbb2fe2f29fca6138cf96cb8b5113892f1", + "reference": "d1d748cbb2fe2f29fca6138cf96cb8b5113892f1", + "shasum": "" }, "require": { "ext-gd": "*" @@ -3084,7 +3098,7 @@ "phpunit/phpunit": "4.8.*", "topthink/framework": "^5.0" }, - "time": "2016-09-29T06:05:43+00:00", + "time": "2024-08-07T10:06:35+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -3105,7 +3119,7 @@ "description": "The ThinkPHP5 Image Package", "support": { "issues": "https://github.com/top-think/think-image/issues", - "source": "https://github.com/top-think/think-image/tree/master" + "source": "https://github.com/top-think/think-image/tree/v1.0.8" }, "install-path": "../topthink/think-image" }, diff --git a/vendor/composer/installed.php b/vendor/composer/installed.php index 4cdddf9d..014b19ea 100644 --- a/vendor/composer/installed.php +++ b/vendor/composer/installed.php @@ -3,7 +3,7 @@ 'name' => 'topthink/think', 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'bbd690ca0f68c671ece05e82edc88ee7a68b82ed', + 'reference' => '1d54946fef97376f7c2789af83a1616dd6f7a380', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -11,9 +11,9 @@ ), 'versions' => array( 'apimatic/core' => array( - 'pretty_version' => '0.3.16', - 'version' => '0.3.16.0', - 'reference' => 'ae4ab4ca26a41be41718f33c703d67b7a767c07b', + 'pretty_version' => '0.3.17', + 'version' => '0.3.17.0', + 'reference' => 'a48a583f686ee3786432b976c795a2817ec095b3', 'type' => 'library', 'install_path' => __DIR__ . '/../apimatic/core', 'aliases' => array(), @@ -55,15 +55,6 @@ 'aliases' => array(), 'dev_requirement' => false, ), - 'composer/pcre' => array( - 'pretty_version' => '3.3.2', - 'version' => '3.3.2.0', - 'reference' => 'b2bed4734f0cc156ee1fe9c0da2550420d99a21e', - 'type' => 'library', - 'install_path' => __DIR__ . '/./pcre', - 'aliases' => array(), - 'dev_requirement' => false, - ), 'ezyang/htmlpurifier' => array( 'pretty_version' => 'v4.19.0', 'version' => '4.19.0.0', @@ -92,18 +83,18 @@ 'dev_requirement' => false, ), 'guzzlehttp/psr7' => array( - 'pretty_version' => '2.8.0', - 'version' => '2.8.0.0', - 'reference' => '21dc724a0583619cd1652f673303492272778051', + 'pretty_version' => '2.9.0', + 'version' => '2.9.0.0', + 'reference' => '7d0ed42f28e42d61352a7a79de682e5e67fec884', 'type' => 'library', 'install_path' => __DIR__ . '/../guzzlehttp/psr7', 'aliases' => array(), 'dev_requirement' => false, ), 'maennchen/zipstream-php' => array( - 'pretty_version' => '3.1.2', - 'version' => '3.1.2.0', - 'reference' => 'aeadcf5c412332eb426c0f9b4485f6accba2a99f', + 'pretty_version' => '2.1.0', + 'version' => '2.1.0.0', + 'reference' => 'c4c5803cc1f93df3d2448478ef79394a5981cc58', 'type' => 'library', 'install_path' => __DIR__ . '/../maennchen/zipstream-php', 'aliases' => array(), @@ -127,6 +118,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'myclabs/php-enum' => array( + 'pretty_version' => '1.8.5', + 'version' => '1.8.5.0', + 'reference' => 'e7be26966b7398204a234f8673fdad5ac6277802', + 'type' => 'library', + 'install_path' => __DIR__ . '/../myclabs/php-enum', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'nyholm/psr7' => array( 'pretty_version' => '1.8.2', 'version' => '1.8.2.0', @@ -137,9 +137,9 @@ 'dev_requirement' => false, ), 'paragonie/constant_time_encoding' => array( - 'pretty_version' => 'v3.1.3', - 'version' => '3.1.3.0', - 'reference' => 'd5b01a39b3415c2cd581d3bd3a3575c1ebbd8e77', + 'pretty_version' => 'v2.8.2', + 'version' => '2.8.2.0', + 'reference' => 'e30811f7bc69e4b5b6d5783e712c06c8eabf0226', 'type' => 'library', 'install_path' => __DIR__ . '/../paragonie/constant_time_encoding', 'aliases' => array(), @@ -194,9 +194,9 @@ 'dev_requirement' => false, ), 'php-http/message' => array( - 'pretty_version' => '1.16.1', - 'version' => '1.16.1.0', - 'reference' => '5997f3289332c699fa2545c427826272498a2088', + 'pretty_version' => '1.16.2', + 'version' => '1.16.2.0', + 'reference' => '06dd5e8562f84e641bf929bfe699ee0f5ce8080a', 'type' => 'library', 'install_path' => __DIR__ . '/../php-http/message', 'aliases' => array(), @@ -209,9 +209,9 @@ ), ), 'php-http/multipart-stream-builder' => array( - 'pretty_version' => '1.3.1', - 'version' => '1.3.1.0', - 'reference' => 'ed56da23b95949ae4747378bed8a5b61a2fdae24', + 'pretty_version' => '1.4.2', + 'version' => '1.4.2.0', + 'reference' => '10086e6de6f53489cca5ecc45b6f468604d3460e', 'type' => 'library', 'install_path' => __DIR__ . '/../php-http/multipart-stream-builder', 'aliases' => array(), @@ -227,18 +227,18 @@ 'dev_requirement' => false, ), 'phpmailer/phpmailer' => array( - 'pretty_version' => 'v6.11.1', - 'version' => '6.11.1.0', - 'reference' => 'd9e3b36b47f04b497a0164c5a20f92acb4593284', + 'pretty_version' => 'v6.12.0', + 'version' => '6.12.0.0', + 'reference' => 'd1ac35d784bf9f5e61b424901d5a014967f15b12', 'type' => 'library', 'install_path' => __DIR__ . '/../phpmailer/phpmailer', 'aliases' => array(), 'dev_requirement' => false, ), 'phpoffice/math' => array( - 'pretty_version' => '0.2.0', - 'version' => '0.2.0.0', - 'reference' => 'fc2eb6d1a61b058d5dac77197059db30ee3c8329', + 'pretty_version' => '0.3.0', + 'version' => '0.3.0.0', + 'reference' => 'fc31c8f57a7a81f962cbf389fd89f4d9d06fc99a', 'type' => 'library', 'install_path' => __DIR__ . '/../phpoffice/math', 'aliases' => array(), @@ -254,18 +254,18 @@ 'dev_requirement' => false, ), 'phpoffice/phpspreadsheet' => array( - 'pretty_version' => '1.30.1', - 'version' => '1.30.1.0', - 'reference' => 'fa8257a579ec623473eabfe49731de5967306c4c', + 'pretty_version' => '1.25.2', + 'version' => '1.25.2.0', + 'reference' => 'a317a09e7def49852400a4b3eca4a4b0790ceeb5', 'type' => 'library', 'install_path' => __DIR__ . '/../phpoffice/phpspreadsheet', 'aliases' => array(), 'dev_requirement' => false, ), 'phpoffice/phpword' => array( - 'pretty_version' => '1.3.0', - 'version' => '1.3.0.0', - 'reference' => '8392134ce4b5dba65130ba956231a1602b848b7f', + 'pretty_version' => '1.4.0', + 'version' => '1.4.0.0', + 'reference' => '6d75328229bc93790b37e93741adf70646cea958', 'type' => 'library', 'install_path' => __DIR__ . '/../phpoffice/phpword', 'aliases' => array(), @@ -297,9 +297,9 @@ ), ), 'psr/http-factory' => array( - 'pretty_version' => '1.0.2', - 'version' => '1.0.2.0', - 'reference' => 'e616d01114759c4c489f93b099585439f795fe35', + 'pretty_version' => '1.1.0', + 'version' => '1.1.0.0', + 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-factory', 'aliases' => array(), @@ -313,9 +313,9 @@ ), ), 'psr/http-message' => array( - 'pretty_version' => '2.0', - 'version' => '2.0.0.0', - 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71', + 'pretty_version' => '1.1', + 'version' => '1.1.0.0', + 'reference' => 'cb6ce4845ce34a8ad9e68117c10ee90a29919eba', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-message', 'aliases' => array(), @@ -329,18 +329,18 @@ ), ), 'psr/log' => array( - 'pretty_version' => '3.0.1', - 'version' => '3.0.1.0', - 'reference' => '79dff0b268932c640297f5208d6298f71855c03e', + 'pretty_version' => '1.1.4', + 'version' => '1.1.4.0', + 'reference' => 'd49695b909c3b7628b6289db5479a1c204601f11', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/log', 'aliases' => array(), 'dev_requirement' => false, ), 'psr/simple-cache' => array( - 'pretty_version' => '3.0.0', - 'version' => '3.0.0.0', - 'reference' => '764e0b3939f5ca87cb904f570ef9be2d78a07865', + 'pretty_version' => '1.0.1', + 'version' => '1.0.1.0', + 'reference' => '408d5eafb83c57f6365a3ca330ff23aa4a5fa39b', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/simple-cache', 'aliases' => array(), @@ -355,6 +355,15 @@ 'aliases' => array(), 'dev_requirement' => false, ), + 'smalot/pdfparser' => array( + 'pretty_version' => 'v2.9.0', + 'version' => '2.9.0.0', + 'reference' => '6b53144fcb24af77093d4150dd7d0dd571f25761', + 'type' => 'library', + 'install_path' => __DIR__ . '/../smalot/pdfparser', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'spatie/data-transfer-object' => array( 'pretty_version' => '1.14.1', 'version' => '1.14.1.0', @@ -365,32 +374,41 @@ 'dev_requirement' => false, ), 'symfony/deprecation-contracts' => array( - 'pretty_version' => 'v3.6.0', - 'version' => '3.6.0.0', - 'reference' => '63afe740e99a13ba87ec199bb07bbdee937a5b62', + 'pretty_version' => 'v2.5.4', + 'version' => '2.5.4.0', + 'reference' => '605389f2a7e5625f273b53960dc46aeaf9c62918', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/deprecation-contracts', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/http-foundation' => array( - 'pretty_version' => 'v8.0.1', - 'version' => '8.0.1.0', - 'reference' => '3690740e2e8b19d877f20d4f10b7a489cddf0fe2', + 'pretty_version' => 'v5.4.50', + 'version' => '5.4.50.0', + 'reference' => '1a0706e8b8041046052ea2695eb8aeee04f97609', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/http-foundation', 'aliases' => array(), 'dev_requirement' => false, ), 'symfony/polyfill-mbstring' => array( - 'pretty_version' => 'v1.32.0', - 'version' => '1.32.0.0', - 'reference' => '6d857f4d76bd4b343eac26d6b539585d2bc56493', + 'pretty_version' => 'v1.37.0', + 'version' => '1.37.0.0', + 'reference' => '6a21eb99c6973357967f6ce3708cd55a6bec6315', 'type' => 'library', 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', 'aliases' => array(), 'dev_requirement' => false, ), + 'symfony/polyfill-php80' => array( + 'pretty_version' => 'v1.37.0', + 'version' => '1.37.0.0', + 'reference' => 'dfb55726c3a76ea3b6459fcfda1ec2d80a682411', + 'type' => 'library', + 'install_path' => __DIR__ . '/../symfony/polyfill-php80', + 'aliases' => array(), + 'dev_requirement' => false, + ), 'tectalic/openai' => array( 'pretty_version' => 'v1.6.0', 'version' => '1.6.0.0', @@ -412,7 +430,7 @@ 'topthink/think' => array( 'pretty_version' => 'dev-master', 'version' => 'dev-master', - 'reference' => 'bbd690ca0f68c671ece05e82edc88ee7a68b82ed', + 'reference' => '1d54946fef97376f7c2789af83a1616dd6f7a380', 'type' => 'project', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), @@ -428,18 +446,18 @@ 'dev_requirement' => false, ), 'topthink/think-helper' => array( - 'pretty_version' => 'v3.1.11', - 'version' => '3.1.11.0', - 'reference' => '1d6ada9b9f3130046bf6922fe1bd159c8d88a33c', + 'pretty_version' => 'v3.1.12', + 'version' => '3.1.12.0', + 'reference' => 'fe277121112a8f1c872e169a733ca80bb11c4acb', 'type' => 'library', 'install_path' => __DIR__ . '/../topthink/think-helper', 'aliases' => array(), 'dev_requirement' => false, ), 'topthink/think-image' => array( - 'pretty_version' => 'v1.0.7', - 'version' => '1.0.7.0', - 'reference' => '8586cf47f117481c6d415b20f7dedf62e79d5512', + 'pretty_version' => 'v1.0.8', + 'version' => '1.0.8.0', + 'reference' => 'd1d748cbb2fe2f29fca6138cf96cb8b5113892f1', 'type' => 'library', 'install_path' => __DIR__ . '/../topthink/think-image', 'aliases' => array(), diff --git a/vendor/phpmailer/phpmailer/README.md b/vendor/phpmailer/phpmailer/README.md index 51c97517..646649c9 100644 --- a/vendor/phpmailer/phpmailer/README.md +++ b/vendor/phpmailer/phpmailer/README.md @@ -48,7 +48,7 @@ This software is distributed under the [LGPL 2.1](https://www.gnu.org/licenses/o PHPMailer is available on [Packagist](https://packagist.org/packages/phpmailer/phpmailer) (using semantic versioning), and installation via [Composer](https://getcomposer.org) is the recommended way to install PHPMailer. Just add this line to your `composer.json` file: ```json -"phpmailer/phpmailer": "^6.11.1" +"phpmailer/phpmailer": "^6.12.0" ``` or run diff --git a/vendor/phpmailer/phpmailer/VERSION b/vendor/phpmailer/phpmailer/VERSION index fac714a3..d4e6cb42 100644 --- a/vendor/phpmailer/phpmailer/VERSION +++ b/vendor/phpmailer/phpmailer/VERSION @@ -1 +1 @@ -6.11.1 +6.12.0 diff --git a/vendor/phpmailer/phpmailer/composer.json b/vendor/phpmailer/phpmailer/composer.json index e762d59d..7b008b7c 100644 --- a/vendor/phpmailer/phpmailer/composer.json +++ b/vendor/phpmailer/phpmailer/composer.json @@ -49,15 +49,14 @@ }, "suggest": { "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", - "ext-imap": "Needed to support advanced email address parsing according to RFC822", "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", "ext-openssl": "Needed for secure SMTP sending and DKIM signing", "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", "hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication", "league/oauth2-google": "Needed for Google XOAUTH2 authentication", "psr/log": "For optional PSR-3 debug logging", - "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)", - "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication" + "thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication", + "symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)" }, "autoload": { "psr-4": { @@ -72,7 +71,6 @@ "license": "LGPL-2.1-only", "scripts": { "check": "./vendor/bin/phpcs", - "style": "./vendor/bin/phpcbf", "test": "./vendor/bin/phpunit --no-coverage", "coverage": "./vendor/bin/phpunit", "lint": [ diff --git a/vendor/phpmailer/phpmailer/language/phpmailer.lang-es.php b/vendor/phpmailer/phpmailer/language/phpmailer.lang-es.php index a871824f..4e74bfb7 100644 --- a/vendor/phpmailer/phpmailer/language/phpmailer.lang-es.php +++ b/vendor/phpmailer/phpmailer/language/phpmailer.lang-es.php @@ -9,7 +9,7 @@ */ $PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.'; -$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP ha sido afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.'; +$PHPMAILER_LANG['buggy_php'] = 'Tu versión de PHP está afectada por un bug que puede resultar en mensajes corruptos. Para arreglarlo, cambia a enviar usando SMTP, deshabilita la opción mail.add_x_header en tu php.ini, cambia a MacOS o Linux, o actualiza tu PHP a la versión 7.0.17+ o 7.1.3+.'; $PHPMAILER_LANG['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.'; $PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.'; $PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.'; @@ -18,7 +18,7 @@ $PHPMAILER_LANG['execute'] = 'Imposible ejecutar: '; $PHPMAILER_LANG['extension_missing'] = 'Extensión faltante: '; $PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: '; $PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el archivo: '; -$PHPMAILER_LANG['from_failed'] = 'La siguiente dirección de remitente falló: '; +$PHPMAILER_LANG['from_failed'] = 'La(s) siguiente(s) direcciones de remitente fallaron: '; $PHPMAILER_LANG['instantiate'] = 'Imposible crear una instancia de la función Mail.'; $PHPMAILER_LANG['invalid_address'] = 'Imposible enviar: dirección de email inválido: '; $PHPMAILER_LANG['invalid_header'] = 'Nombre o valor de encabezado no válido'; @@ -34,5 +34,3 @@ $PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falló.'; $PHPMAILER_LANG['smtp_detail'] = 'Detalle: '; $PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: '; $PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: '; -$PHPMAILER_LANG['imap_recommended'] = 'No se recomienda usar el analizador de direcciones simplificado. Instala la extensión IMAP de PHP para un análisis RFC822 más completo.'; -$PHPMAILER_LANG['deprecated_argument'] = 'El argumento $useimap ha quedado obsoleto'; diff --git a/vendor/phpmailer/phpmailer/src/PHPMailer.php b/vendor/phpmailer/phpmailer/src/PHPMailer.php index 0a8711f4..4b3f34c8 100644 --- a/vendor/phpmailer/phpmailer/src/PHPMailer.php +++ b/vendor/phpmailer/phpmailer/src/PHPMailer.php @@ -561,9 +561,9 @@ class PHPMailer * string $body the email body * string $from email address of sender * string $extra extra information of possible use - * 'smtp_transaction_id' => last smtp transaction id + * "smtp_transaction_id' => last smtp transaction id * - * @var callable|callable-string + * @var string */ public $action_function = ''; @@ -711,7 +711,7 @@ class PHPMailer * * @var array */ - protected static $language = []; + protected $language = []; /** * The number of errors encountered. @@ -768,7 +768,7 @@ class PHPMailer * * @var string */ - const VERSION = '6.11.1'; + const VERSION = '6.12.0'; /** * Error severity: message only, continue processing. @@ -1102,7 +1102,7 @@ class PHPMailer //At-sign is missing. $error_message = sprintf( '%s (%s): %s', - self::lang('invalid_address'), + $this->lang('invalid_address'), $kind, $address ); @@ -1187,7 +1187,7 @@ class PHPMailer if (!in_array($kind, ['to', 'cc', 'bcc', 'Reply-To'])) { $error_message = sprintf( '%s: %s', - self::lang('Invalid recipient kind'), + $this->lang('Invalid recipient kind'), $kind ); $this->setError($error_message); @@ -1201,7 +1201,7 @@ class PHPMailer if (!static::validateAddress($address)) { $error_message = sprintf( '%s (%s): %s', - self::lang('invalid_address'), + $this->lang('invalid_address'), $kind, $address ); @@ -1220,16 +1220,12 @@ class PHPMailer return true; } - } else { - foreach ($this->ReplyTo as $replyTo) { - if (0 === strcasecmp($replyTo[0], $address)) { - return false; - } - } - $this->ReplyTo[] = [$address, $name]; + } elseif (!array_key_exists(strtolower($address), $this->ReplyTo)) { + $this->ReplyTo[strtolower($address)] = [$address, $name]; return true; } + return false; } @@ -1242,18 +1238,15 @@ class PHPMailer * @see https://www.andrew.cmu.edu/user/agreen1/testing/mrbs/web/Mail/RFC822.php A more careful implementation * * @param string $addrstr The address list string - * @param null $useimap Deprecated argument since 6.11.0. + * @param bool $useimap Whether to use the IMAP extension to parse the list * @param string $charset The charset to use when decoding the address list string. * * @return array */ - public static function parseAddresses($addrstr, $useimap = null, $charset = self::CHARSET_ISO88591) + public static function parseAddresses($addrstr, $useimap = true, $charset = self::CHARSET_ISO88591) { - if ($useimap !== null) { - trigger_error(self::lang('deprecated_argument'), E_USER_DEPRECATED); - } $addresses = []; - if (function_exists('imap_rfc822_parse_adrlist')) { + if ($useimap && function_exists('imap_rfc822_parse_adrlist')) { //Use this built-in parser if it's available $list = imap_rfc822_parse_adrlist($addrstr, ''); // Clear any potential IMAP errors to get rid of notices being thrown at end of script. @@ -1263,13 +1256,20 @@ class PHPMailer '.SYNTAX-ERROR.' !== $address->host && static::validateAddress($address->mailbox . '@' . $address->host) ) { - //Decode the name part if it's present and maybe encoded + //Decode the name part if it's present and encoded if ( - property_exists($address, 'personal') - && is_string($address->personal) - && $address->personal !== '' + property_exists($address, 'personal') && + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled + defined('MB_CASE_UPPER') && + preg_match('/^=\?.*\?=$/s', $address->personal) ) { - $address->personal = static::decodeHeader($address->personal, $charset); + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $address->personal = str_replace('_', '=20', $address->personal); + //Decode the name + $address->personal = mb_decode_mimeheader($address->personal); + mb_internal_encoding($origCharset); } $addresses[] = [ @@ -1280,51 +1280,40 @@ class PHPMailer } } else { //Use this simpler parser - $addresses = static::parseSimplerAddresses($addrstr, $charset); - } - - return $addresses; - } - - /** - * Parse a string containing one or more RFC822-style comma-separated email addresses - * with the form "display name

" into an array of name/address pairs. - * Uses a simpler parser that does not require the IMAP extension but doesnt support - * the full RFC822 spec. For full RFC822 support, use the PHP IMAP extension. - * - * @param string $addrstr The address list string - * @param string $charset The charset to use when decoding the address list string. - * - * @return array - */ - protected static function parseSimplerAddresses($addrstr, $charset) - { - // Emit a runtime notice to recommend using the IMAP extension for full RFC822 parsing - trigger_error(self::lang('imap_recommended'), E_USER_NOTICE); - - $addresses = []; - $list = explode(',', $addrstr); - foreach ($list as $address) { - $address = trim($address); - //Is there a separate name part? - if (strpos($address, '<') === false) { - //No separate name, just use the whole thing - if (static::validateAddress($address)) { - $addresses[] = [ - 'name' => '', - 'address' => $address, - ]; - } - } else { - $parsed = static::parseEmailString($address); - $email = $parsed['email']; - if (static::validateAddress($email)) { - $name = static::decodeHeader($parsed['name'], $charset); - $addresses[] = [ - //Remove any surrounding quotes and spaces from the name - 'name' => trim($name, '\'" '), - 'address' => $email, - ]; + $list = explode(',', $addrstr); + foreach ($list as $address) { + $address = trim($address); + //Is there a separate name part? + if (strpos($address, '<') === false) { + //No separate name, just use the whole thing + if (static::validateAddress($address)) { + $addresses[] = [ + 'name' => '', + 'address' => $address, + ]; + } + } else { + list($name, $email) = explode('<', $address); + $email = trim(str_replace('>', '', $email)); + $name = trim($name); + if (static::validateAddress($email)) { + //Check for a Mbstring constant rather than using extension_loaded, which is sometimes disabled + //If this name is encoded, decode it + if (defined('MB_CASE_UPPER') && preg_match('/^=\?.*\?=$/s', $name)) { + $origCharset = mb_internal_encoding(); + mb_internal_encoding($charset); + //Undo any RFC2047-encoded spaces-as-underscores + $name = str_replace('_', '=20', $name); + //Decode the name + $name = mb_decode_mimeheader($name); + mb_internal_encoding($origCharset); + } + $addresses[] = [ + //Remove any surrounding quotes and spaces from the name + 'name' => trim($name, '\'" '), + 'address' => $email, + ]; + } } } } @@ -1332,42 +1321,6 @@ class PHPMailer return $addresses; } - /** - * Parse a string containing an email address with an optional name - * and divide it into a name and email address. - * - * @param string $input The email with name. - * - * @return array{name: string, email: string} - */ - private static function parseEmailString($input) - { - $input = trim((string)$input); - - if ($input === '') { - return ['name' => '', 'email' => '']; - } - - $pattern = '/^\s*(?:(?:"([^"]*)"|\'([^\']*)\'|([^<]*?))\s*)?<\s*([^>]+)\s*>\s*$/'; - if (preg_match($pattern, $input, $matches)) { - $name = ''; - // Double quotes including special scenarios. - if (isset($matches[1]) && $matches[1] !== '') { - $name = $matches[1]; - // Single quotes including special scenarios. - } elseif (isset($matches[2]) && $matches[2] !== '') { - $name = $matches[2]; - // Simplest scenario, name and email are in the format "Name ". - } elseif (isset($matches[3])) { - $name = trim($matches[3]); - } - - return ['name' => $name, 'email' => trim($matches[4])]; - } - - return ['name' => '', 'email' => $input]; - } - /** * Set the From and FromName properties. * @@ -1381,10 +1334,6 @@ class PHPMailer */ public function setFrom($address, $name = '', $auto = true) { - if (is_null($name)) { - //Helps avoid a deprecation warning in the preg_replace() below - $name = ''; - } $address = trim((string)$address); $name = trim(preg_replace('/[\r\n]+/', '', $name)); //Strip breaks and trim //Don't validate now addresses with IDN. Will be done in send(). @@ -1396,7 +1345,7 @@ class PHPMailer ) { $error_message = sprintf( '%s (From): %s', - self::lang('invalid_address'), + $this->lang('invalid_address'), $address ); $this->setError($error_message); @@ -1652,7 +1601,7 @@ class PHPMailer && ini_get('mail.add_x_header') === '1' && stripos(PHP_OS, 'WIN') === 0 ) { - trigger_error(self::lang('buggy_php'), E_USER_WARNING); + trigger_error($this->lang('buggy_php'), E_USER_WARNING); } try { @@ -1682,7 +1631,7 @@ class PHPMailer call_user_func_array([$this, 'addAnAddress'], $params); } if (count($this->to) + count($this->cc) + count($this->bcc) < 1) { - throw new Exception(self::lang('provide_address'), self::STOP_CRITICAL); + throw new Exception($this->lang('provide_address'), self::STOP_CRITICAL); } //Validate From, Sender, and ConfirmReadingTo addresses @@ -1699,7 +1648,7 @@ class PHPMailer if (!static::validateAddress($this->{$address_kind})) { $error_message = sprintf( '%s (%s): %s', - self::lang('invalid_address'), + $this->lang('invalid_address'), $address_kind, $this->{$address_kind} ); @@ -1721,7 +1670,7 @@ class PHPMailer $this->setMessageType(); //Refuse to send an empty message unless we are specifically allowing it if (!$this->AllowEmpty && empty($this->Body)) { - throw new Exception(self::lang('empty_message'), self::STOP_CRITICAL); + throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); } //Trim subject consistently @@ -1860,10 +1809,8 @@ class PHPMailer } else { $sendmailFmt = '%s -oi -f%s -t'; } - } elseif ($this->Mailer === 'qmail') { - $sendmailFmt = '%s'; } else { - //Allow sendmail to choose a default envelope sender. It may + //allow sendmail to choose a default envelope sender. It may //seem preferable to force it to use the From header as with //SMTP, but that introduces new problems (see //), and @@ -1881,35 +1828,33 @@ class PHPMailer foreach ($this->SingleToArray as $toAddr) { $mail = @popen($sendmail, 'w'); if (!$mail) { - throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } $this->edebug("To: {$toAddr}"); fwrite($mail, 'To: ' . $toAddr . "\n"); fwrite($mail, $header); fwrite($mail, $body); $result = pclose($mail); - $addrinfo = static::parseAddresses($toAddr, null, $this->CharSet); - foreach ($addrinfo as $addr) { - $this->doCallback( - ($result === 0), - [[$addr['address'], $addr['name']]], - $this->cc, - $this->bcc, - $this->Subject, - $body, - $this->From, - [] - ); - } + $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); + $this->doCallback( + ($result === 0), + [[$addrinfo['address'], $addrinfo['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { - throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } } else { $mail = @popen($sendmail, 'w'); if (!$mail) { - throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } fwrite($mail, $header); fwrite($mail, $body); @@ -1926,7 +1871,7 @@ class PHPMailer ); $this->edebug("Result: " . ($result === 0 ? 'true' : 'false')); if (0 !== $result) { - throw new Exception(self::lang('execute') . $this->Sendmail, self::STOP_CRITICAL); + throw new Exception($this->lang('execute') . $this->Sendmail, self::STOP_CRITICAL); } } @@ -2065,19 +2010,17 @@ class PHPMailer if ($this->SingleTo && count($toArr) > 1) { foreach ($toArr as $toAddr) { $result = $this->mailPassthru($toAddr, $this->Subject, $body, $header, $params); - $addrinfo = static::parseAddresses($toAddr, null, $this->CharSet); - foreach ($addrinfo as $addr) { - $this->doCallback( - $result, - [[$addr['address'], $addr['name']]], - $this->cc, - $this->bcc, - $this->Subject, - $body, - $this->From, - [] - ); - } + $addrinfo = static::parseAddresses($toAddr, true, $this->CharSet); + $this->doCallback( + $result, + [[$addrinfo['address'], $addrinfo['name']]], + $this->cc, + $this->bcc, + $this->Subject, + $body, + $this->From, + [] + ); } } else { $result = $this->mailPassthru($to, $this->Subject, $body, $header, $params); @@ -2087,7 +2030,7 @@ class PHPMailer ini_set('sendmail_from', $old_from); } if (!$result) { - throw new Exception(self::lang('instantiate'), self::STOP_CRITICAL); + throw new Exception($this->lang('instantiate'), self::STOP_CRITICAL); } return true; @@ -2173,12 +2116,12 @@ class PHPMailer $header = static::stripTrailingWSP($header) . static::$LE . static::$LE; $bad_rcpt = []; if (!$this->smtpConnect($this->SMTPOptions)) { - throw new Exception(self::lang('smtp_connect_failed'), self::STOP_CRITICAL); + throw new Exception($this->lang('smtp_connect_failed'), self::STOP_CRITICAL); } //If we have recipient addresses that need Unicode support, //but the server doesn't support it, stop here if ($this->UseSMTPUTF8 && !$this->smtp->getServerExt('SMTPUTF8')) { - throw new Exception(self::lang('no_smtputf8'), self::STOP_CRITICAL); + throw new Exception($this->lang('no_smtputf8'), self::STOP_CRITICAL); } //Sender already validated in preSend() if ('' === $this->Sender) { @@ -2190,7 +2133,7 @@ class PHPMailer $this->smtp->xclient($this->SMTPXClient); } if (!$this->smtp->mail($smtp_from)) { - $this->setError(self::lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); + $this->setError($this->lang('from_failed') . $smtp_from . ' : ' . implode(',', $this->smtp->getError())); throw new Exception($this->ErrorInfo, self::STOP_CRITICAL); } @@ -2212,7 +2155,7 @@ class PHPMailer //Only send the DATA command if we have viable recipients if ((count($this->all_recipients) > count($bad_rcpt)) && !$this->smtp->data($header . $body)) { - throw new Exception(self::lang('data_not_accepted'), self::STOP_CRITICAL); + throw new Exception($this->lang('data_not_accepted'), self::STOP_CRITICAL); } $smtp_transaction_id = $this->smtp->getLastTransactionID(); @@ -2243,7 +2186,7 @@ class PHPMailer foreach ($bad_rcpt as $bad) { $errstr .= $bad['to'] . ': ' . $bad['error']; } - throw new Exception(self::lang('recipients_failed') . $errstr, self::STOP_CONTINUE); + throw new Exception($this->lang('recipients_failed') . $errstr, self::STOP_CONTINUE); } return true; @@ -2297,7 +2240,7 @@ class PHPMailer $hostinfo ) ) { - $this->edebug(self::lang('invalid_hostentry') . ' ' . trim($hostentry)); + $this->edebug($this->lang('invalid_hostentry') . ' ' . trim($hostentry)); //Not a valid host entry continue; } @@ -2309,7 +2252,7 @@ class PHPMailer //Check the host name is a valid name or IP address before trying to use it if (!static::isValidHost($hostinfo[2])) { - $this->edebug(self::lang('invalid_host') . ' ' . $hostinfo[2]); + $this->edebug($this->lang('invalid_host') . ' ' . $hostinfo[2]); continue; } $prefix = ''; @@ -2329,7 +2272,7 @@ class PHPMailer if (static::ENCRYPTION_STARTTLS === $secure || static::ENCRYPTION_SMTPS === $secure) { //Check for an OpenSSL constant rather than using extension_loaded, which is sometimes disabled if (!$sslext) { - throw new Exception(self::lang('extension_missing') . 'openssl', self::STOP_CRITICAL); + throw new Exception($this->lang('extension_missing') . 'openssl', self::STOP_CRITICAL); } } $host = $hostinfo[2]; @@ -2381,7 +2324,7 @@ class PHPMailer $this->oauth ) ) { - throw new Exception(self::lang('authenticate')); + throw new Exception($this->lang('authenticate')); } return true; @@ -2431,7 +2374,7 @@ class PHPMailer * * @return bool Returns true if the requested language was loaded, false otherwise. */ - public static function setLanguage($langcode = 'en', $lang_path = '') + public function setLanguage($langcode = 'en', $lang_path = '') { //Backwards compatibility for renamed language codes $renamed_langcodes = [ @@ -2480,9 +2423,6 @@ class PHPMailer 'smtp_error' => 'SMTP server error: ', 'variable_set' => 'Cannot set or reset variable: ', 'no_smtputf8' => 'Server does not support SMTPUTF8 needed to send to Unicode addresses', - 'imap_recommended' => 'Using simplified address parser is not recommended. ' . - 'Install the PHP IMAP extension for full RFC822 parsing.', - 'deprecated_argument' => 'Argument $useimap is deprecated', ]; if (empty($lang_path)) { //Calculate an absolute path so it can work if CWD is not here @@ -2549,7 +2489,7 @@ class PHPMailer } } } - self::$language = $PHPMAILER_LANG; + $this->language = $PHPMAILER_LANG; return $foundlang; //Returns false if language not found } @@ -2561,11 +2501,11 @@ class PHPMailer */ public function getTranslations() { - if (empty(self::$language)) { - self::setLanguage(); // Set the default language. + if (empty($this->language)) { + $this->setLanguage(); // Set the default language. } - return self::$language; + return $this->language; } /** @@ -2988,6 +2928,10 @@ class PHPMailer //Create unique IDs and preset boundaries $this->setBoundaries(); + if ($this->sign_key_file) { + $body .= $this->getMailMIME() . static::$LE; + } + $this->setWordWrap(); $bodyEncoding = $this->Encoding; @@ -3019,12 +2963,6 @@ class PHPMailer if (static::ENCODING_BASE64 !== $altBodyEncoding && static::hasLineLongerThanMax($this->AltBody)) { $altBodyEncoding = static::ENCODING_QUOTED_PRINTABLE; } - - if ($this->sign_key_file) { - $this->Encoding = $bodyEncoding; - $body .= $this->getMailMIME() . static::$LE; - } - //Use this as a preamble in all multipart message types $mimepre = ''; switch ($this->message_type) { @@ -3206,12 +3144,12 @@ class PHPMailer if ($this->isError()) { $body = ''; if ($this->exceptions) { - throw new Exception(self::lang('empty_message'), self::STOP_CRITICAL); + throw new Exception($this->lang('empty_message'), self::STOP_CRITICAL); } } elseif ($this->sign_key_file) { try { if (!defined('PKCS7_TEXT')) { - throw new Exception(self::lang('extension_missing') . 'openssl'); + throw new Exception($this->lang('extension_missing') . 'openssl'); } $file = tempnam(sys_get_temp_dir(), 'srcsign'); @@ -3249,7 +3187,7 @@ class PHPMailer $body = $parts[1]; } else { @unlink($signed); - throw new Exception(self::lang('signing') . openssl_error_string()); + throw new Exception($this->lang('signing') . openssl_error_string()); } } catch (Exception $exc) { $body = ''; @@ -3394,7 +3332,7 @@ class PHPMailer ) { try { if (!static::fileIsAccessible($path)) { - throw new Exception(self::lang('file_access') . $path, self::STOP_CONTINUE); + throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); } //If a MIME type is not specified, try to work it out from the file name @@ -3407,7 +3345,7 @@ class PHPMailer $name = $filename; } if (!$this->validateEncoding($encoding)) { - throw new Exception(self::lang('encoding') . $encoding); + throw new Exception($this->lang('encoding') . $encoding); } $this->attachment[] = [ @@ -3568,11 +3506,11 @@ class PHPMailer { try { if (!static::fileIsAccessible($path)) { - throw new Exception(self::lang('file_open') . $path, self::STOP_CONTINUE); + throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); } $file_buffer = file_get_contents($path); if (false === $file_buffer) { - throw new Exception(self::lang('file_open') . $path, self::STOP_CONTINUE); + throw new Exception($this->lang('file_open') . $path, self::STOP_CONTINUE); } $file_buffer = $this->encodeString($file_buffer, $encoding); @@ -3625,9 +3563,9 @@ class PHPMailer $encoded = $this->encodeQP($str); break; default: - $this->setError(self::lang('encoding') . $encoding); + $this->setError($this->lang('encoding') . $encoding); if ($this->exceptions) { - throw new Exception(self::lang('encoding') . $encoding); + throw new Exception($this->lang('encoding') . $encoding); } break; } @@ -3733,42 +3671,6 @@ class PHPMailer return trim(static::normalizeBreaks($encoded)); } - /** - * Decode an RFC2047-encoded header value - * Attempts multiple strategies so it works even when the mbstring extension is disabled. - * - * @param string $value The header value to decode - * @param string $charset The target charset to convert to, defaults to ISO-8859-1 for BC - * - * @return string The decoded header value - */ - public static function decodeHeader($value, $charset = self::CHARSET_ISO88591) - { - if (!is_string($value) || $value === '') { - return ''; - } - // Detect the presence of any RFC2047 encoded-words - $hasEncodedWord = (bool) preg_match('/=\?.*\?=/s', $value); - if ($hasEncodedWord && defined('MB_CASE_UPPER')) { - $origCharset = mb_internal_encoding(); - // Always decode to UTF-8 to provide a consistent, modern output encoding. - mb_internal_encoding($charset); - if (PHP_VERSION_ID < 80300) { - // Undo any RFC2047-encoded spaces-as-underscores. - $value = str_replace('_', '=20', $value); - } else { - // PHP 8.3+ already interprets underscores as spaces. Remove additional - // linear whitespace between adjacent encoded words to avoid double spacing. - $value = preg_replace('/(\?=)\s+(=\?)/', '$1$2', $value); - } - // Decode the header value - $value = mb_decode_mimeheader($value); - mb_internal_encoding($origCharset); - } - - return $value; - } - /** * Check if a string contains multi-byte characters. * @@ -3938,7 +3840,7 @@ class PHPMailer } if (!$this->validateEncoding($encoding)) { - throw new Exception(self::lang('encoding') . $encoding); + throw new Exception($this->lang('encoding') . $encoding); } //Append to $attachment array @@ -3997,7 +3899,7 @@ class PHPMailer ) { try { if (!static::fileIsAccessible($path)) { - throw new Exception(self::lang('file_access') . $path, self::STOP_CONTINUE); + throw new Exception($this->lang('file_access') . $path, self::STOP_CONTINUE); } //If a MIME type is not specified, try to work it out from the file name @@ -4006,7 +3908,7 @@ class PHPMailer } if (!$this->validateEncoding($encoding)) { - throw new Exception(self::lang('encoding') . $encoding); + throw new Exception($this->lang('encoding') . $encoding); } $filename = (string) static::mb_pathinfo($path, PATHINFO_BASENAME); @@ -4072,7 +3974,7 @@ class PHPMailer } if (!$this->validateEncoding($encoding)) { - throw new Exception(self::lang('encoding') . $encoding); + throw new Exception($this->lang('encoding') . $encoding); } //Append to $attachment array @@ -4329,7 +4231,7 @@ class PHPMailer } if (strpbrk($name . $value, "\r\n") !== false) { if ($this->exceptions) { - throw new Exception(self::lang('invalid_header')); + throw new Exception($this->lang('invalid_header')); } return false; @@ -4353,15 +4255,15 @@ class PHPMailer if ('smtp' === $this->Mailer && null !== $this->smtp) { $lasterror = $this->smtp->getError(); if (!empty($lasterror['error'])) { - $msg .= ' ' . self::lang('smtp_error') . $lasterror['error']; + $msg .= ' ' . $this->lang('smtp_error') . $lasterror['error']; if (!empty($lasterror['detail'])) { - $msg .= ' ' . self::lang('smtp_detail') . $lasterror['detail']; + $msg .= ' ' . $this->lang('smtp_detail') . $lasterror['detail']; } if (!empty($lasterror['smtp_code'])) { - $msg .= ' ' . self::lang('smtp_code') . $lasterror['smtp_code']; + $msg .= ' ' . $this->lang('smtp_code') . $lasterror['smtp_code']; } if (!empty($lasterror['smtp_code_ex'])) { - $msg .= ' ' . self::lang('smtp_code_ex') . $lasterror['smtp_code_ex']; + $msg .= ' ' . $this->lang('smtp_code_ex') . $lasterror['smtp_code_ex']; } } } @@ -4486,21 +4388,21 @@ class PHPMailer * * @return string */ - protected static function lang($key) + protected function lang($key) { - if (count(self::$language) < 1) { - self::setLanguage(); //Set the default language + if (count($this->language) < 1) { + $this->setLanguage(); //Set the default language } - if (array_key_exists($key, self::$language)) { + if (array_key_exists($key, $this->language)) { if ('smtp_connect_failed' === $key) { //Include a link to troubleshooting docs on SMTP connection failure. //This is by far the biggest cause of support questions //but it's usually not PHPMailer's fault. - return self::$language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; + return $this->language[$key] . ' https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting'; } - return self::$language[$key]; + return $this->language[$key]; } //Return the key as a fallback @@ -4515,7 +4417,7 @@ class PHPMailer */ private function getSmtpErrorMessage($base_key) { - $message = self::lang($base_key); + $message = $this->lang($base_key); $error = $this->smtp->getError(); if (!empty($error['error'])) { $message .= ' ' . $error['error']; @@ -4559,7 +4461,7 @@ class PHPMailer //Ensure name is not empty, and that neither name nor value contain line breaks if (empty($name) || strpbrk($name . $value, "\r\n") !== false) { if ($this->exceptions) { - throw new Exception(self::lang('invalid_header')); + throw new Exception($this->lang('invalid_header')); } return false; @@ -4952,7 +4854,7 @@ class PHPMailer return true; } - $this->setError(self::lang('variable_set') . $name); + $this->setError($this->lang('variable_set') . $name); return false; } @@ -5090,7 +4992,7 @@ class PHPMailer { if (!defined('PKCS7_TEXT')) { if ($this->exceptions) { - throw new Exception(self::lang('extension_missing') . 'openssl'); + throw new Exception($this->lang('extension_missing') . 'openssl'); } return ''; diff --git a/vendor/phpmailer/phpmailer/src/POP3.php b/vendor/phpmailer/phpmailer/src/POP3.php index 2c2cf789..4d3e8db2 100644 --- a/vendor/phpmailer/phpmailer/src/POP3.php +++ b/vendor/phpmailer/phpmailer/src/POP3.php @@ -46,7 +46,7 @@ class POP3 * * @var string */ - const VERSION = '6.11.1'; + const VERSION = '6.12.0'; /** * Default POP3 port number. diff --git a/vendor/phpmailer/phpmailer/src/SMTP.php b/vendor/phpmailer/phpmailer/src/SMTP.php index 3772c94a..d8749c5b 100644 --- a/vendor/phpmailer/phpmailer/src/SMTP.php +++ b/vendor/phpmailer/phpmailer/src/SMTP.php @@ -35,7 +35,7 @@ class SMTP * * @var string */ - const VERSION = '6.11.1'; + const VERSION = '6.12.0'; /** * SMTP line break constant. @@ -205,7 +205,6 @@ class SMTP 'Haraka' => '/[\d]{3} Message Queued \((.*)\)/', 'ZoneMTA' => '/[\d]{3} Message queued as (.*)/', 'Mailjet' => '/[\d]{3} OK queued as (.*)/', - 'Gsmtp' => '/[\d]{3} 2\.0\.0 OK (.*) - gsmtp/', ]; /** @@ -634,41 +633,10 @@ class SMTP return false; } $oauth = $OAuth->getOauth64(); - /* - * An SMTP command line can have a maximum length of 512 bytes, including the command name, - * so the base64-encoded OAUTH token has a maximum length of: - * 512 - 13 (AUTH XOAUTH2) - 2 (CRLF) = 497 bytes - * If the token is longer than that, the command and the token must be sent separately as described in - * https://www.rfc-editor.org/rfc/rfc4954#section-4 - */ - if ($oauth === '') { - //Sending an empty auth token is legitimate, but it must be encoded as '=' - //to indicate it's not a 2-part command - if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 =', 235)) { - return false; - } - } elseif (strlen($oauth) <= 497) { - //Authenticate using a token in the initial-response part - if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { - return false; - } - } else { - //The token is too long, so we need to send it in two parts. - //Send the auth command without a token and expect a 334 - if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2', 334)) { - return false; - } - //Send the token - if (!$this->sendCommand('OAuth TOKEN', $oauth, [235, 334])) { - return false; - } - //If the server answers with 334, send an empty line and wait for a 235 - if ( - substr($this->last_reply, 0, 3) === '334' - && $this->sendCommand('AUTH End', '', 235) - ) { - return false; - } + + //Start authentication + if (!$this->sendCommand('AUTH', 'AUTH XOAUTH2 ' . $oauth, 235)) { + return false; } break; default: @@ -1341,16 +1309,7 @@ class SMTP //stream_select returns false when the `select` system call is interrupted //by an incoming signal, try the select again - if ( - stripos($message, 'interrupted system call') !== false || - ( - // on applications with a different locale than english, the message above is not found because - // it's translated. So we also check for the SOCKET_EINTR constant which is defined under - // Windows and UNIX-like platforms (if available on the platform). - defined('SOCKET_EINTR') && - stripos($message, 'stream_select(): Unable to select [' . SOCKET_EINTR . ']') !== false - ) - ) { + if (stripos($message, 'interrupted system call') !== false) { $this->edebug( 'SMTP -> get_lines(): retrying stream_select', self::DEBUG_LOWLEVEL diff --git a/vendor/phpoffice/math/.github/workflows/php.yml b/vendor/phpoffice/math/.github/workflows/php.yml index df807baa..5390049e 100644 --- a/vendor/phpoffice/math/.github/workflows/php.yml +++ b/vendor/phpoffice/math/.github/workflows/php.yml @@ -45,6 +45,7 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' steps: - name: Setup PHP uses: shivammathur/setup-php@v2 @@ -75,6 +76,7 @@ jobs: - '8.1' - '8.2' - '8.3' + - '8.4' steps: - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/vendor/phpoffice/math/docs/index.md b/vendor/phpoffice/math/docs/index.md index 340b0dcc..80b4333c 100644 --- a/vendor/phpoffice/math/docs/index.md +++ b/vendor/phpoffice/math/docs/index.md @@ -48,7 +48,7 @@ Math is an open source project licensed under the terms of [MIT](https://github. | **Simple** | Fraction | :material-check: | :material-check: | | | Superscript | :material-check: | | | **Architectural** | Row | :material-check: | :material-check: | -| | Semantics | | | +| | Semantics | :material-check: | | ## Contributing diff --git a/vendor/phpoffice/math/mkdocs.yml b/vendor/phpoffice/math/mkdocs.yml index 972d2741..198c5fe5 100644 --- a/vendor/phpoffice/math/mkdocs.yml +++ b/vendor/phpoffice/math/mkdocs.yml @@ -57,7 +57,9 @@ nav: - Writers: 'usage/writers.md' - Credits: 'credits.md' - Releases: - - '0.1.0 (WIP)': 'changes/0.1.0.md' + - '0.3.0 (WIP)': 'changes/0.3.0.md' + - '0.2.0': 'changes/0.2.0.md' + - '0.1.0': 'changes/0.1.0.md' - Developers: - 'Coveralls': 'https://coveralls.io/github/PHPOffice/Math' - 'Code Coverage': 'coverage/index.html' diff --git a/vendor/phpoffice/math/src/Math/Reader/MathML.php b/vendor/phpoffice/math/src/Math/Reader/MathML.php index 58d93935..cf1edb8d 100644 --- a/vendor/phpoffice/math/src/Math/Reader/MathML.php +++ b/vendor/phpoffice/math/src/Math/Reader/MathML.php @@ -10,6 +10,7 @@ use PhpOffice\Math\Element; use PhpOffice\Math\Exception\InvalidInputException; use PhpOffice\Math\Exception\NotImplementedException; use PhpOffice\Math\Math; +use PhpOffice\Math\Reader\Security\XmlScanner; class MathML implements ReaderInterface { @@ -22,8 +23,17 @@ class MathML implements ReaderInterface /** @var DOMXPath */ private $xpath; + /** @var XmlScanner */ + private $xmlScanner; + + public function __construct() + { + $this->xmlScanner = XmlScanner::getInstance(); + } + public function read(string $content): ?Math { + $content = $this->xmlScanner->scan($content); $content = str_replace( [ '⁢', @@ -35,7 +45,7 @@ class MathML implements ReaderInterface ); $this->dom = new DOMDocument(); - $this->dom->loadXML($content, LIBXML_DTDLOAD); + $this->dom->loadXML($content); $this->math = new Math(); $this->parseNode(null, $this->math); diff --git a/vendor/phpoffice/math/src/Math/Writer/MathML.php b/vendor/phpoffice/math/src/Math/Writer/MathML.php index d8905f00..34a20dc3 100644 --- a/vendor/phpoffice/math/src/Math/Writer/MathML.php +++ b/vendor/phpoffice/math/src/Math/Writer/MathML.php @@ -40,6 +40,26 @@ class MathML implements WriterInterface { $tagName = $this->getElementTagName($element); + // Element\AbstractGroupElement + if ($element instanceof Element\Semantics) { + $this->output->startElement($tagName); + // Write elements + foreach ($element->getElements() as $childElement) { + $this->writeElementItem($childElement); + } + + // Write annotations + foreach ($element->getAnnotations() as $encoding => $annotation) { + $this->output->startElement('annotation'); + $this->output->writeAttribute('encoding', $encoding); + $this->output->text($annotation); + $this->output->endElement(); + } + $this->output->endElement(); + + return; + } + // Element\AbstractGroupElement if ($element instanceof Element\AbstractGroupElement) { $this->output->startElement($tagName); @@ -121,6 +141,9 @@ class MathML implements WriterInterface if ($element instanceof Element\Operator) { return 'mo'; } + if ($element instanceof Element\Semantics) { + return 'semantics'; + } throw new NotImplementedException(sprintf( '%s : The element of the class `%s` has no tag name', diff --git a/vendor/phpoffice/math/tests/Math/Reader/MathMLTest.php b/vendor/phpoffice/math/tests/Math/Reader/MathMLTest.php index db9b174a..a17c179c 100644 --- a/vendor/phpoffice/math/tests/Math/Reader/MathMLTest.php +++ b/vendor/phpoffice/math/tests/Math/Reader/MathMLTest.php @@ -7,6 +7,7 @@ namespace Tests\PhpOffice\Math\Reader; use PhpOffice\Math\Element; use PhpOffice\Math\Exception\InvalidInputException; use PhpOffice\Math\Exception\NotImplementedException; +use PhpOffice\Math\Exception\SecurityException; use PhpOffice\Math\Math; use PhpOffice\Math\Reader\MathML; use PHPUnit\Framework\TestCase; @@ -294,4 +295,15 @@ class MathMLTest extends TestCase $reader = new MathML(); $math = $reader->read($content); } + + public function testReadSecurity(): void + { + $this->expectException(SecurityException::class); + $this->expectExceptionMessage('Detected use of ENTITY in XML, loading aborted to prevent XXE/XEE attacks'); + + $content = ' M'; + + $reader = new MathML(); + $math = $reader->read($content); + } } diff --git a/vendor/phpoffice/math/tests/Math/Writer/MathMLTest.php b/vendor/phpoffice/math/tests/Math/Writer/MathMLTest.php index 06aac383..90724856 100644 --- a/vendor/phpoffice/math/tests/Math/Writer/MathMLTest.php +++ b/vendor/phpoffice/math/tests/Math/Writer/MathMLTest.php @@ -80,6 +80,35 @@ class MathMLTest extends WriterTestCase $this->assertIsSchemaMathMLValid($output); } + public function testWriteSemantics(): void + { + $opTimes = new Element\Operator('⁢'); + + $math = new Math(); + + $semantics = new Element\Semantics(); + $semantics->add(new Element\Identifier('y')); + $semantics->addAnnotation('application/x-tex', ' y '); + + $math->add($semantics); + + $writer = new MathML(); + $output = $writer->write($math); + + $expected = '' + . PHP_EOL + . '' + . '' + . '' + . 'y' + . ' y ' + . '' + . '' + . PHP_EOL; + $this->assertEquals($expected, $output); + $this->assertIsSchemaMathMLValid($output); + } + public function testWriteNotImplemented(): void { $this->expectException(NotImplementedException::class); diff --git a/vendor/phpoffice/phpword/LICENSE b/vendor/phpoffice/phpword/LICENSE index 8a1acaea..aebd12b0 100644 --- a/vendor/phpoffice/phpword/LICENSE +++ b/vendor/phpoffice/phpword/LICENSE @@ -1,6 +1,6 @@ PHPWord, a pure PHP library for reading and writing word processing documents. -Copyright (c) 2010-2016 PHPWord. +Copyright (c) 2010-2025 PHPWord. PHPWord is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License version 3 as published by diff --git a/vendor/phpoffice/phpword/README.md b/vendor/phpoffice/phpword/README.md index 11bcd152..e080d32d 100644 --- a/vendor/phpoffice/phpword/README.md +++ b/vendor/phpoffice/phpword/README.md @@ -1,11 +1,11 @@ # ![PHPWord](https://rawgit.com/PHPOffice/PHPWord/develop/docs/images/phpword.svg "PHPWord") -[![Latest Stable Version](https://poser.pugx.org/phpoffice/phpword/v/stable.png)](https://packagist.org/packages/phpoffice/phpword) +[![Latest Stable Version](https://poser.pugx.org/phpoffice/phpword/v)](https://packagist.org/packages/phpoffice/phpword) [![Coverage Status](https://coveralls.io/repos/github/PHPOffice/PHPWord/badge.svg?branch=master)](https://coveralls.io/github/PHPOffice/PHPWord?branch=master) -[![Total Downloads](https://poser.pugx.org/phpoffice/phpword/downloads.png)](https://packagist.org/packages/phpoffice/phpword) -[![License](https://poser.pugx.org/phpoffice/phpword/license.png)](https://packagist.org/packages/phpoffice/phpword) -[![CI](https://github.com/PHPOffice/PHPWord/actions/workflows/ci.yml/badge.svg)](https://github.com/PHPOffice/PHPWord/actions/workflows/ci.yml) -[![Join the chat at https://gitter.im/PHPOffice/PHPWord](https://img.shields.io/badge/GITTER-join%20chat-green.svg)](https://gitter.im/PHPOffice/PHPWord) +[![Total Downloads](https://poser.pugx.org/phpoffice/phpword/downloads)](https://packagist.org/packages/phpoffice/phpword) +[![License](https://poser.pugx.org/phpoffice/phpword/license)](https://packagist.org/packages/phpoffice/phpword) + +Branch Master : [![PHPWord](https://github.com/PHPOffice/PHPWord/actions/workflows/php.yml/badge.svg?branch=master)](https://github.com/PHPOffice/PHPWord/actions/workflows/php.yml) PHPWord is a library written in pure PHP that provides a set of classes to write to and read from different document file formats. The current version of PHPWord supports Microsoft [Office Open XML](http://en.wikipedia.org/wiki/Office_Open_XML) (OOXML or OpenXML), OASIS [Open Document Format for Office Applications](http://en.wikipedia.org/wiki/OpenDocument) (OpenDocument or ODF), [Rich Text Format](http://en.wikipedia.org/wiki/Rich_Text_Format) (RTF), HTML, and PDF. @@ -81,7 +81,6 @@ The following is a basic usage example of the PHPWord library. ```php =7.0", - "tecnickcom/tcpdf": "^6.5", - "symfony/process": "^4.4 || ^5.0", + "dompdf/dompdf": "^2.0 || ^3.0", "friendsofphp/php-cs-fixer": "^3.3", - "phpstan/phpstan-phpunit": "@stable" + "mpdf/mpdf": "^7.0 || ^8.0", + "phpmd/phpmd": "^2.13", + "phpstan/phpstan": "^0.12.88 || ^1.0.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": ">=7.0", + "symfony/process": "^4.4 || ^5.0", + "tecnickcom/tcpdf": "^6.5" }, "suggest": { - "ext-zip": "Allows writing OOXML and ODF", - "ext-gd2": "Allows adding images", "ext-xmlwriter": "Allows writing OOXML and ODF", "ext-xsl": "Allows applying XSL style sheet to headers, to main document part, and to footers of an OOXML template", "dompdf/dompdf": "Allows writing PDF" diff --git a/vendor/phpoffice/phpword/mkdocs.yml b/vendor/phpoffice/phpword/mkdocs.yml index dfabbb19..97c84211 100644 --- a/vendor/phpoffice/phpword/mkdocs.yml +++ b/vendor/phpoffice/phpword/mkdocs.yml @@ -63,6 +63,7 @@ nav: - OLE Object: 'usage/elements/oleobject.md' - Page Break: 'usage/elements/pagebreak.md' - Preserve Text: 'usage/elements/preservetext.md' + - Ruby: 'usage/elements/ruby.md' - Text: 'usage/elements/text.md' - TextBox: 'usage/elements/textbox.md' - Text Break: 'usage/elements/textbreak.md' @@ -87,7 +88,9 @@ nav: - Credits: 'credits.md' - Releases: - '1.x': - - '1.3.0 (WIP)': 'changes/1.x/1.3.0.md' + - '1.5.0 (WIP)': 'changes/1.x/1.5.0.md' + - '1.4.0': 'changes/1.x/1.4.0.md' + - '1.3.0': 'changes/1.x/1.3.0.md' - '1.2.0': 'changes/1.x/1.2.0.md' - '1.1.0': 'changes/1.x/1.1.0.md' - '1.0.0': 'changes/1.x/1.0.0.md' diff --git a/vendor/phpoffice/phpword/phpstan-baseline.neon b/vendor/phpoffice/phpword/phpstan-baseline.neon index 909718b8..86a54276 100644 --- a/vendor/phpoffice/phpword/phpstan-baseline.neon +++ b/vendor/phpoffice/phpword/phpstan-baseline.neon @@ -375,11 +375,6 @@ parameters: count: 1 path: src/PhpWord/Shared/Drawing.php - - - message: "#^Binary operation \"\\*\" between string and 50 results in an error\\.$#" - count: 1 - path: src/PhpWord/Shared/Html.php - - message: "#^Call to an undefined method DOMNode\\:\\:getAttribute\\(\\)\\.$#" count: 1 @@ -435,6 +430,11 @@ parameters: count: 1 path: src/PhpWord/Shared/Html.php + - + message: "#^Method PhpOffice\\\\PhpWord\\\\Shared\\\\Html\\:\\:parseRuby\\(\\) has no return type specified\\.$#" + count: 1 + path: src/PhpWord/Shared/Html.php + - message: "#^Method PhpOffice\\\\PhpWord\\\\Shared\\\\Html\\:\\:parseStyleDeclarations\\(\\) has no return type specified\\.$#" count: 1 @@ -447,7 +447,7 @@ parameters: - message: "#^Parameter \\#1 \\$attribute of static method PhpOffice\\\\PhpWord\\\\Shared\\\\Html\\:\\:parseStyle\\(\\) expects DOMAttr, DOMNode given\\.$#" - count: 1 + count: 3 path: src/PhpWord/Shared/Html.php - @@ -685,11 +685,6 @@ parameters: count: 1 path: src/PhpWord/Style/Cell.php - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Cell\\:\\:\\$shading is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Cell.php - - message: "#^Method PhpOffice\\\\PhpWord\\\\Style\\\\Chart\\:\\:getMajorTickPosition\\(\\) has no return type specified\\.$#" count: 1 @@ -760,41 +755,6 @@ parameters: count: 1 path: src/PhpWord/Style/Font.php - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$allCaps is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Font.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$doubleStrikethrough is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Font.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$lang is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Font.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$paragraph is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Font.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$shading is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Font.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$smallCaps is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Font.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Font\\:\\:\\$strikethrough is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Font.php - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Line\\:\\:\\$weight \\(int\\) does not accept float\\|int\\|null\\.$#" count: 1 @@ -815,21 +775,6 @@ parameters: count: 1 path: src/PhpWord/Style/Paragraph.php - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Paragraph\\:\\:\\$indentation is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Paragraph.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Paragraph\\:\\:\\$shading is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Paragraph.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Paragraph\\:\\:\\$spacing is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Paragraph.php - - message: "#^Result of && is always false\\.$#" count: 1 @@ -840,36 +785,6 @@ parameters: count: 1 path: src/PhpWord/Style/Section.php - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Section\\:\\:\\$lineNumbering is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Section.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Shape\\:\\:\\$extrusion is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Shape.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Shape\\:\\:\\$fill is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Shape.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Shape\\:\\:\\$frame is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Shape.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Shape\\:\\:\\$outline is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Shape.php - - - - message: "#^Property PhpOffice\\\\PhpWord\\\\Style\\\\Shape\\:\\:\\$shadow is never written, only read\\.$#" - count: 1 - path: src/PhpWord/Style/Shape.php - - message: "#^Method PhpOffice\\\\PhpWord\\\\Style\\\\TOC\\:\\:setTabLeader\\(\\) should return PhpOffice\\\\PhpWord\\\\Style\\\\TOC but returns PhpOffice\\\\PhpWord\\\\Style\\\\Tab\\.$#" count: 1 @@ -1120,11 +1035,6 @@ parameters: count: 1 path: src/PhpWord/Writer/AbstractWriter.php - - - message: "#^PHPDoc tag @param has invalid value \\(\\\\PhpOffice\\\\PhpWord\\\\PhpWord\\)\\: Unexpected token \"\\\\n \\*\", expected variable at offset 78$#" - count: 1 - path: src/PhpWord/Writer/AbstractWriter.php - - message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\HTML\\\\Element\\\\AbstractElement\\:\\:write\\(\\) has no return type specified\\.$#" count: 1 @@ -1146,14 +1056,14 @@ parameters: path: src/PhpWord/Writer/ODText/Element/Table.php - - message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\Text\\:\\:replacetabs\\(\\) has parameter \\$text with no type specified\\.$#" + message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\AbstractElement\\:\\:replaceTabs\\(\\) has parameter \\$text with no type specified\\.$#" count: 1 - path: src/PhpWord/Writer/ODText/Element/Text.php + path: src/PhpWord/Writer/ODText/Element/AbstractElement.php - - message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\Text\\:\\:replacetabs\\(\\) has parameter \\$xmlWriter with no type specified\\.$#" + message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\AbstractElement\\:\\:replaceTabs\\(\\) has parameter \\$xmlWriter with no type specified\\.$#" count: 1 - path: src/PhpWord/Writer/ODText/Element/Text.php + path: src/PhpWord/Writer/ODText/Element/AbstractElement.php - message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\ODText\\\\Element\\\\Text\\:\\:writeChangeInsertion\\(\\) has parameter \\$start with no type specified\\.$#" @@ -1315,11 +1225,6 @@ parameters: count: 1 path: src/PhpWord/Writer/RTF/Style/Border.php - - - message: "#^PHPDoc tag @param has invalid value \\(\\\\PhpOffice\\\\PhpWord\\\\PhpWord\\)\\: Unexpected token \"\\\\n \", expected variable at offset 86$#" - count: 1 - path: src/PhpWord/Writer/Word2007.php - - message: "#^Method PhpOffice\\\\PhpWord\\\\Writer\\\\Word2007\\\\Element\\\\AbstractElement\\:\\:write\\(\\) has no return type specified\\.$#" count: 1 @@ -1426,29 +1331,29 @@ parameters: path: tests/PhpWordTests/AbstractTestReader.php - - message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbeddedTest\\:\\:getBaseUrl\\(\\) has no return type specified\\.$#" + message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbedded\\:\\:getBaseUrl\\(\\) has no return type specified\\.$#" count: 1 - path: tests/PhpWordTests/AbstractWebServerEmbeddedTest.php + path: tests/PhpWordTests/AbstractWebServerEmbedded.php - - message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbeddedTest\\:\\:getRemoteBmpImageUrl\\(\\) has no return type specified\\.$#" + message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbedded\\:\\:getRemoteBmpImageUrl\\(\\) has no return type specified\\.$#" count: 1 - path: tests/PhpWordTests/AbstractWebServerEmbeddedTest.php + path: tests/PhpWordTests/AbstractWebServerEmbedded.php - - message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbeddedTest\\:\\:getRemoteGifImageUrl\\(\\) has no return type specified\\.$#" + message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbedded\\:\\:getRemoteGifImageUrl\\(\\) has no return type specified\\.$#" count: 1 - path: tests/PhpWordTests/AbstractWebServerEmbeddedTest.php + path: tests/PhpWordTests/AbstractWebServerEmbedded.php - - message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbeddedTest\\:\\:getRemoteImageUrl\\(\\) has no return type specified\\.$#" + message: "#^Method PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbedded\\:\\:getRemoteImageUrl\\(\\) has no return type specified\\.$#" count: 1 - path: tests/PhpWordTests/AbstractWebServerEmbeddedTest.php + path: tests/PhpWordTests/AbstractWebServerEmbedded.php - - message: "#^Property PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbeddedTest\\:\\:\\$httpServer has no type specified\\.$#" + message: "#^Property PhpOffice\\\\PhpWordTests\\\\AbstractWebServerEmbedded\\:\\:\\$httpServer has no type specified\\.$#" count: 1 - path: tests/PhpWordTests/AbstractWebServerEmbeddedTest.php + path: tests/PhpWordTests/AbstractWebServerEmbedded.php - message: "#^Parameter \\#1 \\$width of class PhpOffice\\\\PhpWord\\\\Element\\\\Cell constructor expects int\\|null, string given\\.$#" @@ -1789,6 +1694,11 @@ parameters: message: "#^Cannot access property \\$length on DOMNodeList\\\\|false\\.$#" count: 9 path: tests/PhpWordTests/Writer/HTML/ElementTest.php + + - + message: "#^Cannot access property \\$length on DOMNodeList\\\\|false\\.$#" + count: 2 + path: tests/PhpWordTests/Writer/HTML/Element/RubyTest.php - message: "#^Cannot call method item\\(\\) on DOMNodeList\\\\|false\\.$#" diff --git a/vendor/phpoffice/phpword/phpword.ini.dist b/vendor/phpoffice/phpword/phpword.ini.dist index f3f66dbe..21d3b506 100644 --- a/vendor/phpoffice/phpword/phpword.ini.dist +++ b/vendor/phpoffice/phpword/phpword.ini.dist @@ -14,6 +14,7 @@ outputEscapingEnabled = false defaultFontName = Arial defaultFontSize = 10 +defaultFontColor = 000000 [Paper] diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/AbstractCollection.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/AbstractCollection.php index 646489d8..97c2c45d 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Collection/AbstractCollection.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/AbstractCollection.php @@ -1,4 +1,5 @@ */ class Bookmarks extends AbstractCollection diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/Charts.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/Charts.php index 7c2dfbab..ef1c6597 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Collection/Charts.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/Charts.php @@ -1,4 +1,5 @@ */ class Charts extends AbstractCollection diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/Comments.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/Comments.php index 5fa4020a..4f139435 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Collection/Comments.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/Comments.php @@ -1,4 +1,5 @@ */ class Comments extends AbstractCollection diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/Endnotes.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/Endnotes.php index 09903b1b..3de1ad22 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Collection/Endnotes.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/Endnotes.php @@ -1,4 +1,5 @@ */ class Endnotes extends AbstractCollection diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/Footnotes.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/Footnotes.php index 0387fce3..804a45c8 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Collection/Footnotes.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/Footnotes.php @@ -1,4 +1,5 @@ */ class Footnotes extends AbstractCollection diff --git a/vendor/phpoffice/phpword/src/PhpWord/Collection/Titles.php b/vendor/phpoffice/phpword/src/PhpWord/Collection/Titles.php index 543aabda..1a943697 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Collection/Titles.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Collection/Titles.php @@ -1,4 +1,5 @@ */ class Titles extends AbstractCollection diff --git a/vendor/phpoffice/phpword/src/PhpWord/ComplexType/FootnoteProperties.php b/vendor/phpoffice/phpword/src/PhpWord/ComplexType/FootnoteProperties.php index 2e7743ca..10c426e8 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/ComplexType/FootnoteProperties.php +++ b/vendor/phpoffice/phpword/src/PhpWord/ComplexType/FootnoteProperties.php @@ -1,4 +1,5 @@ newInstanceArgs($elementArgs); // Set parent container @@ -165,7 +167,7 @@ abstract class AbstractContainer extends AbstractElement /** * Get all elements. * - * @return \PhpOffice\PhpWord\Element\AbstractElement[] + * @return AbstractElement[] */ public function getElements() { @@ -177,7 +179,7 @@ abstract class AbstractContainer extends AbstractElement * * @param int $index * - * @return null|\PhpOffice\PhpWord\Element\AbstractElement + * @return null|AbstractElement */ public function getElement($index) { @@ -191,13 +193,13 @@ abstract class AbstractContainer extends AbstractElement /** * Removes the element at requested index. * - * @param int|\PhpOffice\PhpWord\Element\AbstractElement $toRemove + * @param AbstractElement|int $toRemove */ public function removeElement($toRemove): void { if (is_int($toRemove) && array_key_exists($toRemove, $this->elements)) { unset($this->elements[$toRemove]); - } elseif ($toRemove instanceof \PhpOffice\PhpWord\Element\AbstractElement) { + } elseif ($toRemove instanceof AbstractElement) { foreach ($this->elements as $key => $element) { if ($element->getElementId() === $toRemove->getElementId()) { unset($this->elements[$key]); @@ -253,7 +255,7 @@ abstract class AbstractContainer extends AbstractElement 'Footnote' => ['Section', 'TextRun', 'Cell', 'ListItemRun'], 'Endnote' => ['Section', 'TextRun', 'Cell'], 'PreserveText' => ['Section', 'Header', 'Footer', 'Cell'], - 'Title' => ['Section', 'Cell'], + 'Title' => ['Section', 'Cell', 'Header'], 'TOC' => ['Section'], 'PageBreak' => ['Section'], 'Chart' => ['Section', 'Cell'], diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/AbstractElement.php b/vendor/phpoffice/phpword/src/PhpWord/Element/AbstractElement.php index 385e4d31..3a29b686 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Element/AbstractElement.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Element/AbstractElement.php @@ -1,4 +1,5 @@ elementId = substr(md5(mt_rand()), 0, 6); + $this->elementId = substr(md5((string) mt_rand()), 0, 6); } /** @@ -291,8 +290,6 @@ abstract class AbstractElement /** * Get comments start. - * - * @return Comments */ public function getCommentsRangeStart(): ?Comments { @@ -301,8 +298,6 @@ abstract class AbstractElement /** * Get comment start. - * - * @return Comment */ public function getCommentRangeStart(): ?Comment { @@ -339,8 +334,6 @@ abstract class AbstractElement /** * Get comments end. - * - * @return Comments */ public function getCommentsRangeEnd(): ?Comments { @@ -349,8 +342,6 @@ abstract class AbstractElement /** * Get comment end. - * - * @return Comment */ public function getCommentRangeEnd(): ?Comment { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/Bookmark.php b/vendor/phpoffice/phpword/src/PhpWord/Element/Bookmark.php index 4fe3d0f0..151d5a48 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Element/Bookmark.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Element/Bookmark.php @@ -1,4 +1,5 @@ text; } @@ -109,7 +108,7 @@ class Link extends AbstractElement /** * Get Text style. * - * @return null|\PhpOffice\PhpWord\Style\Font|string + * @return null|Font|string */ public function getFontStyle() { @@ -119,7 +118,7 @@ class Link extends AbstractElement /** * Get Paragraph style. * - * @return null|\PhpOffice\PhpWord\Style\Paragraph|string + * @return null|Paragraph|string */ public function getParagraphStyle() { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/ListItem.php b/vendor/phpoffice/phpword/src/PhpWord/Element/ListItem.php index d3f25c29..bedfd110 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Element/ListItem.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Element/ListItem.php @@ -1,4 +1,5 @@ $collectionArray; if (in_array($type, [Header::AUTO, Header::FIRST, Header::EVEN])) { $index = count($collection); - /** @var \PhpOffice\PhpWord\Element\AbstractContainer $container Type hint */ + /** @var AbstractContainer $container Type hint */ $container = new $containerClass($this->sectionId, ++$index, $type); $container->setPhpWord($this->phpWord); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/Shape.php b/vendor/phpoffice/phpword/src/PhpWord/Element/Shape.php index 15161f89..9ea221db 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Element/Shape.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Element/Shape.php @@ -1,4 +1,5 @@ phpWord->getTitles()->getItems(); foreach ($titles as $i => $title) { - /** @var \PhpOffice\PhpWord\Element\Title $title Type hint */ + /** @var Title $title Type hint */ $depth = $title->getDepth(); if ($this->minDepth > $depth) { unset($titles[$i]); @@ -110,7 +111,7 @@ class TOC extends AbstractElement /** * Get TOC Style. * - * @return \PhpOffice\PhpWord\Style\TOC + * @return TOCStyle */ public function getStyleTOC() { @@ -120,7 +121,7 @@ class TOC extends AbstractElement /** * Get Font Style. * - * @return \PhpOffice\PhpWord\Style\Font|string + * @return Font|string */ public function getStyleFont() { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/Table.php b/vendor/phpoffice/phpword/src/PhpWord/Element/Table.php index 53a828a9..7fb10306 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Element/Table.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Element/Table.php @@ -1,4 +1,5 @@ rows); for ($i = 0; $i < $rowCount; ++$i) { - /** @var \PhpOffice\PhpWord\Element\Row $row Type hint */ + /** @var Row $row Type hint */ $row = $this->rows[$i]; $cellCount = count($row->getCells()); if ($columnCount < $cellCount) { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/Text.php b/vendor/phpoffice/phpword/src/PhpWord/Element/Text.php index 96953953..f20b273e 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Element/Text.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Element/Text.php @@ -1,4 +1,5 @@ text; } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/TextBox.php b/vendor/phpoffice/phpword/src/PhpWord/Element/TextBox.php index af37f657..b9f8b50b 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Element/TextBox.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Element/TextBox.php @@ -1,4 +1,5 @@ getElements() as $element) { if ($element instanceof Text) { $outstr .= $element->getText(); + } elseif ($element instanceof Ruby) { + $outstr .= $element->getBaseTextRun()->getText() . + ' (' . $element->getRubyTextRun()->getText() . ')'; } } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Element/Title.php b/vendor/phpoffice/phpword/src/PhpWord/Element/Title.php index 8fd5b21b..89f438a5 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Element/Title.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Element/Title.php @@ -1,4 +1,5 @@ load($filename); @@ -100,7 +101,7 @@ abstract class IOFactory */ public static function extractVariables(string $filename, string $readerName = 'Word2007'): array { - /** @var \PhpOffice\PhpWord\Reader\ReaderInterface $reader */ + /** @var ReaderInterface $reader */ $reader = self::createReader($readerName); $document = $reader->load($filename); $extractedVariables = []; diff --git a/vendor/phpoffice/phpword/src/PhpWord/Media.php b/vendor/phpoffice/phpword/src/PhpWord/Media.php index 31487a91..0a340a0a 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Media.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Media.php @@ -1,4 +1,5 @@ setRels($relationships); $part->read($phpWord); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Reader/ODText/AbstractPart.php b/vendor/phpoffice/phpword/src/PhpWord/Reader/ODText/AbstractPart.php index 4c89192e..447c96f0 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Reader/ODText/AbstractPart.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Reader/ODText/AbstractPart.php @@ -1,4 +1,5 @@ getElements('w:r|w:hyperlink', $domNode); - if ($nodes->length === 1) { + $hasRubyElement = $xmlReader->elementExists('w:r/w:ruby', $domNode); + if ($nodes->length === 1 && !$hasRubyElement) { $textContent = htmlspecialchars($xmlReader->getValue('w:t', $nodes->item(0)), ENT_QUOTES, 'UTF-8'); } else { $textContent = new TextRun($paragraphStyle); @@ -450,7 +455,7 @@ abstract class AbstractPart /** * Read w:r. * - * @param \PhpOffice\PhpWord\Element\AbstractContainer $parent + * @param AbstractContainer $parent * @param string $docPart * @param mixed $paragraphStyle * @@ -584,9 +589,47 @@ abstract class AbstractPart } } elseif ($node->nodeName == 'w:softHyphen') { $element = $parent->addText("\u{200c}", $fontStyle, $paragraphStyle); + } elseif ($node->nodeName == 'w:ruby') { + $rubyPropertiesNode = $xmlReader->getElement('w:rubyPr', $node); + $properties = $this->readRubyProperties($xmlReader, $rubyPropertiesNode); + // read base text node + $baseText = new TextRun($paragraphStyle); + $baseTextNode = $xmlReader->getElement('w:rubyBase/w:r', $node); + $this->readRun($xmlReader, $baseTextNode, $baseText, $docPart, $paragraphStyle); + // read the actual ruby text (e.g. furigana in Japanese) + $rubyText = new TextRun($paragraphStyle); + $rubyTextNode = $xmlReader->getElement('w:rt/w:r', $node); + $this->readRun($xmlReader, $rubyTextNode, $rubyText, $docPart, $paragraphStyle); + // add element to parent + $parent->addRuby($baseText, $rubyText, $properties); } } + /** + * Read w:rubyPr element. + * + * @param XMLReader $xmlReader reader for XML + * @param DOMElement $domNode w:RubyPr element + * + * @return RubyProperties ruby properties from element + */ + protected function readRubyProperties(XMLReader $xmlReader, DOMElement $domNode): RubyProperties + { + $rubyAlignment = $xmlReader->getElement('w:rubyAlign', $domNode)->getAttribute('w:val'); + $rubyHps = $xmlReader->getElement('w:hps', $domNode)->getAttribute('w:val'); // font face + $rubyHpsRaise = $xmlReader->getElement('w:hpsRaise', $domNode)->getAttribute('w:val'); // pts above base text + $rubyHpsBaseText = $xmlReader->getElement('w:hpsBaseText', $domNode)->getAttribute('w:val'); // base text size + $rubyLid = $xmlReader->getElement('w:lid', $domNode)->getAttribute('w:val'); // type of ruby + $properties = new RubyProperties(); + $properties->setAlignment($rubyAlignment); + $properties->setFontFaceSize((float) $rubyHps); + $properties->setFontPointsAboveBaseText((float) $rubyHpsRaise); + $properties->setFontSizeForBaseText((float) $rubyHpsBaseText); + $properties->setLanguageId($rubyLid); + + return $properties; + } + /** * Read w:tbl. * @@ -661,8 +704,11 @@ abstract class AbstractPart 'alignment' => [self::READ_VALUE, 'w:jc'], 'basedOn' => [self::READ_VALUE, 'w:basedOn'], 'next' => [self::READ_VALUE, 'w:next'], - 'indent' => [self::READ_VALUE, 'w:ind', 'w:left'], - 'hanging' => [self::READ_VALUE, 'w:ind', 'w:hanging'], + 'indentLeft' => [self::READ_VALUE, 'w:ind', 'w:left'], + 'indentRight' => [self::READ_VALUE, 'w:ind', 'w:right'], + 'indentHanging' => [self::READ_VALUE, 'w:ind', 'w:hanging'], + 'indentFirstLine' => [self::READ_VALUE, 'w:ind', 'w:firstLine'], + 'indentFirstLineChars' => [self::READ_VALUE, 'w:ind', 'w:firstLineChars'], 'spaceAfter' => [self::READ_VALUE, 'w:spacing', 'w:after'], 'spaceBefore' => [self::READ_VALUE, 'w:spacing', 'w:before'], 'widowControl' => [self::READ_FALSE, 'w:widowControl'], diff --git a/vendor/phpoffice/phpword/src/PhpWord/Reader/Word2007/DocPropsApp.php b/vendor/phpoffice/phpword/src/PhpWord/Reader/Word2007/DocPropsApp.php index 9d6f3cbb..c7ecb007 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Reader/Word2007/DocPropsApp.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Reader/Word2007/DocPropsApp.php @@ -1,4 +1,5 @@ setDefaultFontSize($fontDefaultStyle['size']); } + if (array_key_exists('color', $fontDefaultStyle)) { + $phpWord->setDefaultFontColor($fontDefaultStyle['color']); + } if (array_key_exists('lang', $fontDefaultStyle)) { $phpWord->getSettings()->setThemeFontLang(new Language($fontDefaultStyle['lang'])); } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Settings.php b/vendor/phpoffice/phpword/src/PhpWord/Settings.php index 984486cc..16f49166 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Settings.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Settings.php @@ -1,4 +1,5 @@ cssContent); preg_match_all('/(.+?)\s?\{\s?(.+?)\s?\}/', $cssContent, $cssExtracted); - // Check the number of extracted - if (count($cssExtracted) != 3) { - return; - } // Check if there are x selectors and x rules if (count($cssExtracted[1]) != count($cssExtracted[2])) { return; diff --git a/vendor/phpoffice/phpword/src/PhpWord/Shared/Drawing.php b/vendor/phpoffice/phpword/src/PhpWord/Shared/Drawing.php index df218108..8af7da2f 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Shared/Drawing.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Shared/Drawing.php @@ -1,4 +1,5 @@ . * - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element Where the parts need to be added + * @param AbstractContainer $element Where the parts need to be added * @param string $html The code to parse * @param bool $fullHTML If it's a full HTML, no need to add 'body' tag * @param bool $preserveWhiteSpace If false, the whitespaces between nodes will be removed @@ -127,21 +130,21 @@ class Html break; case 'width': // tables, cells + $val = $val === 'auto' ? '100%' : $val; if (false !== strpos($val, '%')) { // e.g. or
$styles['width'] = (int) $val * 50; $styles['unit'] = \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT; } else { // e.g. , where "2" = 2px (always pixels) - $val = (int) $val . 'px'; - $styles['cellSpacing'] = Converter::cssToTwip($val); + $styles['cellSpacing'] = Converter::pixelToTwip(self::convertHtmlSize($val)); break; case 'bgcolor': @@ -185,7 +188,7 @@ class Html * Parse a node and add a corresponding element to the parent element. * * @param DOMNode $node node to parse - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element object to add an element corresponding with the node + * @param AbstractContainer $element object to add an element corresponding with the node * @param array $styles Array with all styles * @param array $data Array to transport data to a next level in the DOM tree, for example level of listitems */ @@ -208,16 +211,16 @@ class Html // Node mapping table $nodes = [ - // $method $node $element $styles $data $argument1 $argument2 - 'p' => ['Paragraph', $node, $element, $styles, null, null, null], - 'h1' => ['Heading', null, $element, $styles, null, 'Heading1', null], - 'h2' => ['Heading', null, $element, $styles, null, 'Heading2', null], - 'h3' => ['Heading', null, $element, $styles, null, 'Heading3', null], - 'h4' => ['Heading', null, $element, $styles, null, 'Heading4', null], - 'h5' => ['Heading', null, $element, $styles, null, 'Heading5', null], - 'h6' => ['Heading', null, $element, $styles, null, 'Heading6', null], - '#text' => ['Text', $node, $element, $styles, null, null, null], - 'strong' => ['Property', null, null, $styles, null, 'bold', true], + // $method $node $element $styles $data $argument1 $argument2 + 'p' => ['Paragraph', $node, $element, $styles, null, null, null], + 'h1' => ['Heading', $node, $element, $styles, null, 'Heading1', null], + 'h2' => ['Heading', $node, $element, $styles, null, 'Heading2', null], + 'h3' => ['Heading', $node, $element, $styles, null, 'Heading3', null], + 'h4' => ['Heading', $node, $element, $styles, null, 'Heading4', null], + 'h5' => ['Heading', $node, $element, $styles, null, 'Heading5', null], + 'h6' => ['Heading', $node, $element, $styles, null, 'Heading6', null], + '#text' => ['Text', $node, $element, $styles, null, null, null], + 'strong' => ['Property', null, null, $styles, null, 'bold', true], 'b' => ['Property', null, null, $styles, null, 'bold', true], 'em' => ['Property', null, null, $styles, null, 'italic', true], 'i' => ['Property', null, null, $styles, null, 'italic', true], @@ -238,6 +241,7 @@ class Html 'a' => ['Link', $node, $element, $styles, null, null, null], 'input' => ['Input', $node, $element, $styles, null, null, null], 'hr' => ['HorizRule', $node, $element, $styles, null, null, null], + 'ruby' => ['Ruby', $node, $element, $styles, null, null, null], ]; $newElement = null; @@ -276,7 +280,7 @@ class Html * Parse child nodes. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\AbstractContainer|Row|Table $element + * @param AbstractContainer|Row|Table $element * @param array $styles * @param array $data */ @@ -298,10 +302,10 @@ class Html * Parse paragraph node. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @param AbstractContainer $element * @param array &$styles * - * @return \PhpOffice\PhpWord\Element\PageBreak|\PhpOffice\PhpWord\Element\TextRun + * @return \PhpOffice\PhpWord\Element\PageBreak|TextRun */ protected static function parseParagraph($node, $element, &$styles) { @@ -317,7 +321,7 @@ class Html * Parse input node. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @param AbstractContainer $element * @param array &$styles */ protected static function parseInput($node, $element, &$styles): void @@ -341,28 +345,25 @@ class Html /** * Parse heading node. * - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element - * @param array &$styles * @param string $argument1 Name of heading style * - * @return \PhpOffice\PhpWord\Element\TextRun - * * @todo Think of a clever way of defining header styles, now it is only based on the assumption, that * Heading1 - Heading6 are already defined somewhere */ - protected static function parseHeading($element, &$styles, $argument1) + protected static function parseHeading(DOMNode $node, AbstractContainer $element, array &$styles, string $argument1): TextRun { - $styles['paragraph'] = $argument1; - $newElement = $element->addTextRun($styles['paragraph']); + $style = new Paragraph(); + $style->setStyleName($argument1); + $style->setStyleByArray(self::parseInlineStyle($node, $styles['paragraph'])); - return $newElement; + return $element->addTextRun($style); } /** * Parse text node. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @param AbstractContainer $element * @param array &$styles */ protected static function parseText($node, $element, &$styles): void @@ -406,7 +407,7 @@ class Html * Parse table node. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @param AbstractContainer $element * @param array &$styles * * @return Table $element @@ -437,7 +438,7 @@ class Html * Parse a table row. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\Table $element + * @param Table $element * @param array &$styles * * @return Row $element @@ -460,10 +461,10 @@ class Html * Parse table cell. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\Table $element + * @param Table $element * @param array &$styles * - * @return \PhpOffice\PhpWord\Element\Cell|\PhpOffice\PhpWord\Element\TextRun $element + * @return \PhpOffice\PhpWord\Element\Cell|TextRun $element */ protected static function parseCell($node, $element, &$styles) { @@ -554,7 +555,7 @@ class Html * Parse list node. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @param AbstractContainer $element * @param array &$styles * @param array &$data */ @@ -627,15 +628,15 @@ class Html return [ 'type' => 'hybridMultilevel', 'levels' => [ - ['format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 720, 'left' => 720, 'hanging' => 360, 'font' => 'Symbol', 'hint' => 'default'], - ['format' => NumberFormat::BULLET, 'text' => 'o', 'alignment' => 'left', 'tabPos' => 1440, 'left' => 1440, 'hanging' => 360, 'font' => 'Courier New', 'hint' => 'default'], - ['format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 2160, 'left' => 2160, 'hanging' => 360, 'font' => 'Wingdings', 'hint' => 'default'], - ['format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 2880, 'left' => 2880, 'hanging' => 360, 'font' => 'Symbol', 'hint' => 'default'], - ['format' => NumberFormat::BULLET, 'text' => 'o', 'alignment' => 'left', 'tabPos' => 3600, 'left' => 3600, 'hanging' => 360, 'font' => 'Courier New', 'hint' => 'default'], - ['format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 4320, 'left' => 4320, 'hanging' => 360, 'font' => 'Wingdings', 'hint' => 'default'], - ['format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 5040, 'left' => 5040, 'hanging' => 360, 'font' => 'Symbol', 'hint' => 'default'], - ['format' => NumberFormat::BULLET, 'text' => 'o', 'alignment' => 'left', 'tabPos' => 5760, 'left' => 5760, 'hanging' => 360, 'font' => 'Courier New', 'hint' => 'default'], - ['format' => NumberFormat::BULLET, 'text' => '', 'alignment' => 'left', 'tabPos' => 6480, 'left' => 6480, 'hanging' => 360, 'font' => 'Wingdings', 'hint' => 'default'], + ['format' => NumberFormat::BULLET, 'text' => '•', 'alignment' => 'left', 'tabPos' => 720, 'left' => 720, 'hanging' => 360, 'font' => 'Symbol', 'hint' => 'default'], + ['format' => NumberFormat::BULLET, 'text' => '◦', 'alignment' => 'left', 'tabPos' => 1440, 'left' => 1440, 'hanging' => 360, 'font' => 'Courier New', 'hint' => 'default'], + ['format' => NumberFormat::BULLET, 'text' => '•', 'alignment' => 'left', 'tabPos' => 2160, 'left' => 2160, 'hanging' => 360, 'font' => 'Wingdings', 'hint' => 'default'], + ['format' => NumberFormat::BULLET, 'text' => '•', 'alignment' => 'left', 'tabPos' => 2880, 'left' => 2880, 'hanging' => 360, 'font' => 'Symbol', 'hint' => 'default'], + ['format' => NumberFormat::BULLET, 'text' => '◦', 'alignment' => 'left', 'tabPos' => 3600, 'left' => 3600, 'hanging' => 360, 'font' => 'Courier New', 'hint' => 'default'], + ['format' => NumberFormat::BULLET, 'text' => '•', 'alignment' => 'left', 'tabPos' => 4320, 'left' => 4320, 'hanging' => 360, 'font' => 'Wingdings', 'hint' => 'default'], + ['format' => NumberFormat::BULLET, 'text' => '•', 'alignment' => 'left', 'tabPos' => 5040, 'left' => 5040, 'hanging' => 360, 'font' => 'Symbol', 'hint' => 'default'], + ['format' => NumberFormat::BULLET, 'text' => '◦', 'alignment' => 'left', 'tabPos' => 5760, 'left' => 5760, 'hanging' => 360, 'font' => 'Courier New', 'hint' => 'default'], + ['format' => NumberFormat::BULLET, 'text' => '•', 'alignment' => 'left', 'tabPos' => 6480, 'left' => 6480, 'hanging' => 360, 'font' => 'Wingdings', 'hint' => 'default'], ], ]; } @@ -644,7 +645,7 @@ class Html * Parse list item node. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @param AbstractContainer $element * @param array &$styles * @param array $data * @@ -704,6 +705,10 @@ class Html case 'text-align': $styles['alignment'] = self::mapAlign($value, $bidi); + break; + case 'ruby-align': + $styles['rubyAlignment'] = self::mapRubyAlign($value); + break; case 'display': $styles['hidden'] = $value === 'none' || $value === 'hidden'; @@ -733,7 +738,7 @@ class Html break; case 'line-height': $matches = []; - if ($value === 'normal') { + if ($value === 'normal' || $value === 'inherit') { $spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::AUTO; $spacing = 0; } elseif (preg_match('/([0-9]+\.?[0-9]*[a-z]+)/', $value, $matches)) { @@ -803,6 +808,58 @@ class Html $styles['spaceAfter'] = Converter::cssToTwip($value); break; + + case 'padding': + $valueTop = $valueRight = $valueBottom = $valueLeft = null; + $cValue = preg_replace('# +#', ' ', trim($value)); + $paddingArr = explode(' ', $cValue); + $countParams = count($paddingArr); + if ($countParams == 1) { + $valueTop = $valueRight = $valueBottom = $valueLeft = $paddingArr[0]; + } elseif ($countParams == 2) { + $valueTop = $valueBottom = $paddingArr[0]; + $valueRight = $valueLeft = $paddingArr[1]; + } elseif ($countParams == 3) { + $valueTop = $paddingArr[0]; + $valueRight = $valueLeft = $paddingArr[1]; + $valueBottom = $paddingArr[2]; + } elseif ($countParams == 4) { + $valueTop = $paddingArr[0]; + $valueRight = $paddingArr[1]; + $valueBottom = $paddingArr[2]; + $valueLeft = $paddingArr[3]; + } + if ($valueTop !== null) { + $styles['paddingTop'] = Converter::cssToTwip($valueTop); + } + if ($valueRight !== null) { + $styles['paddingRight'] = Converter::cssToTwip($valueRight); + } + if ($valueBottom !== null) { + $styles['paddingBottom'] = Converter::cssToTwip($valueBottom); + } + if ($valueLeft !== null) { + $styles['paddingLeft'] = Converter::cssToTwip($valueLeft); + } + + break; + case 'padding-top': + $styles['paddingTop'] = Converter::cssToTwip($value); + + break; + case 'padding-right': + $styles['paddingRight'] = Converter::cssToTwip($value); + + break; + case 'padding-bottom': + $styles['paddingBottom'] = Converter::cssToTwip($value); + + break; + case 'padding-left': + $styles['paddingLeft'] = Converter::cssToTwip($value); + + break; + case 'border-color': self::mapBorderColor($styles, $value); @@ -886,7 +943,7 @@ class Html * Parse image node. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @param AbstractContainer $element * * @return \PhpOffice\PhpWord\Element\Image */ @@ -901,36 +958,12 @@ class Html break; case 'width': - $width = $attribute->value; - - // pt - if (false !== strpos($width, 'pt')) { - $width = Converter::pointToPixel((float) str_replace('pt', '', $width)); - } - - // px - if (false !== strpos($width, 'px')) { - $width = str_replace('px', '', $width); - } - - $style['width'] = $width; + $style['width'] = self::convertHtmlSize($attribute->value); $style['unit'] = \PhpOffice\PhpWord\Style\Image::UNIT_PX; break; case 'height': - $height = $attribute->value; - - // pt - if (false !== strpos($height, 'pt')) { - $height = Converter::pointToPixel((float) str_replace('pt', '', $height)); - } - - // px - if (false !== strpos($height, 'px')) { - $height = str_replace('px', '', $height); - } - - $style['height'] = $height; + $style['height'] = self::convertHtmlSize($attribute->value); $style['unit'] = \PhpOffice\PhpWord\Style\Image::UNIT_PX; break; @@ -970,14 +1003,15 @@ class Html $match = []; preg_match('/data:image\/(\w+);base64,(.+)/', $src, $match); + if (!empty($match)) { + $src = $imgFile = $tmpDir . uniqid() . '.' . $match[1]; - $src = $imgFile = $tmpDir . uniqid() . '.' . $match[1]; + $ifp = fopen($imgFile, 'wb'); - $ifp = fopen($imgFile, 'wb'); - - if ($ifp !== false) { - fwrite($ifp, base64_decode($match[2])); - fclose($ifp); + if ($ifp !== false) { + fwrite($ifp, base64_decode($match[2])); + fclose($ifp); + } } } $src = urldecode($src); @@ -1073,6 +1107,23 @@ class Html } } + /** + * Transforms a HTML/CSS ruby alignment into a \PhpOffice\PhpWord\SimpleType\Jc. + */ + protected static function mapRubyAlign(string $cssRubyAlignment): string + { + switch ($cssRubyAlignment) { + case 'center': + return RubyProperties::ALIGNMENT_CENTER; + case 'start': + return RubyProperties::ALIGNMENT_LEFT; + case 'space-between': + return RubyProperties::ALIGNMENT_DISTRIBUTE_SPACE; + default: + return ''; + } + } + /** * Transforms a HTML/CSS vertical alignment. * @@ -1129,7 +1180,7 @@ class Html /** * Parse line break. * - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @param AbstractContainer $element */ protected static function parseLineBreak($element): void { @@ -1140,7 +1191,7 @@ class Html * Parse link node. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @param AbstractContainer $element * @param array $styles */ protected static function parseLink($node, $element, &$styles) @@ -1172,7 +1223,7 @@ class Html * Note: Word rule is not the same as HTML's
since it does not support width and thus neither alignment. * * @param DOMNode $node - * @param \PhpOffice\PhpWord\Element\AbstractContainer $element + * @param AbstractContainer $element */ protected static function parseHorizRule($node, $element): void { @@ -1201,6 +1252,59 @@ class Html // - repeated text, e.g. underline "_", because of unpredictable line wrapping } + /** + * Parse ruby node. + * + * @param DOMNode $node + * @param AbstractContainer $element + * @param array $styles + */ + protected static function parseRuby($node, $element, &$styles) + { + $rubyProperties = new RubyProperties(); + $baseTextRun = new TextRun($styles['paragraph']); + $rubyTextRun = new TextRun(null); + if ($node->hasAttributes()) { + $langAttr = $node->attributes->getNamedItem('lang'); + if ($langAttr !== null) { + $rubyProperties->setLanguageId($langAttr->textContent); + } + $styleAttr = $node->attributes->getNamedItem('style'); + if ($styleAttr !== null) { + $styles = self::parseStyle($styleAttr, $styles['paragraph']); + if (isset($styles['rubyAlignment']) && $styles['rubyAlignment'] !== '') { + $rubyProperties->setAlignment($styles['rubyAlignment']); + } + if (isset($styles['size']) && $styles['size'] !== '') { + $rubyProperties->setFontSizeForBaseText($styles['size']); + } + $baseTextRun->setParagraphStyle($styles); + } + } + foreach ($node->childNodes as $child) { + if ($child->nodeName === '#text') { + $content = trim($child->textContent); + if ($content !== '') { + $baseTextRun->addText($content); + } + } elseif ($child->nodeName === 'rt') { + $rubyTextRun->addText(trim($child->textContent)); + if ($child->hasAttributes()) { + $styleAttr = $child->attributes->getNamedItem('style'); + if ($styleAttr !== null) { + $styles = self::parseStyle($styleAttr, []); + if (isset($styles['size']) && $styles['size'] !== '') { + $rubyProperties->setFontFaceSize($styles['size']); + } + $rubyTextRun->setParagraphStyle($styles); + } + } + } + } + + return $element->addRuby($baseTextRun, $rubyTextRun, $rubyProperties); + } + private static function convertRgb(string $rgb): string { if (preg_match(self::RGB_REGEXP, $rgb, $matches) === 1) { @@ -1209,4 +1313,22 @@ class Html return trim($rgb, '# '); } + + /** + * Transform HTML sizes (pt, px) in pixels. + */ + protected static function convertHtmlSize(string $size): float + { + // pt + if (false !== strpos($size, 'pt')) { + return Converter::pointToPixel((float) str_replace('pt', '', $size)); + } + + // px + if (false !== strpos($size, 'px')) { + return (float) str_replace('px', '', $size); + } + + return (float) $size; + } } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/vendor/phpoffice/phpword/src/PhpWord/Shared/Microsoft/PasswordEncoder.php index d6cf69fc..4762cc71 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Shared/Microsoft/PasswordEncoder.php @@ -1,4 +1,5 @@ tempDir . DIRECTORY_SEPARATOR . $filenameParts['basename'], 'wb'); - fwrite($handle, $contents); - fclose($handle); + if ($handle) { + fwrite($handle, $contents); + fclose($handle); + } // Add temp file to zip $filename = $this->tempDir . DIRECTORY_SEPARATOR . $filenameParts['basename']; @@ -420,4 +423,15 @@ class ZipArchive return ($listIndex > -1) ? $listIndex : false; } + + /** + * Add an empty directory to the zip archive (emulate \ZipArchive). + * + * @param string $dirname Directory name to add to the zip archive + */ + public function addEmptyDir(string $dirname): bool + { + // Create a directory entry by adding an empty file with trailing slash + return $this->addFromString(rtrim($dirname, '/') . '/', ''); + } } diff --git a/vendor/phpoffice/phpword/src/PhpWord/SimpleType/Border.php b/vendor/phpoffice/phpword/src/PhpWord/SimpleType/Border.php index 6cb42f04..acd1c1a1 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/SimpleType/Border.php +++ b/vendor/phpoffice/phpword/src/PhpWord/SimpleType/Border.php @@ -1,4 +1,5 @@ vAlign = null; + + return $this; + } + VerticalJc::validate($value); $this->vAlign = $this->setEnumVal($value, VerticalJc::values(), $this->vAlign); @@ -235,7 +262,7 @@ class Cell extends Border /** * Get vertical merge (rowspan). * - * @return string + * @return null|string */ public function getVMerge() { @@ -245,12 +272,18 @@ class Cell extends Border /** * Set vertical merge (rowspan). * - * @param string $value + * @param null|string $value * * @return self */ public function setVMerge($value = null) { + if ($value === null) { + $this->vMerge = null; + + return $this; + } + $enum = [self::VMERGE_RESTART, self::VMERGE_CONTINUE]; $this->vMerge = $this->setEnumVal($value, $enum, $this->vMerge); @@ -260,7 +293,7 @@ class Cell extends Border /** * Get shading. * - * @return \PhpOffice\PhpWord\Style\Shading + * @return Shading */ public function getShading() { @@ -344,4 +377,84 @@ class Cell extends Border { return $this->noWrap; } + + /** + * Get style padding-top. + */ + public function getPaddingTop(): ?int + { + return $this->paddingTop; + } + + /** + * Set style padding-top. + * + * @return $this + */ + public function setPaddingTop(int $value): self + { + $this->paddingTop = $value; + + return $this; + } + + /** + * Get style padding-bottom. + */ + public function getPaddingBottom(): ?int + { + return $this->paddingBottom; + } + + /** + * Set style padding-bottom. + * + * @return $this + */ + public function setPaddingBottom(int $value): self + { + $this->paddingBottom = $value; + + return $this; + } + + /** + * Get style padding-left. + */ + public function getPaddingLeft(): ?int + { + return $this->paddingLeft; + } + + /** + * Set style padding-left. + * + * @return $this + */ + public function setPaddingLeft(int $value): self + { + $this->paddingLeft = $value; + + return $this; + } + + /** + * Get style padding-right. + */ + public function getPaddingRight(): ?int + { + return $this->paddingRight; + } + + /** + * Set style padding-right. + * + * @return $this + */ + public function setPaddingRight(int $value): self + { + $this->paddingRight = $value; + + return $this; + } } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Style/Chart.php b/vendor/phpoffice/phpword/src/PhpWord/Style/Chart.php index 9dbc8db0..6cffc4d5 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Style/Chart.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Style/Chart.php @@ -1,4 +1,5 @@ color; } @@ -682,7 +681,7 @@ class Font extends AbstractStyle * * @param string $value * - * @return \PhpOffice\PhpWord\Style\Table + * @return Table */ public function setBgColor($value = null) { @@ -812,7 +811,7 @@ class Font extends AbstractStyle /** * Get paragraph style. * - * @return \PhpOffice\PhpWord\Style\Paragraph + * @return Paragraph */ public function getParagraph() { @@ -860,7 +859,7 @@ class Font extends AbstractStyle /** * Get shading. * - * @return \PhpOffice\PhpWord\Style\Shading + * @return Shading */ public function getShading() { @@ -884,7 +883,7 @@ class Font extends AbstractStyle /** * Get language. * - * @return null|\PhpOffice\PhpWord\Style\Language + * @return null|Language */ public function getLang() { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Style/Frame.php b/vendor/phpoffice/phpword/src/PhpWord/Style/Frame.php index 45fc583e..016722f3 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Style/Frame.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Style/Frame.php @@ -1,4 +1,5 @@ left; } /** * Set left. - * - * @param float|int $value - * - * @return self */ - public function setLeft($value) + public function setLeft(?float $value): self { - $this->left = $this->setNumericVal($value, $this->left); + $this->left = $this->setNumericVal($value); return $this; } /** * Get right. - * - * @return float|int */ - public function getRight() + public function getRight(): ?float { return $this->right; } /** * Set right. - * - * @param float|int $value - * - * @return self */ - public function setRight($value) + public function setRight(?float $value): self { - $this->right = $this->setNumericVal($value, $this->right); + $this->right = $this->setNumericVal($value); return $this; } /** * Get first line. - * - * @return float|int */ - public function getFirstLine() + public function getFirstLine(): ?float { return $this->firstLine; } /** * Set first line. - * - * @param float|int $value - * - * @return self */ - public function setFirstLine($value) + public function setFirstLine(?float $value): self { - $this->firstLine = $this->setNumericVal($value, $this->firstLine); + $this->firstLine = $this->setNumericVal($value); + + return $this; + } + + /** + * Get first line chars. + */ + public function getFirstLineChars(): int + { + return $this->firstLineChars; + } + + /** + * Set first line chars. + */ + public function setFirstLineChars(int $value): self + { + $this->firstLineChars = $this->setIntVal($value, $this->firstLineChars); return $this; } /** * Get hanging. - * - * @return float|int */ - public function getHanging() + public function getHanging(): ?float { return $this->hanging; } /** * Set hanging. - * - * @param float|int $value - * - * @return self */ - public function setHanging($value = null) + public function setHanging(?float $value = null): self { - $this->hanging = $this->setNumericVal($value, $this->hanging); + $this->hanging = $this->setNumericVal($value); return $this; } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Style/Language.php b/vendor/phpoffice/phpword/src/PhpWord/Style/Language.php index 18e7f76e..54e43765 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Style/Language.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Style/Language.php @@ -1,4 +1,5 @@ numId; } /** * Set Id. - * - * @param int $value - * - * @return self */ - public function setNumId($value) + public function setNumId(int $value): self { $this->numId = $this->setIntVal($value, $this->numId); @@ -78,22 +73,16 @@ class Numbering extends AbstractStyle /** * Get multilevel type. - * - * @return string */ - public function getType() + public function getType(): ?string { return $this->type; } /** * Set multilevel type. - * - * @param string $value - * - * @return self */ - public function setType($value) + public function setType(string $value): self { $enum = ['singleLevel', 'multilevel', 'hybridMultilevel']; $this->type = $this->setEnumVal($value, $enum, $this->type); @@ -106,19 +95,15 @@ class Numbering extends AbstractStyle * * @return NumberingLevel[] */ - public function getLevels() + public function getLevels(): array { return $this->levels; } /** * Set multilevel type. - * - * @param array $values - * - * @return self */ - public function setLevels($values) + public function setLevels(array $values): self { if (is_array($values)) { foreach ($values as $key => $value) { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Style/NumberingLevel.php b/vendor/phpoffice/phpword/src/PhpWord/Style/NumberingLevel.php index 39c0d839..31ec3738 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Style/NumberingLevel.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Style/NumberingLevel.php @@ -1,4 +1,5 @@ getChildStyleValue($this->indentation, 'hanging'); + } + /** * Get indentation. * - * @return null|\PhpOffice\PhpWord\Style\Indentation + * @deprecated 1.4.0 Use getIndentLeft */ - public function getIndentation() + public function getIndent(): ?float + { + return $this->getChildStyleValue($this->indentation, 'left'); + } + + /** + * Get indentation. + */ + public function getIndentation(): ?Indentation { return $this->indentation; } /** - * Set shading. - * - * @param mixed $value - * - * @return self + * Get firstLine. */ - public function setIndentation($value = null) + public function getIndentFirstLine(): ?float { + return $this->getChildStyleValue($this->indentation, 'firstLine'); + } + + /** + * Get left indentation. + */ + public function getIndentLeft(): ?float + { + return $this->getChildStyleValue($this->indentation, 'left'); + } + + /** + * Get right indentation. + */ + public function getIndentRight(): ?float + { + return $this->getChildStyleValue($this->indentation, 'right'); + } + + /** + * Set hanging. + * + * @deprecated 1.4.0 Use setIndentHanging + */ + public function setHanging(?float $value = null): self + { + return $this->setIndentation(['hanging' => $value]); + } + + /** + * Set indentation. + * + * @deprecated 1.4.0 Use setIndentLeft + */ + public function setIndent(?float $value = null): self + { + return $this->setIndentation(['left' => $value]); + } + + /** + * Set indentation. + * + * @param array{ + * left?:null|float|int|numeric-string, + * right?:null|float|int|numeric-string, + * hanging?:null|float|int|numeric-string, + * firstLine?:null|float|int|numeric-string + * } $value + */ + public function setIndentation(array $value = []): self + { + $value = array_map(function ($indent) { + if (is_string($indent) || is_numeric($indent)) { + $indent = $this->setFloatVal($indent); + } + + return $indent; + }, $value); $this->setObjectVal($value, 'Indentation', $this->indentation); return $this; } /** - * Get indentation. - * - * @return int + * Set hanging indentation. */ - public function getIndent() - { - return $this->getChildStyleValue($this->indentation, 'left'); - } - - /** - * Set indentation. - * - * @param int $value - * - * @return self - */ - public function setIndent($value = null) - { - return $this->setIndentation(['left' => $value]); - } - - /** - * Get hanging. - * - * @return int - */ - public function getHanging() - { - return $this->getChildStyleValue($this->indentation, 'hanging'); - } - - /** - * Set hanging. - * - * @param int $value - * - * @return self - */ - public function setHanging($value = null) + public function setIndentHanging(?float $value = null): self { return $this->setIndentation(['hanging' => $value]); } + /** + * Set firstline indentation. + */ + public function setIndentFirstLine(?float $value = null): self + { + return $this->setIndentation(['firstLine' => $value]); + } + + /** + * Set firstlineChars indentation. + */ + public function setIndentFirstLineChars(int $value = 0): self + { + return $this->setIndentation(['firstLineChars' => $value]); + } + + /** + * Set left indentation. + */ + public function setIndentLeft(?float $value = null): self + { + return $this->setIndentation(['left' => $value]); + } + + /** + * Set right indentation. + */ + public function setIndentRight(?float $value = null): self + { + return $this->setIndentation(['right' => $value]); + } + /** * Get spacing. * - * @return \PhpOffice\PhpWord\Style\Spacing + * @return Spacing * * @todo Rename to getSpacing in 1.0 */ @@ -498,7 +565,7 @@ class Paragraph extends Border * * @param string $value Possible values are defined in LineSpacingRule * - * @return \PhpOffice\PhpWord\Style\Paragraph + * @return Paragraph */ public function setSpacingLineRule($value) { @@ -686,7 +753,7 @@ class Paragraph extends Border /** * Get tabs. * - * @return \PhpOffice\PhpWord\Style\Tab[] + * @return Tab[] */ public function getTabs() { @@ -712,7 +779,7 @@ class Paragraph extends Border /** * Get shading. * - * @return \PhpOffice\PhpWord\Style\Shading + * @return Shading */ public function getShading() { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Style/Row.php b/vendor/phpoffice/phpword/src/PhpWord/Style/Row.php index 765c54f8..31ae3ded 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Style/Row.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Style/Row.php @@ -1,4 +1,5 @@ zip()->AddFromString("word/media/image1.jpg", file_get_contents($file));
* To read a file: $templateProcessor->zip()->getFromName("word/media/image1.jpg"); * - * @return \PhpOffice\PhpWord\Shared\ZipArchive + * @return ZipArchive */ public function zip() { @@ -269,7 +269,7 @@ class TemplateProcessor */ protected static function ensureUtf8Encoded($subject) { - return $subject ? Text::toUTF8($subject) : ''; + return (null !== $subject) ? Text::toUTF8($subject) : ''; } /** @@ -281,7 +281,7 @@ class TemplateProcessor $objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName; $xmlWriter = new XMLWriter(); - /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */ + /** @var Writer\Word2007\Element\AbstractElement $elementWriter */ $elementWriter = new $objectClass($xmlWriter, $complexType, true); $elementWriter->write(); @@ -308,7 +308,7 @@ class TemplateProcessor $objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName; $xmlWriter = new XMLWriter(); - /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */ + /** @var Writer\Word2007\Element\AbstractElement $elementWriter */ $elementWriter = new $objectClass($xmlWriter, $complexType, false); $elementWriter->write(); @@ -362,10 +362,10 @@ class TemplateProcessor /** * Set values from a one-dimensional array of "variable => value"-pairs. */ - public function setValues(array $values): void + public function setValues(array $values, int $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT): void { foreach ($values as $macro => $replace) { - $this->setValue($macro, $replace); + $this->setValue($macro, $replace, $limit); } } @@ -406,7 +406,7 @@ class TemplateProcessor $filename = "charts/chart{$rId}.xml"; // Get the part writer - $writerPart = new \PhpOffice\PhpWord\Writer\Word2007\Part\Chart(); + $writerPart = new Writer\Word2007\Part\Chart(); $writerPart->setElement($chart); // ContentTypes.xml @@ -499,20 +499,22 @@ class TemplateProcessor $widthFloat = $heightFloat * $imageRatio; $matches = []; preg_match('/\\d([a-z%]+)$/', $height, $matches); - $width = $widthFloat . $matches[1]; + $width = $widthFloat . (!empty($matches) ? $matches[1] : 'px'); } elseif ($height === '') { // defined height is empty $widthFloat = (float) $width; $heightFloat = $widthFloat / $imageRatio; $matches = []; preg_match('/\\d([a-z%]+)$/', $width, $matches); - $height = $heightFloat . $matches[1]; + $height = $heightFloat . (!empty($matches) ? $matches[1] : 'px'); } else { // we have defined size, but we need also check it aspect ratio $widthMatches = []; preg_match('/\\d([a-z%]+)$/', $width, $widthMatches); $heightMatches = []; preg_match('/\\d([a-z%]+)$/', $height, $heightMatches); // try to fix only if dimensions are same - if ($widthMatches[1] == $heightMatches[1]) { + if (!empty($widthMatches) + && !empty($heightMatches) + && $widthMatches[1] == $heightMatches[1]) { $dimention = $widthMatches[1]; $widthFloat = (float) $width; $heightFloat = (float) $height; @@ -1287,7 +1289,7 @@ class TemplateProcessor * @param int $count * @param string $xmlBlock * - * @return string + * @return array */ protected function indexClonedVariables($count, $xmlBlock) { @@ -1339,7 +1341,7 @@ class TemplateProcessor * @param string $block New block content * @param string $blockType XML tag type of block * - * @return \PhpOffice\PhpWord\TemplateProcessor Fluent interface + * @return TemplateProcessor Fluent interface */ public function replaceXmlBlock($macro, $block, $blockType = 'w:p') { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/AbstractWriter.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/AbstractWriter.php index 8ebf98c7..13859d82 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/AbstractWriter.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/AbstractWriter.php @@ -1,4 +1,5 @@ parts as $partName) { $partClass = 'PhpOffice\\PhpWord\\Writer\\HTML\\Part\\' . $partName; if (class_exists($partClass)) { - /** @var \PhpOffice\PhpWord\Writer\HTML\Part\AbstractPart $part Type hint */ + /** @var HTML\Part\AbstractPart $part Type hint */ $part = new $partClass(); $part->setParentWriter($this); $this->writerParts[strtolower($partName)] = $part; diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/AbstractElement.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/AbstractElement.php index 7c7bde31..0e6a112e 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/AbstractElement.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/AbstractElement.php @@ -1,4 +1,5 @@ namespace, $elementClass); if (class_exists($writerClass)) { - /** @var \PhpOffice\PhpWord\Writer\HTML\Element\AbstractElement $writer Type hint */ + /** @var AbstractElement $writer Type hint */ $writer = new $writerClass($this->parentWriter, $element, $withoutP); $content .= $writer->write(); } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/Endnote.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/Endnote.php index 1c35e8fa..7e7f31d4 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/Endnote.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/Endnote.php @@ -1,4 +1,5 @@ element; - $text = $this->parentWriter->escapeHTML($element->getText()); + $text = $this->parentWriter->escapeHTML($element->getText() ?? ''); if (!$this->withoutP && !trim($text)) { $text = ' '; } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/TextBreak.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/TextBreak.php index af73cb4a..576f6a83 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/TextBreak.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Element/TextBreak.php @@ -1,4 +1,5 @@ ' . PHP_EOL; - + $defaultFontColor = Settings::getDefaultFontColor(); // Default styles $astarray = [ 'font-family' => $this->getFontFamily(Settings::getDefaultFontName(), $this->getParentWriter()->getDefaultGenericFont()), 'font-size' => Settings::getDefaultFontSize() . 'pt', + 'color' => "#{$defaultFontColor}", ]; // Mpdf sometimes needs separate tag for body; doesn't harm others. $bodyarray = $astarray; diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Style/AbstractStyle.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Style/AbstractStyle.php index a6507867..4672347b 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Style/AbstractStyle.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/HTML/Style/AbstractStyle.php @@ -1,4 +1,5 @@ getVAlign(); + } foreach (['Top', 'Left', 'Bottom', 'Right'] as $direction) { $method = 'getBorder' . $direction . 'Style'; diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText.php index 616119e5..c9a524e8 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText.php @@ -1,4 +1,5 @@ parts) as $partName) { $partClass = static::class . '\\Part\\' . $partName; if (class_exists($partClass)) { - /** @var \PhpOffice\PhpWord\Writer\ODText\Part\AbstractPart $partObject Type hint */ + /** @var AbstractPart $partObject Type hint */ $partObject = new $partClass(); $partObject->setParentWriter($this); $this->writerParts[strtolower($partName)] = $partObject; diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/AbstractElement.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/AbstractElement.php index 5cd396aa..97d1875c 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/AbstractElement.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/AbstractElement.php @@ -1,4 +1,5 @@ startElement('text:s'); + $xmlWriter->writeAttributeIf($num > 1, 'text:c', "$num"); + $xmlWriter->endElement(); + $text = preg_replace('/^ +/', '', $text); + } + preg_match_all('/([\\s\\S]*?)(\\t| +| ?$)/', $text, $matches, PREG_SET_ORDER); + foreach ($matches as $match) { + $this->writeText($match[1]); + if ($match[2] === '') { + break; + } elseif ($match[2] === "\t") { + $xmlWriter->writeElement('text:tab'); + } elseif ($match[2] === ' ') { + $xmlWriter->writeElement('text:s'); + + break; + } else { + $num = strlen($match[2]); + $xmlWriter->startElement('text:s'); + $xmlWriter->writeAttributeIf($num > 1, 'text:c', "$num"); + $xmlWriter->endElement(); + } + } + } } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Container.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Container.php index 6e6b88ea..6b0fee8c 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Container.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Container.php @@ -1,4 +1,5 @@ + */ + protected $containerWithoutP = ['TextRun', 'Footnote', 'Endnote']; } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Field.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Field.php index 46f62b0f..6b548078 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Field.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Field.php @@ -1,4 +1,5 @@ getXmlWriter(); $element = $this->getElement(); - if (!$element instanceof \PhpOffice\PhpWord\Element\Image) { + if (!$element instanceof ElementImage) { return; } @@ -43,11 +44,16 @@ class Image extends AbstractElement $width = Converter::pixelToCm($style->getWidth()); $height = Converter::pixelToCm($style->getHeight()); - $xmlWriter->startElement('text:p'); - $xmlWriter->writeAttribute('text:style-name', 'IM' . $mediaIndex); + $xmlWriter = $this->getXmlWriter(); + + if (!$this->withoutP) { + $xmlWriter->startElement('text:p'); + $xmlWriter->writeAttribute('text:style-name', 'IM' . $mediaIndex); + } $xmlWriter->startElement('draw:frame'); $xmlWriter->writeAttribute('draw:style-name', 'fr' . $mediaIndex); + $xmlWriter->writeAttributeIf($this->withoutP, 'draw:text-style-name', 'IM' . $mediaIndex); $xmlWriter->writeAttribute('draw:name', $element->getElementId()); $xmlWriter->writeAttribute('text:anchor-type', 'as-char'); $xmlWriter->writeAttribute('svg:width', $width . 'cm'); @@ -63,6 +69,8 @@ class Image extends AbstractElement $xmlWriter->endElement(); // draw:frame - $xmlWriter->endElement(); // text:p + if (!$this->withoutP) { + $xmlWriter->endElement(); // text:p + } } } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Link.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Link.php index 0375b11b..9ef35692 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Link.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Link.php @@ -1,4 +1,5 @@ getXmlWriter(); $element = $this->getElement(); - if (!$element instanceof \PhpOffice\PhpWord\Element\Table) { + if (!$element instanceof TableElement) { return; } $rows = $element->getRows(); @@ -77,7 +78,7 @@ class Table extends AbstractElement private function writeRow(XMLWriter $xmlWriter, RowElement $row): void { $xmlWriter->startElement('table:table-row'); - /** @var \PhpOffice\PhpWord\Element\Row $row Type hint */ + /** @var RowElement $row Type hint */ foreach ($row->getCells() as $cell) { $xmlWriter->startElement('table:table-cell'); $xmlWriter->writeAttribute('office:value-type', 'string'); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Text.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Text.php index 75fb9308..39969723 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Text.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/Text.php @@ -1,4 +1,5 @@ writeAttribute('text:change-id', $element->getTrackChange()->getElementId()); $xmlWriter->endElement(); } else { - if (empty($fontStyle)) { - if (empty($paragraphStyle)) { - if (!$this->withoutP) { - $xmlWriter->writeAttribute('text:style-name', 'Normal'); - } - } elseif (is_string($paragraphStyle)) { - if (!$this->withoutP) { - $xmlWriter->writeAttribute('text:style-name', $paragraphStyle); - } + if (empty($paragraphStyle)) { + if (!$this->withoutP) { + $xmlWriter->writeAttribute('text:style-name', 'Normal'); } - $this->writeChangeInsertion(true, $element->getTrackChange()); - $this->replaceTabs($element->getText(), $xmlWriter); - $this->writeChangeInsertion(false, $element->getTrackChange()); - } else { - if (empty($paragraphStyle)) { - if (!$this->withoutP) { - $xmlWriter->writeAttribute('text:style-name', 'Normal'); - } - } elseif (is_string($paragraphStyle)) { - if (!$this->withoutP) { - $xmlWriter->writeAttribute('text:style-name', $paragraphStyle); - } + } elseif (is_string($paragraphStyle)) { + if (!$this->withoutP) { + $xmlWriter->writeAttribute('text:style-name', $paragraphStyle); } + } + + if (!empty($fontStyle)) { // text:span $xmlWriter->startElement('text:span'); if (is_string($fontStyle)) { $xmlWriter->writeAttribute('text:style-name', $fontStyle); } - $this->writeChangeInsertion(true, $element->getTrackChange()); - $this->replaceTabs($element->getText(), $xmlWriter); - $this->writeChangeInsertion(false, $element->getTrackChange()); + } + + $this->writeChangeInsertion(true, $element->getTrackChange()); + $this->replaceTabs($element->getText(), $xmlWriter); + $this->writeChangeInsertion(false, $element->getTrackChange()); + + if (!empty($fontStyle)) { $xmlWriter->endElement(); } } @@ -96,35 +89,6 @@ class Text extends AbstractElement } } - private function replacetabs($text, $xmlWriter): void - { - if (preg_match('/^ +/', $text, $matches)) { - $num = strlen($matches[0]); - $xmlWriter->startElement('text:s'); - $xmlWriter->writeAttributeIf($num > 1, 'text:c', "$num"); - $xmlWriter->endElement(); - $text = preg_replace('/^ +/', '', $text); - } - preg_match_all('/([\\s\\S]*?)(\\t| +| ?$)/', $text, $matches, PREG_SET_ORDER); - foreach ($matches as $match) { - $this->writeText($match[1]); - if ($match[2] === '') { - break; - } elseif ($match[2] === "\t") { - $xmlWriter->writeElement('text:tab'); - } elseif ($match[2] === ' ') { - $xmlWriter->writeElement('text:s'); - - break; - } else { - $num = strlen($match[2]); - $xmlWriter->startElement('text:s'); - $xmlWriter->writeAttributeIf($num > 1, 'text:c', "$num"); - $xmlWriter->endElement(); - } - } - } - private function writeChangeInsertion($start = true, ?TrackChange $trackChange = null): void { if ($trackChange == null || $trackChange->getChangeType() != TrackChange::INSERTED) { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/TextBreak.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/TextBreak.php index 1bfe3988..1a697007 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/TextBreak.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Element/TextBreak.php @@ -1,4 +1,5 @@ imageParagraphStyles as $style) { - $styleWriter = new \PhpOffice\PhpWord\Writer\ODText\Style\Paragraph($xmlWriter, $style); + $styleWriter = new ParagraphStyleWriter($xmlWriter, $style); $styleWriter->write(); } } @@ -256,7 +257,7 @@ class Content extends AbstractPart * * Table style can be null or string of the style name * - * @param \PhpOffice\PhpWord\Element\AbstractContainer $container + * @param AbstractContainer $container * @param int $paragraphStyleCount * @param int $fontStyleCount * @@ -277,7 +278,7 @@ class Content extends AbstractPart $style = $element->getStyle(); $style->setStyleName('fr' . $element->getMediaIndex()); $this->autoStyles['Image'][] = $style; - $sty = new \PhpOffice\PhpWord\Style\Paragraph(); + $sty = new Paragraph(); $sty->setStyleName('IM' . $element->getMediaIndex()); $sty->setAuto(); $sty->setAlignment($style->getAlignment()); @@ -300,7 +301,7 @@ class Content extends AbstractPart /** * Get style of individual element. * - * @param \PhpOffice\PhpWord\Element\Text $element + * @param Text $element * @param int $paragraphStyleCount * @param int $fontStyleCount */ @@ -346,7 +347,7 @@ class Content extends AbstractPart /** * Get font style of individual field element. * - * @param \PhpOffice\PhpWord\Element\Field $element + * @param Field $element * @param int $fontStyleCount */ private function getElementStyleField($element, &$fontStyleCount): void @@ -371,7 +372,7 @@ class Content extends AbstractPart /** * Get style of individual element. * - * @param \PhpOffice\PhpWord\Element\TextRun $element + * @param TextRun $element * @param int $paragraphStyleCount */ private function getElementStyleTextRun($element, &$paragraphStyleCount): void diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Part/Manifest.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Part/Manifest.php index 37fb7979..200da158 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Part/Manifest.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Part/Manifest.php @@ -1,4 +1,5 @@ startElement('style:text-properties'); - $xmlWriter->writeAttribute('style:use-window-font-color', 'true'); + $xmlWriter->writeAttribute('style:use-window-font-color', 'false'); $xmlWriter->writeAttribute('style:font-name', Settings::getDefaultFontName()); $xmlWriter->writeAttribute('fo:font-size', Settings::getDefaultFontSize() . 'pt'); $xmlWriter->writeAttribute('fo:language', $latinLang[0]); $xmlWriter->writeAttribute('fo:country', $latinLang[1]); + $xmlWriter->writeAttribute('fo:color', '#' . Settings::getDefaultFontColor()); $xmlWriter->writeAttribute('style:letter-kerning', 'true'); $xmlWriter->writeAttribute('style:font-name-asian', Settings::getDefaultFontName() . '2'); $xmlWriter->writeAttribute('style:font-size-asian', Settings::getDefaultFontSize() . 'pt'); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/AbstractStyle.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/AbstractStyle.php index 439434c9..3545009f 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/AbstractStyle.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/AbstractStyle.php @@ -1,4 +1,5 @@ setStyleName($style->getStyleName()); - $temp2 = new \PhpOffice\PhpWord\Writer\ODText\Style\Paragraph($xmlWriter, $temp1); + $temp2 = new Paragraph($xmlWriter, $temp1); $temp2->write(); } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Image.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Image.php index 79ddfc50..56c4f57a 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Image.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Image.php @@ -1,4 +1,5 @@ getStyle(); - if (!$style instanceof \PhpOffice\PhpWord\Style\Paragraph) { + if (!$style instanceof Style\Paragraph) { return; } $xmlWriter = $this->getXmlWriter(); @@ -73,13 +74,13 @@ class Paragraph extends AbstractStyle } elseif (substr($styleName, 0, 2) === 'HD') { $styleAuto = true; $psm = 'Heading_' . substr($styleName, 2); - $stylep = \PhpOffice\PhpWord\Style::getStyle($psm); - if ($stylep instanceof \PhpOffice\PhpWord\Style\Font) { + $stylep = Style::getStyle($psm); + if ($stylep instanceof Style\Font) { if (method_exists($stylep, 'getParagraph')) { $stylep = $stylep->getParagraph(); } } - if ($stylep instanceof \PhpOffice\PhpWord\Style\Paragraph) { + if ($stylep instanceof Style\Paragraph) { if ($stylep->hasPageBreakBefore()) { $breakbefore = true; } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Section.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Section.php index 0a250194..05152a39 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Section.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/ODText/Style/Section.php @@ -1,4 +1,5 @@ parts as $partName) { $partClass = static::class . '\\Part\\' . $partName; if (class_exists($partClass)) { - /** @var \PhpOffice\PhpWord\Writer\RTF\Part\AbstractPart $part Type hint */ + /** @var RTF\Part\AbstractPart $part Type hint */ $part = new $partClass(); $part->setParentWriter($this); $this->writerParts[strtolower($partName)] = $part; diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/AbstractElement.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/AbstractElement.php index 5c33868a..e007e6aa 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/AbstractElement.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/AbstractElement.php @@ -1,4 +1,5 @@ parentWriter; /** @var \PhpOffice\PhpWord\Element\Text $element Type hint */ @@ -188,7 +189,7 @@ abstract class AbstractElement return ''; } - /** @var \PhpOffice\PhpWord\Writer\RTF $parentWriter Type hint */ + /** @var WriterRTF $parentWriter Type hint */ $parentWriter = $this->parentWriter; // Create style writer and set color/name index diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/Container.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/Container.php index 5e198aec..dcac8ec0 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/Container.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Element/Container.php @@ -1,4 +1,5 @@ element; $elementClass = str_replace('\\Writer\\RTF', '', static::class); - if (!$element instanceof $elementClass || !is_string($element->getText())) { + if (!$element instanceof $elementClass) { return ''; } + $textToWrite = $element->getText(); + if ($textToWrite instanceof \PhpOffice\PhpWord\Element\TextRun) { + $textToWrite = $textToWrite->getText(); // gets text from TextRun + } + $this->getStyles(); $content = ''; @@ -82,7 +88,7 @@ class Title extends Text $content .= '{'; $content .= $this->writeFontStyle(); - $content .= $this->writeText($element->getText()); + $content .= $this->writeText($textToWrite); $content .= '}'; $content .= $this->writeClosing(); $content .= $endout; diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/AbstractPart.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/AbstractPart.php index be772b93..a07f70bb 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/AbstractPart.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/AbstractPart.php @@ -1,4 +1,5 @@ getHeaders() as $header) { $type = $header->getType(); - if ($evenOdd || $type !== FOOTER::EVEN) { + if ($evenOdd || $type !== Footer::EVEN) { $content .= '{\\header'; if ($type === Footer::FIRST) { $content .= 'f'; } elseif ($evenOdd) { - $content .= ($type === FOOTER::EVEN) ? 'l' : 'r'; + $content .= ($type === Footer::EVEN) ? 'l' : 'r'; } foreach ($header->getElements() as $element) { $cl = get_class($element); @@ -182,12 +183,12 @@ class Document extends AbstractPart } foreach ($section->getFooters() as $footer) { $type = $footer->getType(); - if ($evenOdd || $type !== FOOTER::EVEN) { + if ($evenOdd || $type !== Footer::EVEN) { $content .= '{\\footer'; if ($type === Footer::FIRST) { $content .= 'f'; } elseif ($evenOdd) { - $content .= ($type === FOOTER::EVEN) ? 'l' : 'r'; + $content .= ($type === Footer::EVEN) ? 'l' : 'r'; } foreach ($footer->getElements() as $element) { $cl = get_class($element); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/Header.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/Header.php index 7f8cc84b..97644fe4 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/Header.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/RTF/Part/Header.php @@ -1,4 +1,5 @@ parts) as $partName) { $partClass = static::class . '\\Part\\' . $partName; if (class_exists($partClass)) { - /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $part Type hint */ + /** @var Word2007\Part\AbstractPart $part Type hint */ $part = new $partClass(); $part->setParentWriter($this); $this->writerParts[strtolower($partName)] = $part; @@ -179,7 +178,7 @@ class Word2007 extends AbstractWriter implements WriterInterface $this->registerContentTypes($media); } - /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $writerPart Type hint */ + /** @var Word2007\Part\AbstractPart $writerPart Type hint */ $writerPart = $this->getWriterPart('relspart')->setMedia($media); $zip->addFromString("word/_rels/{$file}.xml.rels", $writerPart->write()); } @@ -206,7 +205,7 @@ class Word2007 extends AbstractWriter implements WriterInterface $this->contentTypes['override']["/word/$elmFile"] = $elmType; $this->relationships[] = ['target' => $elmFile, 'type' => $elmType, 'rID' => $rId]; - /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $writerPart Type hint */ + /** @var Word2007\Part\AbstractPart $writerPart Type hint */ $writerPart = $this->getWriterPart($elmType)->setElement($element); $zip->addFromString("word/$elmFile", $writerPart->write()); } @@ -236,7 +235,7 @@ class Word2007 extends AbstractWriter implements WriterInterface // Write relationships file, e.g. word/_rels/footnotes.xml if (!empty($media)) { - /** @var \PhpOffice\PhpWord\Writer\Word2007\Part\AbstractPart $writerPart Type hint */ + /** @var Word2007\Part\AbstractPart $writerPart Type hint */ $writerPart = $this->getWriterPart('relspart')->setMedia($media); $zip->addFromString("word/_rels/{$partName}.xml.rels", $writerPart->write()); } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/AbstractElement.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/AbstractElement.php index b677556d..5743c8c7 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/AbstractElement.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/AbstractElement.php @@ -1,4 +1,5 @@ xmlWriter = $xmlWriter; $this->element = $element; @@ -76,7 +75,7 @@ abstract class AbstractElement /** * Get XML Writer. * - * @return \PhpOffice\PhpWord\Shared\XMLWriter + * @return XMLWriter */ protected function getXmlWriter() { @@ -86,7 +85,7 @@ abstract class AbstractElement /** * Get element. * - * @return \PhpOffice\PhpWord\Element\AbstractElement + * @return Element */ protected function getElement() { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Bookmark.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Bookmark.php index 1e618af9..ba61ad69 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Bookmark.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Bookmark.php @@ -1,4 +1,5 @@ + */ + protected $containerWithoutP = ['TextRun', 'Footnote', 'Endnote', 'ListItemRun']; + /** * Write element. */ @@ -46,7 +52,7 @@ class Container extends AbstractElement return; } $containerClass = substr(get_class($container), strrpos(get_class($container), '\\') + 1); - $withoutP = in_array($containerClass, ['TextRun', 'Footnote', 'Endnote', 'ListItemRun']); + $withoutP = in_array($containerClass, $this->containerWithoutP); $xmlWriter = $this->getXmlWriter(); // Loop through elements @@ -62,7 +68,7 @@ class Container extends AbstractElement $writeLastTextBreak = ($containerClass == 'Cell') && ($elementClass == '' || $elementClass == 'Table'); if ($writeLastTextBreak) { $writerClass = $this->namespace . '\\TextBreak'; - /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $writer Type hint */ + /** @var AbstractElement $writer Type hint */ $writer = new $writerClass($xmlWriter, new TextBreakElement(), $withoutP); $writer->write(); } @@ -70,18 +76,14 @@ class Container extends AbstractElement /** * Write individual element. - * - * @param bool $withoutP - * - * @return string */ - private function writeElement(XMLWriter $xmlWriter, Element $element, $withoutP) + private function writeElement(XMLWriter $xmlWriter, Element $element, bool $withoutP): string { $elementClass = substr(get_class($element), strrpos(get_class($element), '\\') + 1); $writerClass = $this->namespace . '\\' . $elementClass; if (class_exists($writerClass)) { - /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $writer Type hint */ + /** @var AbstractElement $writer Type hint */ $writer = new $writerClass($xmlWriter, $element, $withoutP); $writer->setPart($this->getPart()); $writer->write(); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Endnote.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Endnote.php index f96ac797..6a00ed5b 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Endnote.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Endnote.php @@ -1,4 +1,5 @@ endElement(); // w:r if ($element->getText() != null) { - if ($element->getText() instanceof \PhpOffice\PhpWord\Element\TextRun) { + if ($element->getText() instanceof TextRun) { $containerWriter = new Container($xmlWriter, $element->getText(), true); $containerWriter->write(); } @@ -262,7 +263,7 @@ class Field extends Text $xmlWriter->endElement(); // w:r if ($element->getText() != null) { - if ($element->getText() instanceof \PhpOffice\PhpWord\Element\TextRun) { + if ($element->getText() instanceof TextRun) { $containerWriter = new Container($xmlWriter, $element->getText(), true); $containerWriter->write(); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Footnote.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Footnote.php index 77073a23..68f998e3 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Footnote.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Footnote.php @@ -1,4 +1,5 @@ startElement('w:r'); $xmlWriter->startElement('w:instrText'); $xmlWriter->writeAttribute('xml:space', 'preserve'); - $xmlWriter->text("PAGEREF _Toc{$rId} \\h"); + $xmlWriter->text("PAGEREF $rId \\h"); $xmlWriter->endElement(); $xmlWriter->endElement(); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Table.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Table.php index a32cc196..2bb1b3f3 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Table.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/Table.php @@ -1,4 +1,5 @@ getBgColor()) { $xmlWriter->writeAttribute('fillcolor', $style->getBgColor()); + } else { + $xmlWriter->writeAttribute('filled', 'f'); + } + + if (!$style->getBorderColor()) { + $xmlWriter->writeAttribute('stroked', 'f'); + $xmlWriter->writeAttribute('strokecolor', 'white'); } $styleWriter->write(); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/TextBreak.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/TextBreak.php index bcae3b2e..4c2ecde7 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/TextBreak.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Element/TextBreak.php @@ -1,4 +1,5 @@ getSalt() == null) { - $documentProtection->setSalt(openssl_random_pseudo_bytes(16)); + $documentProtection->setSalt((string) openssl_random_pseudo_bytes(16)); } $passwordHash = PasswordEncoder::hashPassword($documentProtection->getPassword(), $documentProtection->getAlgorithm(), $documentProtection->getSalt(), $documentProtection->getSpinCount()); $this->settings['w:documentProtection'] = [ diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Part/Styles.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Part/Styles.php index 2112fd3c..edf0314c 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Part/Styles.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Part/Styles.php @@ -1,4 +1,5 @@ getParentWriter()->getPhpWord(); $fontName = $phpWord->getDefaultFontName(); + $asianFontName = $phpWord->getDefaultAsianFontName(); $fontSize = $phpWord->getDefaultFontSize(); + $fontColor = $phpWord->getDefaultFontColor(); $language = $phpWord->getSettings()->getThemeFontLang(); $latinLanguage = ($language == null || $language->getLatin() === null) ? 'en-US' : $language->getLatin(); @@ -94,9 +97,12 @@ class Styles extends AbstractPart $xmlWriter->startElement('w:rFonts'); $xmlWriter->writeAttribute('w:ascii', $fontName); $xmlWriter->writeAttribute('w:hAnsi', $fontName); - $xmlWriter->writeAttribute('w:eastAsia', $fontName); + $xmlWriter->writeAttribute('w:eastAsia', $asianFontName); $xmlWriter->writeAttribute('w:cs', $fontName); $xmlWriter->endElement(); // w:rFonts + $xmlWriter->startElement('w:color'); + $xmlWriter->writeAttribute('w:val', $fontColor); + $xmlWriter->endElement(); $xmlWriter->startElement('w:sz'); $xmlWriter->writeAttribute('w:val', $fontSize * 2); $xmlWriter->endElement(); // w:sz @@ -125,7 +131,7 @@ class Styles extends AbstractPart if (isset($styles['Normal'])) { $normalStyle = $styles['Normal']; // w:pPr - if ($normalStyle instanceof Fontstyle && $normalStyle->getParagraph() != null) { + if ($normalStyle instanceof FontStyle && $normalStyle->getParagraph() != null) { $styleWriter = new ParagraphStyleWriter($xmlWriter, $normalStyle->getParagraph()); $styleWriter->write(); } elseif ($normalStyle instanceof ParagraphStyle) { diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Part/Theme.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Part/Theme.php index ad57d664..a70c248d 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Part/Theme.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Part/Theme.php @@ -1,4 +1,5 @@ write(); } diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Cell.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Cell.php index 6e22597d..bb0d6d71 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Cell.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Cell.php @@ -1,4 +1,5 @@ endElement(); // w:tcW } + $paddingTop = $style->getPaddingTop(); + $paddingLeft = $style->getPaddingLeft(); + $paddingBottom = $style->getPaddingBottom(); + $paddingRight = $style->getPaddingRight(); + + if ($paddingTop !== null || $paddingLeft !== null || $paddingBottom !== null || $paddingRight !== null) { + $xmlWriter->startElement('w:tcMar'); + if ($paddingTop !== null) { + $xmlWriter->startElement('w:top'); + $xmlWriter->writeAttribute('w:w', $paddingTop); + $xmlWriter->writeAttribute('w:type', \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP); + $xmlWriter->endElement(); // w:top + } + if ($paddingLeft !== null) { + $xmlWriter->startElement('w:start'); + $xmlWriter->writeAttribute('w:w', $paddingLeft); + $xmlWriter->writeAttribute('w:type', \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP); + $xmlWriter->endElement(); // w:start + } + if ($paddingBottom !== null) { + $xmlWriter->startElement('w:bottom'); + $xmlWriter->writeAttribute('w:w', $paddingBottom); + $xmlWriter->writeAttribute('w:type', \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP); + $xmlWriter->endElement(); // w:bottom + } + if ($paddingRight !== null) { + $xmlWriter->startElement('w:end'); + $xmlWriter->writeAttribute('w:w', $paddingRight); + $xmlWriter->writeAttribute('w:type', \PhpOffice\PhpWord\SimpleType\TblWidth::TWIP); + $xmlWriter->endElement(); // w:end + } + $xmlWriter->endElement(); // w:tcMar + } + // Text direction $textDir = $style->getTextDirection(); $xmlWriter->writeElementIf(null !== $textDir, 'w:textDirection', 'w:val', $textDir); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Extrusion.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Extrusion.php index f6ad6221..8bb92187 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Extrusion.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Extrusion.php @@ -1,4 +1,5 @@ getFirstLine(); $xmlWriter->writeAttributeIf(null !== $firstLine, 'w:firstLine', $this->convertTwip($firstLine)); + $firstLineChars = $style->getFirstLineChars(); + $xmlWriter->writeAttributeIf(0 !== $firstLineChars, 'w:firstLineChars', $this->convertTwip($firstLineChars)); + $hanging = $style->getHanging(); $xmlWriter->writeAttributeIf(null !== $hanging, 'w:hanging', $this->convertTwip($hanging)); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Line.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Line.php index 90107f8e..2603545f 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Line.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Line.php @@ -1,4 +1,5 @@ startElement('w:numPr'); diff --git a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Row.php b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Row.php index 7e1468e8..2b9d804f 100644 --- a/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Row.php +++ b/vendor/phpoffice/phpword/src/PhpWord/Writer/Word2007/Style/Row.php @@ -1,4 +1,5 @@ =8.0.0" + "php": ">=5.3.0" }, "autoload": { "psr-4": { - "Psr\\Log\\": "src" + "Psr\\Log\\": "Psr/Log/" } }, "extra": { "branch-alias": { - "dev-master": "3.x-dev" + "dev-master": "1.1.x-dev" } } } diff --git a/vendor/topthink/think-image/src/Image.php b/vendor/topthink/think-image/src/Image.php index e28a13d7..b7301ba0 100644 --- a/vendor/topthink/think-image/src/Image.php +++ b/vendor/topthink/think-image/src/Image.php @@ -140,6 +140,31 @@ class Image return $this; } + /** + * http输出图片 + * @return void + */ + public function output() + { + $type = $this->info['type']; + header("content-type: image/{$type}"); + switch ($type) { + case 'png': + imagepng($this->im); + break; + case 'gif': + imagegif($this->im); + break; + case 'jpeg': + imagejpeg($this->im); + break; + case 'wbmp': + imagewbmp($this->im); + break; + } + exit; + } + /** * 返回图像宽度 * @return int 图像宽度 @@ -257,16 +282,17 @@ class Image public function crop($w, $h, $x = 0, $y = 0, $width = null, $height = null) { //设置保存尺寸 + empty($width) && $width = $w; empty($height) && $height = $h; do { //创建新图像 - $img = imagecreatetruecolor($width, $height); + $img = imagecreatetruecolor((int) $width, (int) $height); // 调整默认颜色 $color = imagecolorallocate($img, 255, 255, 255); imagefill($img, 0, 0, $color); //裁剪 - imagecopyresampled($img, $this->im, 0, 0, $x, $y, $width, $height, $w, $h); + imagecopyresampled($img, $this->im, 0, 0, (int) $x, (int) $y, (int) $width, (int) $height, $w, $h); imagedestroy($this->im); //销毁原图 //设置新图像 $this->im = $img; @@ -302,18 +328,18 @@ class Image $scale = min($width / $w, $height / $h); //设置缩略图的坐标及宽度和高度 $x = $y = 0; - $width = $w * $scale; - $height = $h * $scale; + $width = (int) ($w * $scale); + $height = (int) ($h * $scale); break; /* 居中裁剪 */ case self::THUMB_CENTER: //计算缩放比例 $scale = max($width / $w, $height / $h); //设置缩略图的坐标及宽度和高度 - $w = $width / $scale; - $h = $height / $scale; - $x = ($this->info['width'] - $w) / 2; - $y = ($this->info['height'] - $h) / 2; + $w = (int) ($width / $scale); + $h = (int) ($height / $scale); + $x = (int) (($this->info['width'] - $w) / 2); + $y = (int) (($this->info['height'] - $h) / 2); break; /* 左上角裁剪 */ case self::THUMB_NORTHWEST: @@ -321,18 +347,18 @@ class Image $scale = max($width / $w, $height / $h); //设置缩略图的坐标及宽度和高度 $x = $y = 0; - $w = $width / $scale; - $h = $height / $scale; + $w = (int) ($width / $scale); + $h = (int) ($height / $scale); break; /* 右下角裁剪 */ case self::THUMB_SOUTHEAST: //计算缩放比例 $scale = max($width / $w, $height / $h); //设置缩略图的坐标及宽度和高度 - $w = $width / $scale; - $h = $height / $scale; - $x = $this->info['width'] - $w; - $y = $this->info['height'] - $h; + $w = (int) ($width / $scale); + $h = (int) ($height / $scale); + $x = (int) ($this->info['width'] - $w); + $y = (int) ($this->info['height'] - $h); break; /* 填充 */ case self::THUMB_FILLED: @@ -343,12 +369,12 @@ class Image $scale = min($width / $w, $height / $h); } //设置缩略图的坐标及宽度和高度 - $neww = $w * $scale; - $newh = $h * $scale; + $neww = (int) ($w * $scale); + $newh = (int) ($h * $scale); $x = $this->info['width'] - $w; $y = $this->info['height'] - $h; - $posx = ($width - $w * $scale) / 2; - $posy = ($height - $h * $scale) / 2; + $posx = (int) (($width - $w * $scale) / 2); + $posy = (int) (($height - $h * $scale) / 2); do { //创建新图像 $img = imagecreatetruecolor($width, $height); @@ -377,9 +403,9 @@ class Image /** * 添加水印 * - * @param string $source 水印图片路径 - * @param int $locate 水印位置 - * @param int $alpha 透明度 + * @param string $source 水印图片路径 + * @param int|array $locate 水印位置 + * @param int $alpha 透明度 * @return $this */ public function water($source, $locate = self::WATER_SOUTHEAST, $alpha = 100) @@ -457,10 +483,10 @@ class Image // 调整默认颜色 $color = imagecolorallocate($src, 255, 255, 255); imagefill($src, 0, 0, $color); - imagecopy($src, $this->im, 0, 0, $x, $y, $info[0], $info[1]); + imagecopy($src, $this->im, 0, 0, (int) $x, (int) $y, $info[0], $info[1]); imagecopy($src, $water, 0, 0, 0, 0, $info[0], $info[1]); - imagecopymerge($this->im, $src, $x, $y, 0, 0, $info[0], $info[1], $alpha); - //销毁零时图片资源 + imagecopymerge($this->im, $src, (int) $x, (int) $y, 0, 0, $info[0], $info[1], $alpha); + //销毁临时图片资源 imagedestroy($src); } while (!empty($this->gif) && $this->gifNext()); //销毁水印资源 @@ -471,13 +497,13 @@ class Image /** * 图像添加文字 * - * @param string $text 添加的文字 - * @param string $font 字体路径 - * @param integer $size 字号 - * @param string $color 文字颜色 - * @param int $locate 文字写入位置 - * @param integer $offset 文字相对当前位置的偏移量 - * @param integer $angle 文字倾斜角度 + * @param string $text 添加的文字 + * @param string $font 字体路径 + * @param integer $size 字号 + * @param string $color 文字颜色 + * @param int|array $locate 文字写入位置 + * @param integer|array $offset 文字相对当前位置的偏移量 + * @param integer $angle 文字倾斜角度 * * @return $this * @throws ImageException @@ -559,6 +585,24 @@ class Image $offset = intval($offset); $ox = $oy = $offset; } + /* 图片黑白检测 */ + if ("auto" == $color) { + //X方向采集宽度:单英文字符占据宽度约为字体大小/1.6,单中文字符占据宽度约为字体大小*4/3;Y方向采集宽度:英文字符高度约为字体大小,中文会高一些。 + //使用保守宽度,以免在纯英文情况下采集区域超出图像范围,并且精度完全可以满足本功能。 + $pickX = intval(mb_strwidth($text) * ($size / 1.6)); + $pickY = $size; + + $brightness = 0; + for ($i = $x + $ox; $i < $pickX + $x + $ox; $i++) { + //根据文字基线确定要进行遍历的像素 + for ($j = $y + $oy - $pickY; $j < $y + $oy; $j++) { + //基线修正 + $brightness += self::getBrightnessOfPixel($i, $j); + } + } + + $color = $brightness / ($pickX * $pickY) > 127 ? '#00000000' : '#ffffffff'; + } /* 设置颜色 */ if (is_string($color) && 0 === strpos($color, '#')) { $color = str_split(substr($color, 1), 2); @@ -577,6 +621,22 @@ class Image return $this; } + /** + * 获取图片指定像素点的亮度值 + */ + private function getBrightnessOfPixel($x, $y) + { + $rgb = imagecolorat($this->im, $x, $y); + $r = ($rgb >> 16) & 0xFF; + $g = ($rgb >> 8) & 0xFF; + $b = $rgb & 0xFF; + + //红绿蓝能量不同,亮度不同,对应系数也不同(参考https://www.w3.org/TR/AERT/#color-contrast) + $brightness = intval($r * 0.299 + $g * 0.587 + $b * 0.114); + + return $brightness; + } + /** * 切换到GIF的下一帧并保存当前帧 */ diff --git a/vendor/topthink/think-image/src/image/Exception.php b/vendor/topthink/think-image/src/image/Exception.php index 2ebafd8c..c35c71b9 100644 --- a/vendor/topthink/think-image/src/image/Exception.php +++ b/vendor/topthink/think-image/src/image/Exception.php @@ -11,8 +11,7 @@ namespace think\image; - class Exception extends \RuntimeException { -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/src/image/gif/Decoder.php b/vendor/topthink/think-image/src/image/gif/Decoder.php index a5630926..95fa2b84 100644 --- a/vendor/topthink/think-image/src/image/gif/Decoder.php +++ b/vendor/topthink/think-image/src/image/gif/Decoder.php @@ -11,7 +11,6 @@ namespace think\image\gif; - class Decoder { public $GIF_buffer = []; @@ -204,4 +203,4 @@ class Decoder { return ($this->GIF_delays); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/src/image/gif/Encoder.php b/vendor/topthink/think-image/src/image/gif/Encoder.php index 688f7a0a..763a1fd8 100644 --- a/vendor/topthink/think-image/src/image/gif/Encoder.php +++ b/vendor/topthink/think-image/src/image/gif/Encoder.php @@ -48,7 +48,7 @@ class Encoder for ($i = 0; $i < count($GIF_src); $i++) { if (strtolower($GIF_mod) == "url") { $this->BUF[] = fread(fopen($GIF_src[$i], "rb"), filesize($GIF_src[$i])); - } else if (strtolower($GIF_mod) == "bin") { + } elseif (strtolower($GIF_mod) == "bin") { $this->BUF[] = $GIF_src[$i]; } else { printf("%s: %s ( %s )!", $this->VER, $this->ERR['ERR02'], $GIF_mod); @@ -74,7 +74,7 @@ class Encoder } $this->addHeader(); for ($i = 0; $i < count($this->BUF); $i++) { - $this->addFrames($i, $GIF_dly[$i]); + isset($GIF_dly[$i]) && $this->addFrames($i, $GIF_dly[$i]); } $this->addFooter(); } @@ -219,4 +219,4 @@ class Encoder { return ($this->GIF); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/src/image/gif/Gif.php b/vendor/topthink/think-image/src/image/gif/Gif.php index b8909158..f803db0c 100644 --- a/vendor/topthink/think-image/src/image/gif/Gif.php +++ b/vendor/topthink/think-image/src/image/gif/Gif.php @@ -85,4 +85,4 @@ class Gif $gif = new Encoder($this->frames, $this->delays, 0, 2, 0, 0, 0, 'bin'); file_put_contents($pathname, $gif->getAnimation()); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/tests/CropTest.php b/vendor/topthink/think-image/tests/CropTest.php index 26a05313..e2676307 100644 --- a/vendor/topthink/think-image/tests/CropTest.php +++ b/vendor/topthink/think-image/tests/CropTest.php @@ -64,4 +64,4 @@ class CropTest extends TestCase @unlink($pathname); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/tests/FlipTest.php b/vendor/topthink/think-image/tests/FlipTest.php index bf7a5e26..d91d3af9 100644 --- a/vendor/topthink/think-image/tests/FlipTest.php +++ b/vendor/topthink/think-image/tests/FlipTest.php @@ -27,7 +27,6 @@ class FlipTest extends TestCase @unlink($pathname); } - public function testGif() { $pathname = TEST_PATH . 'tmp/flip.gif'; @@ -40,4 +39,4 @@ class FlipTest extends TestCase @unlink($pathname); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/tests/InfoTest.php b/vendor/topthink/think-image/tests/InfoTest.php index 22132ca4..97ff4861 100644 --- a/vendor/topthink/think-image/tests/InfoTest.php +++ b/vendor/topthink/think-image/tests/InfoTest.php @@ -37,7 +37,6 @@ class InfoTest extends TestCase $this->assertEquals([800, 600], $image->size()); } - public function testPng() { $image = Image::open($this->getPng()); @@ -57,4 +56,4 @@ class InfoTest extends TestCase $this->assertEquals('image/gif', $image->mime()); $this->assertEquals([380, 216], $image->size()); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/tests/RotateTest.php b/vendor/topthink/think-image/tests/RotateTest.php index 56572d27..202062d3 100644 --- a/vendor/topthink/think-image/tests/RotateTest.php +++ b/vendor/topthink/think-image/tests/RotateTest.php @@ -39,4 +39,4 @@ class RotateTest extends TestCase @unlink($pathname); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/tests/TestCase.php b/vendor/topthink/think-image/tests/TestCase.php index 89ba1b45..3fd401ce 100644 --- a/vendor/topthink/think-image/tests/TestCase.php +++ b/vendor/topthink/think-image/tests/TestCase.php @@ -30,4 +30,4 @@ abstract class TestCase extends \PHPUnit_Framework_TestCase { return new File(TEST_PATH . 'images/test.gif'); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/tests/TextTest.php b/vendor/topthink/think-image/tests/TextTest.php index 04506a27..87f04674 100644 --- a/vendor/topthink/think-image/tests/TextTest.php +++ b/vendor/topthink/think-image/tests/TextTest.php @@ -55,4 +55,4 @@ class TextTest extends TestCase @unlink($pathname); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/tests/ThumbTest.php b/vendor/topthink/think-image/tests/ThumbTest.php index 07113c8e..98354c45 100644 --- a/vendor/topthink/think-image/tests/ThumbTest.php +++ b/vendor/topthink/think-image/tests/ThumbTest.php @@ -103,7 +103,6 @@ class ThumbTest extends TestCase @unlink($pathname); } - public function testPng() { $pathname = TEST_PATH . 'tmp/thumb.png'; @@ -281,4 +280,4 @@ class ThumbTest extends TestCase @unlink($pathname); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/tests/WaterTest.php b/vendor/topthink/think-image/tests/WaterTest.php index b6a2bcce..7c2c1dac 100644 --- a/vendor/topthink/think-image/tests/WaterTest.php +++ b/vendor/topthink/think-image/tests/WaterTest.php @@ -55,4 +55,4 @@ class WaterTest extends TestCase @unlink($pathname); } -} \ No newline at end of file +} diff --git a/vendor/topthink/think-image/tests/autoload.php b/vendor/topthink/think-image/tests/autoload.php index f2e8aaef..d21954b2 100644 --- a/vendor/topthink/think-image/tests/autoload.php +++ b/vendor/topthink/think-image/tests/autoload.php @@ -12,4 +12,4 @@ define('TEST_PATH', __DIR__ . '/'); // 加载框架基础文件 require __DIR__ . '/../thinkphp/base.php'; \think\Loader::addNamespace('tests', TEST_PATH); -\think\Loader::addNamespace('think', __DIR__ . '/../src/'); \ No newline at end of file +\think\Loader::addNamespace('think', __DIR__ . '/../src/');