├── version.xml ├── index.php ├── CITATION.cff ├── NOTICE.txt ├── templates ├── settings.tpl ├── index_modal.tpl └── index.tpl ├── locale ├── en │ └── locale.po └── ar │ └── locale.po ├── README.md ├── QuickStatementsPlugin.inc.php ├── QuickStatementsBuilder.inc.php └── LICENSE.txt /version.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | quickstatements 17 | plugins.importexport 18 | 1.0.0 19 | 2025-09-03 20 | true 21 | QuickStatementsPlugin 22 | importexport 23 | quickstatements 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | , Queen Arwa University 16 | * Repository: https://github.com/saddamalsalfi/ojs-quickstatements-export 17 | * Issues: https://github.com/saddamalsalfi/ojs-quickstatements-export/issues 18 | * 19 | * (c) 2025 Saddam Al-Slfi / Queen Arwa University. All rights reserved. 20 | * 21 | * License: GNU General Public License v3.0 or later 22 | * SPDX-License-Identifier: GPL-3.0-or-later 23 | * 24 | * This file is part of the "OJS QuickStatements Export" plugin. 25 | * See the LICENSE file distributed with this source for full terms. 26 | * 27 | * Security note: 28 | * - Escape all user-facing output (TemplateManager assigns, Smarty templates). 29 | * - Validate/normalize external inputs (DOIs, usernames, tokens). 30 | * - Provide a descriptive User-Agent when calling external APIs (Toolforge). 31 | */ 32 | 33 | require_once('QuickStatementsPlugin.inc.php'); 34 | return new QuickStatementsPlugin(); 35 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # Project: OJS QuickStatements Export 2 | # File: CITATION.cff 3 | # (c) 2025 Saddam Al-Slfi / Queen Arwa University 4 | # License: GPL-3.0-or-later 5 | # SPDX-License-Identifier: GPL-3.0-or-later 6 | 7 | 8 | 9 | cff-version: 1.2.0 10 | message: "If you use this software, please cite it as below." 11 | title: "OJS QuickStatements Export" 12 | abstract: | 13 | An OJS/PKP plugin to export issues and articles to Wikidata via the 14 | QuickStatements service. The plugin can auto-submit batches to the 15 | QuickStatements API, and it de-duplicates by DOI using the Wikidata API 16 | to update existing items instead of creating duplicates. It supports 17 | Arabic/English localization, Ajax-based settings in modals, and OJS-native 18 | backend layouts. Configuration includes journal QID, preferred languages, 19 | QuickStatements username, optional batch name prefix, API token, and 20 | an auto-submit toggle. 21 | 22 | type: software 23 | authors: 24 | - given-names: "Saddam" 25 | family-names: "Al-Slfi" 26 | affiliation: "Queen Arwa University" 27 | email: "saddamalsalfi@qau.edu.ye" 28 | license: GPL-3.0-or-later 29 | repository-code: "https://github.com/saddamalsalfi/ojs-quickstatements-export" 30 | keywords: 31 | - OJS 32 | - PKP 33 | - Wikidata 34 | - QuickStatements 35 | - DOI 36 | - metadata 37 | - scholarly-communication 38 | - PHP 39 | - plugin 40 | - API 41 | version: "1.0.0" 42 | date-released: "2025-09-07" 43 | 44 | preferred-citation: 45 | type: software 46 | title: "OJS QuickStatements Export" 47 | authors: 48 | - given-names: "Saddam" 49 | family-names: "Al-Slfi" 50 | affiliation: "Queen Arwa University" 51 | version: "1.0.0" 52 | date-released: "2025-09-07" 53 | url: "https://github.com/saddamalsalfi/ojs-quickstatements-export" 54 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | QuickStatements Export Plugin for OJS 2 | NOTICE.txt 3 | 4 | Copyright (c) 2025 Saddam Al-Slfi, Queen Arwa University 5 | Contact: saddamalsalfi@qau.edu.ye 6 | 7 | License 8 | ------- 9 | This software is licensed under the GNU General Public License v3.0 or later (GPL-3.0-or-later). 10 | See the LICENSE file distributed with this plugin for the full license text. 11 | 12 | Attribution and Third-Party Credits 13 | ----------------------------------- 14 | This plugin integrates with, and is distributed for use with, the following open-source projects and services: 15 | 16 | • Open Journal Systems (OJS) and the Public Knowledge Project (PKP) libraries 17 | - © Public Knowledge Project. Licensed under GPL or compatible licenses. 18 | - Includes components such as PKP’s Template/Dispatcher/Router layers. 19 | 20 | • Smarty Template Engine 21 | - © Smarty developers. Licensed under the Smarty license / LGPL. 22 | 23 | • Laravel Illuminate Components (as included by PKP) 24 | - © Laravel contributors. Licensed under the MIT License. 25 | 26 | • cURL / libcurl (used indirectly via PHP cURL extension) 27 | - © Daniel Stenberg and contributors. Licensed under the curl license / MIT-compatible. 28 | 29 | • QuickStatements (Toolforge) API 30 | - Community tool used to post batches of Wikidata commands. 31 | - Hosted on Wikimedia Toolforge infrastructure. 32 | 33 | • Wikidata API 34 | - Public API offered by Wikidata/Wikimedia for entity lookup and metadata. 35 | 36 | Trademarks, Names, and Affiliation 37 | ---------------------------------- 38 | • “Wikidata”, “Wikimedia”, and the Wikimedia logos are trademarks of the Wikimedia Foundation, Inc. 39 | • “Toolforge” is part of Wikimedia Cloud Services. 40 | • “QuickStatements” is a community tool (credit to its author/maintainers). 41 | • “Open Journal Systems (OJS)” and “Public Knowledge Project (PKP)” are names/trademarks of their respective owners. 42 | 43 | This plugin is an independent, community-developed integration. It is not affiliated with, sponsored by, or endorsed by the Wikimedia Foundation, the QuickStatements tool maintainers, or the Public Knowledge Project. 44 | 45 | Network and Data Use 46 | -------------------- 47 | When the “Auto Submit to QuickStatements” option is enabled, this plugin can transmit exported metadata (including item labels, statements, and optional references) to: 48 | https://quickstatements.toolforge.org/api.php 49 | 50 | The plugin may also perform read-only verification against: 51 | https://www.wikidata.org/w/api.php 52 | 53 | Administrators are responsible for: 54 | • Reviewing institutional/network policies before enabling outbound API requests; 55 | • Configuring a descriptive and contactable HTTP User-Agent (already set by the plugin); 56 | • Keeping any QuickStatements API token confidential and rotating it if exposed. 57 | 58 | Privacy and Security Notice 59 | --------------------------- 60 | • The QuickStatements token you configure is used solely to authenticate batch submissions to QuickStatements. Treat it as a secret credential. 61 | • Exported content reflects data present in your OJS instance (eg, titles, authors, DOIs) and any options you select (eg, include authors, references). 62 | • Review your journal’s privacy policy and applicable laws/regulations before exporting personal data to third-party services. 63 | 64 | Redistribution 65 | -------------- 66 | If you redistribute this plugin (modified or unmodified), you must: 67 | • Retain this NOTICE file; 68 | • Retain the LICENSE file and all applicable copyright/trademark notices; 69 | • Clearly mark any changes you make and the date of those changes. 70 | 71 | NO WARRANTY 72 | ----------- 73 | This software is provided “AS IS”, without warranty of any kind. See the LICENSE file (GPL-3.0-or-later) for details, including warranty disclaimers and limitations of liability. 74 | 75 | Contact 76 | ------- 77 | Primary maintainer: Saddam Al-Slfi 78 | Email: saddamalsalfi@qau.edu.ye 79 | Institution: Queen Arwa University 80 | -------------------------------------------------------------------------------- /templates/settings.tpl: -------------------------------------------------------------------------------- 1 | {* 2 | Project: OJS QuickStatements Export 3 | Template: quickstatements 4 | Purpose: OJS-native backend UI (full page or modal) for export/settings. 5 | 6 | Notes: 7 | - Use {translate key="…"} for all visible strings. 8 | - Keep markup accessible (labels, aria-*), RTL-friendly. 9 | - Notifications are printed into $resultHtml (nofilter when it’s safe/escaped). 10 | - Forms should use AjaxFormHandler inside modals. 11 | 12 | (c) 2025 Saddam Al-Slfi / Queen Arwa University 13 | License: GPL-3.0-or-later | SPDX-License-Identifier: GPL-3.0-or-later 14 | *} 15 | 16 | {* إشعار نجاح اختياري *} 17 | {if $saved} 18 |
19 | {translate key="common.saved"} 20 |
21 | {/if} 22 | 23 |
24 | {if $csrfToken} 25 | 26 | {/if} 27 | 28 |
29 |

{translate key="plugins.importexport.quickstatements.settings.heading"}

30 | 31 |
32 |
33 | 34 |
{translate key="plugins.importexport.quickstatements.settings.journalQid.example"}
35 |
36 | 37 |
38 |
39 | 40 |
{translate key="plugins.importexport.quickstatements.settings.prefLangs.help"}
41 |
42 | 43 |
44 | 45 |
46 |
47 | 48 |
49 | 50 |
51 |
52 | 53 |
54 | 55 |
56 |
57 | 58 |
{translate key="plugins.importexport.quickstatements.settings.qsToken.help"}
59 |
60 | 61 |
62 | 66 |
{translate key="plugins.importexport.quickstatements.settings.qsAutoSubmit.help"}
67 |
68 |
69 | 70 |
71 | 72 |
73 |
74 | 75 | {if $isModal} 76 | {literal} 77 | 84 | {/literal} 85 | {/if} 86 | -------------------------------------------------------------------------------- /locale/en/locale.po: -------------------------------------------------------------------------------- 1 | # Project: OJS QuickStatements Export 2 | # File: locale.po (en) 3 | # Description: English translations for plugin UI strings. 4 | # (c) 2025 Saddam Al-Slfi / Queen Arwa University 5 | # License: GPL-3.0-or-later 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | 8 | 9 | msgid "" 10 | msgstr "" 11 | "Project-Id-Version: quickstatements\n" 12 | "POT-Creation-Date: 2025-09-07 00:00+0000\n" 13 | "PO-Revision-Date: 2025-09-07 00:00+0000\n" 14 | "Language: en\n" 15 | "Language-Team: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 20 | 21 | # ===== Plugin meta ===== 22 | msgid "plugins.importexport.quickstatements.displayName" 23 | msgstr "Wikidata QuickStatements Export" 24 | 25 | msgid "plugins.importexport.quickstatements.description" 26 | msgstr "Export issues or articles to Wikidata using QuickStatements." 27 | 28 | msgid "plugins.importexport.quickstatements.settings" 29 | msgstr "Settings" 30 | 31 | # ===== Export form ===== 32 | msgid "plugins.importexport.quickstatements.export.scope" 33 | msgstr "Export scope" 34 | 35 | msgid "plugins.importexport.quickstatements.export.scope.all" 36 | msgstr "All published submissions" 37 | 38 | msgid "plugins.importexport.quickstatements.export.scope.issues" 39 | msgstr "Specific issues (IDs)" 40 | 41 | msgid "plugins.importexport.quickstatements.export.scope.submissions" 42 | msgstr "Specific submissions (IDs)" 43 | 44 | msgid "plugins.importexport.quickstatements.export.scope.help" 45 | msgstr "Choose what to export." 46 | 47 | msgid "plugins.importexport.quickstatements.export.ids" 48 | msgstr "IDs" 49 | 50 | msgid "plugins.importexport.quickstatements.export.ids.help" 51 | msgstr "Only required when exporting specific issues or submissions." 52 | 53 | msgid "plugins.importexport.quickstatements.export.format" 54 | msgstr "Output format" 55 | 56 | msgid "plugins.importexport.quickstatements.export.format.tsv" 57 | msgstr "TSV (QuickStatements)" 58 | 59 | msgid "plugins.importexport.quickstatements.export.format.commands" 60 | msgstr "Commands (QuickStatements)" 61 | 62 | msgid "plugins.importexport.quickstatements.export.withLabels" 63 | msgstr "Include labels/descriptions" 64 | 65 | msgid "plugins.importexport.quickstatements.export.includeAuthors" 66 | msgstr "Include authors (P2093) + order" 67 | 68 | msgid "plugins.importexport.quickstatements.export.resolveAffil" 69 | msgstr "Try affiliations → Wikidata (P1416)" 70 | 71 | msgid "plugins.importexport.quickstatements.export.includeRefs" 72 | msgstr "Include references (P2860)" 73 | 74 | msgid "plugins.importexport.quickstatements.export.resolveRefs" 75 | msgstr "Resolve reference DOIs via Wikidata" 76 | 77 | msgid "plugins.importexport.quickstatements.export.updateIfExists" 78 | msgstr "Update existing item if DOI already has a QID" 79 | 80 | msgid "plugins.importexport.quickstatements.export.run" 81 | msgstr "Run export" 82 | 83 | # ===== Export result notices ===== 84 | msgid "plugins.importexport.quickstatements.notice.okTitle" 85 | msgstr "The batch was submitted to QuickStatements successfully" 86 | 87 | msgid "plugins.importexport.quickstatements.notice.viewBatch" 88 | msgstr "View on QuickStatements" 89 | 90 | msgid "plugins.importexport.quickstatements.notice.noCommands" 91 | msgstr "No commands to submit (nothing was exported)" 92 | 93 | msgid "plugins.importexport.quickstatements.notice.errorTitle" 94 | msgstr "QuickStatements API error" 95 | 96 | msgid "plugins.importexport.quickstatements.notice.httpCode" 97 | msgstr "HTTP code" 98 | 99 | # ===== Settings page (NEW) ===== 100 | msgid "plugins.importexport.quickstatements.settings.heading" 101 | msgstr "Wikidata / QuickStatements settings" 102 | 103 | msgid "plugins.importexport.quickstatements.settings.journalQid.label" 104 | msgstr "Journal QID (P1433)" 105 | 106 | msgid "plugins.importexport.quickstatements.settings.journalQid.example" 107 | msgstr "Example: Q124499613" 108 | 109 | msgid "plugins.importexport.quickstatements.settings.prefLangs.label" 110 | msgstr "Preferred language codes" 111 | 112 | msgid "plugins.importexport.quickstatements.settings.prefLangs.placeholder" 113 | msgstr "ar,en" 114 | 115 | msgid "plugins.importexport.quickstatements.settings.prefLangs.help" 116 | msgstr "Comma-separated list, e.g. ar,en" 117 | 118 | msgid "plugins.importexport.quickstatements.settings.qsUsername.label" 119 | msgstr "QuickStatements username" 120 | 121 | msgid "plugins.importexport.quickstatements.settings.qsUsername.placeholder" 122 | msgstr "Saddam_Hussein_Alsalfi" 123 | 124 | msgid "plugins.importexport.quickstatements.settings.qsBatchPrefix.label" 125 | msgstr "Batch name prefix (optional)" 126 | 127 | msgid "plugins.importexport.quickstatements.settings.qsBatchPrefix.placeholder" 128 | msgstr "QAUSRJ" 129 | 130 | msgid "plugins.importexport.quickstatements.settings.qsToken.label" 131 | msgstr "QuickStatements API token" 132 | 133 | msgid "plugins.importexport.quickstatements.settings.qsToken.help" 134 | msgstr "Paste exactly as provided by QuickStatements. Used for automatic submit." 135 | 136 | msgid "plugins.importexport.quickstatements.settings.qsAutoSubmit.label" 137 | msgstr "Enable automatic submit to QuickStatements instead of downloading a file" 138 | 139 | msgid "plugins.importexport.quickstatements.settings.qsAutoSubmit.help" 140 | msgstr "When enabled, the generated commands are posted directly to QuickStatements." 141 | 142 | msgid "common.saved" 143 | msgstr "Settings saved successfully" 144 | -------------------------------------------------------------------------------- /locale/ar/locale.po: -------------------------------------------------------------------------------- 1 | # Project: OJS QuickStatements Export 2 | # File: locale.po (ar) 3 | # Description: Arabic translations for plugin UI strings. 4 | # (c) 2025 Saddam Al-Slfi / Queen Arwa University 5 | # License: GPL-3.0-or-later 6 | # SPDX-License-Identifier: GPL-3.0-or-later 7 | 8 | 9 | 10 | 11 | msgid "" 12 | msgstr "" 13 | "Project-Id-Version: quickstatements\n" 14 | "POT-Creation-Date: 2025-09-07 00:00+0000\n" 15 | "PO-Revision-Date: 2025-09-07 00:00+0000\n" 16 | "Language: ar\n" 17 | "Language-Team: \n" 18 | "MIME-Version: 1.0\n" 19 | "Content-Type: text/plain; charset=UTF-8\n" 20 | "Content-Transfer-Encoding: 8bit\n" 21 | "Plural-Forms: nplurals=6; plural=n==0?0:n==1?1:n==2?2:(n%100>=3 && n%100<=10)?3:(n%100>=11 && n%100<=99)?4:5;\n" 22 | 23 | # ===== Plugin meta ===== 24 | msgid "plugins.importexport.quickstatements.displayName" 25 | msgstr "تصدير ويكي داتا (QuickStatements)" 26 | 27 | msgid "plugins.importexport.quickstatements.description" 28 | msgstr "تصدير الأعداد أو المقالات إلى ويكي داتا باستخدام QuickStatements." 29 | 30 | msgid "plugins.importexport.quickstatements.settings" 31 | msgstr "الإعدادات" 32 | 33 | # ===== Export form ===== 34 | msgid "plugins.importexport.quickstatements.export.scope" 35 | msgstr "نطاق التصدير" 36 | 37 | msgid "plugins.importexport.quickstatements.export.scope.all" 38 | msgstr "كل المقالات المنشورة" 39 | 40 | msgid "plugins.importexport.quickstatements.export.scope.issues" 41 | msgstr "أعداد محددة (المعرّفات)" 42 | 43 | msgid "plugins.importexport.quickstatements.export.scope.submissions" 44 | msgstr "مقالات محددة (المعرّفات)" 45 | 46 | msgid "plugins.importexport.quickstatements.export.scope.help" 47 | msgstr "اختر ما تريد تصديره." 48 | 49 | msgid "plugins.importexport.quickstatements.export.ids" 50 | msgstr "المعرّفات" 51 | 52 | msgid "plugins.importexport.quickstatements.export.ids.help" 53 | msgstr "مطلوب فقط عند اختيار أعداد/مقالات محددة." 54 | 55 | msgid "plugins.importexport.quickstatements.export.format" 56 | msgstr "صيغة الإخراج" 57 | 58 | msgid "plugins.importexport.quickstatements.export.format.tsv" 59 | msgstr "TSV (لـ QuickStatements)" 60 | 61 | msgid "plugins.importexport.quickstatements.export.format.commands" 62 | msgstr "أوامر (QuickStatements)" 63 | 64 | msgid "plugins.importexport.quickstatements.export.withLabels" 65 | msgstr "تضمين العناوين/الوصف" 66 | 67 | msgid "plugins.importexport.quickstatements.export.includeAuthors" 68 | msgstr "تضمين المؤلفين (P2093) + الترتيب" 69 | 70 | msgid "plugins.importexport.quickstatements.export.resolveAffil" 71 | msgstr "محاولة تحويل الانتماءات إلى ويكي داتا (P1416)" 72 | 73 | msgid "plugins.importexport.quickstatements.export.includeRefs" 74 | msgstr "تضمين المراجع (P2860)" 75 | 76 | msgid "plugins.importexport.quickstatements.export.resolveRefs" 77 | msgstr "تحويل DOIs في المراجع عبر ويكي داتا" 78 | 79 | msgid "plugins.importexport.quickstatements.export.updateIfExists" 80 | msgstr "تحديث العنصر إذا كان للـDOI QID مسبقًا" 81 | 82 | msgid "plugins.importexport.quickstatements.export.run" 83 | msgstr "تنفيذ التصدير" 84 | 85 | # ===== Export result notices ===== 86 | msgid "plugins.importexport.quickstatements.notice.okTitle" 87 | msgstr "تم إرسال الدفعة إلى QuickStatements بنجاح" 88 | 89 | msgid "plugins.importexport.quickstatements.notice.viewBatch" 90 | msgstr "متابعة الدفعة على QuickStatements" 91 | 92 | msgid "plugins.importexport.quickstatements.notice.noCommands" 93 | msgstr "لا توجد أوامر للإرسال (لا شيء للتصدير)" 94 | 95 | msgid "plugins.importexport.quickstatements.notice.errorTitle" 96 | msgstr "خطأ من واجهة QuickStatements API" 97 | 98 | msgid "plugins.importexport.quickstatements.notice.httpCode" 99 | msgstr "رمز الاستجابة" 100 | 101 | # ===== Settings page (NEW) ===== 102 | msgid "plugins.importexport.quickstatements.settings.heading" 103 | msgstr "إعدادات ويكي داتا / QuickStatements" 104 | 105 | msgid "plugins.importexport.quickstatements.settings.journalQid.label" 106 | msgstr "QID الخاص بالمجلة (P1433)" 107 | 108 | msgid "plugins.importexport.quickstatements.settings.journalQid.example" 109 | msgstr "مثال: Q124499613" 110 | 111 | msgid "plugins.importexport.quickstatements.settings.prefLangs.label" 112 | msgstr "أكواد اللغات المفضلة" 113 | 114 | msgid "plugins.importexport.quickstatements.settings.prefLangs.placeholder" 115 | msgstr "ar,en" 116 | 117 | msgid "plugins.importexport.quickstatements.settings.prefLangs.help" 118 | msgstr "اكتبها مفصولة بفواصل، مثل: ar,en" 119 | 120 | msgid "plugins.importexport.quickstatements.settings.qsUsername.label" 121 | msgstr "اسم مستخدم QuickStatements" 122 | 123 | msgid "plugins.importexport.quickstatements.settings.qsUsername.placeholder" 124 | msgstr "Saddam_Hussein_Alsalfi" 125 | 126 | msgid "plugins.importexport.quickstatements.settings.qsBatchPrefix.label" 127 | msgstr "بادئة اسم الدفعة (اختياري)" 128 | 129 | msgid "plugins.importexport.quickstatements.settings.qsBatchPrefix.placeholder" 130 | msgstr "QAUSRJ" 131 | 132 | msgid "plugins.importexport.quickstatements.settings.qsToken.label" 133 | msgstr "رمز QuickStatements API" 134 | 135 | msgid "plugins.importexport.quickstatements.settings.qsToken.help" 136 | msgstr "يوضع كما زوّدك به QuickStatements. يُستخدم للإرسال التلقائي." 137 | 138 | msgid "plugins.importexport.quickstatements.settings.qsAutoSubmit.label" 139 | msgstr "تفعيل الإرسال التلقائي إلى QuickStatements بدلاً من تنزيل الملف" 140 | 141 | msgid "plugins.importexport.quickstatements.settings.qsAutoSubmit.help" 142 | msgstr "عند التفعيل سيتم إرسال الأوامر مباشرةً إلى QuickStatements." 143 | 144 | msgid "common.saved" 145 | msgstr "تم حفظ الاعدادات بنجاح" -------------------------------------------------------------------------------- /templates/index_modal.tpl: -------------------------------------------------------------------------------- 1 | {* 2 | Project: OJS QuickStatements Export 3 | Template: quickstatements 4 | Purpose: OJS-native backend UI (full page or modal) for export/settings. 5 | 6 | Notes: 7 | - Use {translate key="…"} for all visible strings. 8 | - Keep markup accessible (labels, aria-*), RTL-friendly. 9 | - Notifications are printed into $resultHtml (nofilter when it’s safe/escaped). 10 | - Forms should use AjaxFormHandler inside modals. 11 | 12 | (c) 2025 Saddam Al-Slfi / Queen Arwa University 13 | License: GPL-3.0-or-later | SPDX-License-Identifier: GPL-3.0-or-later 14 | *} 15 | 16 | 17 | {* نتيجة الإرسال (إن كانت موجودة) *} 18 | {if $resultHtml} 19 |
{$resultHtml nofilter}
20 | {/if} 21 | 22 | {* اجمع قيم الطلب حتى نسترجع الاختيارات بعد الإرسال *} 23 | {assign var=_mode value=$smarty.request.mode|default:'all'} 24 | {assign var=_fmt value=$smarty.request.fmt|default:'tsv'} 25 | 26 | {* صناديق اختيار: افتراضيًا مُفعّلة مثل السابق *} 27 | {assign var=_withLabels value=$smarty.request.withLabels|default:1} 28 | {assign var=_includeAuthors value=$smarty.request.includeAuthors|default:1} 29 | {assign var=_resolveAffil value=$smarty.request.resolveAffil|default:1} 30 | {assign var=_includeRefs value=$smarty.request.includeRefs|default:0} 31 | {assign var=_resolveRefs value=$smarty.request.resolveRefs|default:0} 32 | {assign var=_updateIfExists value=$smarty.request.updateIfExists|default:1} 33 | 34 |
35 |
36 | 37 | 38 |
39 |
40 | 41 | 46 |
{translate key="plugins.importexport.quickstatements.export.scope.help"}
47 |
48 | 49 |
50 | 51 | 53 |
{translate key="plugins.importexport.quickstatements.export.ids.help"}
54 |
55 | 56 |
57 | 58 | 62 |
63 | 64 |
65 | 66 | 67 | 68 | 69 | 70 | 71 |
72 |
73 | 74 |
75 | 78 |
79 |
80 |
81 | 82 | {literal} 83 | 99 | {/literal} 100 | -------------------------------------------------------------------------------- /templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | {* 3 | Project: OJS QuickStatements Export 4 | Template: quickstatements 5 | Purpose: OJS-native backend UI (full page or modal) for export/settings. 6 | 7 | Notes: 8 | - Use {translate key="…"} for all visible strings. 9 | - Keep markup accessible (labels, aria-*), RTL-friendly. 10 | - Notifications are printed into $resultHtml (nofilter when it’s safe/escaped). 11 | - Forms should use AjaxFormHandler inside modals. 12 | 13 | (c) 2025 Saddam Al-Slfi / Queen Arwa University 14 | License: GPL-3.0-or-later | SPDX-License-Identifier: GPL-3.0-or-later 15 | *} 16 | 17 | 18 | {extends file="layouts/backend.tpl"} 19 | 20 | {block name="page"} 21 |
22 |

{$plugin->getDisplayName()|escape}

23 |

{$plugin->getDescription()|escape}

24 | 25 | {if $resultHtml} 26 |
27 | {$resultHtml nofilter} 28 |
29 | 30 | {literal} 31 | 43 | {/literal} 44 | {/if} 45 | 46 | 47 | {assign var=_mode value=$smarty.request.mode|default:'all'} 48 | {assign var=_fmt value=$smarty.request.fmt|default:'tsv'} 49 | 50 | {assign var=_withLabels value=$smarty.request.withLabels|default:1} 51 | {assign var=_includeAuthors value=$smarty.request.includeAuthors|default:1} 52 | {assign var=_resolveAffil value=$smarty.request.resolveAffil|default:0} 53 | {assign var=_includeRefs value=$smarty.request.includeRefs|default:1} 54 | {assign var=_resolveRefs value=$smarty.request.resolveRefs|default:1} 55 | {assign var=_updateIfExists value=$smarty.request.updateIfExists|default:1} 56 | 57 |
58 |
59 | 60 | 61 |
62 |
63 | 64 | 69 |
{translate key="plugins.importexport.quickstatements.export.scope.help"}
70 |
71 | 72 |
73 | 74 | 76 |
{translate key="plugins.importexport.quickstatements.export.ids.help"}
77 |
78 | 79 |
80 | 81 | 85 |
86 | 87 |
88 | 89 |
90 | 91 |
92 | 93 | 94 | 95 | 96 | 97 |
98 |
99 | 100 |
101 | 104 |
105 |
106 |
107 |
108 | {/block} 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wikidata QuickStatements Export — OJS Plugin 2 | 3 | An Open Journal Systems (OJS) import/export plugin that exports **published articles** to **Wikidata** through **QuickStatements v2**. 4 | It supports **file download** (TSV or QuickStatements command text) and **automatic deposit** to QuickStatements via its HTTP **API**, with clear success/error notifications and a direct link to the created batch. 5 | 6 | > You’ll find the plugin under **Tools → Import/Export**. It also adds **Settings** and **Export** quick-action buttons beneath the plugin in the Plugins list. Both **full-page** and **modal** UIs follow OJS styles and use AJAX where appropriate. 7 | 8 | --- 9 | 10 | ## Table of Contents 11 | 12 | * [Features](#features) 13 | * [Requirements & Compatibility](#requirements--compatibility) 14 | * [Installation](#installation) 15 | * [Enabling the Plugin](#enabling-the-plugin) 16 | * [Configuration](#configuration) 17 | * [Usage](#usage) 18 | 19 | * [Export scopes & formats](#export-scopes--formats) 20 | * [Optional enrichment](#optional-enrichment) 21 | * [Full-page vs Modal](#full-page-vs-modal) 22 | * [Automatic deposit to QuickStatements](#automatic-deposit-to-quickstatements) 23 | * [Data mapping (Wikidata properties)](#data-mapping-wikidata-properties) 24 | * [Localization](#localization) 25 | * [Directory Layout](#directory-layout) 26 | * [Troubleshooting](#troubleshooting) 27 | * [Security Notes](#security-notes) 28 | * [Contributing](#contributing) 29 | * [Credits](#credits) 30 | * [License](#license) 31 | 32 | --- 33 | 34 | ## Features 35 | 36 | * **Flexible export scopes** 37 | 38 | * All **published submissions** in the journal 39 | * By **issue IDs** 40 | * By **submission IDs** 41 | * **Two output formats** 42 | 43 | * **TSV** (for manual QuickStatements upload) 44 | * **QuickStatements v1 command text** 45 | * **Automatic deposit (API)** 46 | 47 | * Sends commands directly to **quickstatements.toolforge.org/api.php** 48 | * Shows an in-app success notice with a **direct batch link**: 49 | 50 | ``` 51 | https://quickstatements.toolforge.org/#/batch/{batch_id} 52 | ``` 53 | * **Optional enrichment** 54 | 55 | * **Labels/Descriptions** honoring **preferred language order** 56 | * **Authors** as string statements (**P2093**) with **series ordinal** (**P1545**) 57 | * Best-effort mapping of **affiliations** to Wikidata (**P1416**) by label 58 | * **Citations** (**P2860**) if reference DOIs resolve to Wikidata QIDs 59 | * **Update-if-exists**: detect an existing item by **DOI** and **update** instead of creating 60 | * **Polished OJS UX** 61 | 62 | * Full backend page extends `layouts/backend.tpl` 63 | * Modal dialogs return `JSONMessage` and use `AjaxFormHandler` for inline notifications 64 | * Retains user selections after submit (page & modal) 65 | 66 | --- 67 | 68 | ## Requirements & Compatibility 69 | 70 | * **OJS 3.5.x ** (tested with TemplateManager, Repo facade, SettingsPluginGridHandler) 71 | * A Wikidata account and access to **QuickStatements** 72 | [https://quickstatements.toolforge.org/](https://quickstatements.toolforge.org/) 73 | 74 | > Other OJS 3.x versions may work with minor routing/handler adjustments. 75 | 76 | --- 77 | 78 | ## Installation 79 | 80 | Place the plugin inside `plugins/importexport/quickstatements`: 81 | 82 | ``` 83 | ojs/ 84 | └── plugins/ 85 | └── importexport/ 86 | └── quickstatements/ 87 | index.php 88 | QuickStatementsPlugin.inc.php 89 | QuickStatementsBuilder.inc.php 90 | version.xml 91 | README.md 92 | locale/ 93 | templates/ 94 | ``` 95 | 96 | If using Git: 97 | 98 | ```bash 99 | cd plugins/importexport 100 | git clone quickstatements 101 | ``` 102 | 103 | Then clear template caches from the OJS admin (or remove `cache/t_compile` carefully). 104 | 105 | --- 106 | 107 | ## Enabling the Plugin 108 | 109 | 1. Sign in as **Journal Manager / Site Admin**. 110 | 2. Go to **Settings → Website → Plugins**. 111 | 3. Enable **Wikidata QuickStatements Export**. 112 | 4. Open **Tools → Import/Export → Wikidata QuickStatements Export** (or use the **Export** button under the plugin). 113 | 114 | --- 115 | 116 | ## Configuration 117 | 118 | Open **Settings** (from the plugin row or inside the tool). The form follows OJS styling, includes CSRF protection, and saves via AJAX. 119 | 120 | * **Journal QID (P1433)** 121 | The Wikidata item for your journal, e.g. `Q124499613`. Used for **“published in”**. 122 | * **Preferred language codes** 123 | Comma-separated (e.g. `ar,en`). Determines the order for labels/titles. 124 | * **qsUsername** 125 | Your QuickStatements username (e.g. `Saddam_Hussein_Alsalfi`). 126 | * **qsBatchPrefix** *(optional)* 127 | Prefix added to generated batch names (e.g. `QAUSRJ`). 128 | * **qsToken** 129 | Your **QuickStatements API token** (looks like `$2y$10$…`). Acquire it from QuickStatements while logged in. 130 | * **qsAutoSubmit** 131 | If enabled, the plugin **sends** the generated commands directly to QuickStatements instead of downloading a file. 132 | 133 | --- 134 | 135 | ## Usage 136 | 137 | Open the tool page (full page) or click **Export** under the plugin (modal). 138 | 139 | ### Export scopes & formats 140 | 141 | * **Scope** 142 | 143 | * **All** published submissions 144 | * **Issues** (enter issue IDs) 145 | * **Submissions** (enter submission IDs) 146 | * **IDs** 147 | Only required when Scope = Issues or Submissions. Example: `12,15,19` 148 | * **Format** 149 | 150 | * **TSV** — for manual upload 151 | * **Commands** — QuickStatements v1 text 152 | 153 | ### Optional enrichment 154 | 155 | * **With labels**: output `Lxx`/`Dxx` and **P1476** (title) in the preferred language 156 | * **Include authors**: **P2093** (author name string) + **P1545** (author order) 157 | * **Resolve affiliations**: try to link affiliations to **P1416** (best-effort, slower) 158 | * **Include references**: add **P2860** when reference DOIs resolve to QIDs 159 | * **Resolve reference DOIs**: try to resolve DOIs in references (slower) 160 | * **Update if DOI exists**: look up the DOI first; if found, **update** that QID 161 | 162 | ### Full-page vs Modal 163 | 164 | * **Full page** 165 | Uses OJS backend layout; on auto-submit success it shows a **green notice** with a link to the created batch. 166 | * **Modal** 167 | Uses `JSONMessage` and `AjaxFormHandler`; the modal content refreshes with an inline success/error message and a batch link (if applicable). 168 | 169 | --- 170 | 171 | ## Automatic deposit to QuickStatements 172 | 173 | When **qsAutoSubmit** is enabled (and **qsUsername** + **qsToken** are set): 174 | 175 | 1. The plugin generates **QuickStatements v1** commands. 176 | 177 | 2. It POSTs to: 178 | 179 | ``` 180 | https://quickstatements.toolforge.org/api.php 181 | action=import 182 | submit=1 183 | format=v1 184 | username= 185 | batchname= 186 | token= 187 | data= 188 | ``` 189 | 190 | 3. Requests include a Toolforge-friendly **User-Agent**, e.g. 191 | `OJS-QuickStatements-Plugin/1.0 (+https://your-host; contact admin@your-host)` 192 | 193 | 4. On success you’ll see: 194 | `status: "OK"` and a `batch_id`, and the UI will show a link: 195 | 196 | ``` 197 | https://quickstatements.toolforge.org/#/batch/{batch_id} 198 | ``` 199 | 200 | 5. If `status: "No commands"`, the UI shows an informational notice. 201 | 202 | 6. On errors (HTTP ≥ 400), the UI shows a red error notice with the HTTP code and a short response snippet. 203 | 204 | --- 205 | 206 | ## Data mapping (Wikidata properties) 207 | 208 | Typical output includes: 209 | 210 | * **P31 → Q13442814** (instance of: scholarly article) 211 | * **P1476** (title) with language tag 212 | * **P577** (publication date) with precise granularity 213 | * **P1433** (published in) → journal QID from settings 214 | * **P478** (volume), **P433** (issue), **P304** (pages) 215 | * **P356** (DOI) 216 | * **P407** (language of work) using a bundled language map (ar, en, fr, de, es, it, ru, fa, tr) 217 | * **P2093** (author name string) + **P1545** (author order) 218 | * **P2860** (cites work) if references resolve to QIDs 219 | * **P1416** (affiliation) if an organization label is matched 220 | * **P953** (full work available at) and **P856** (official website) when URLs exist 221 | 222 | The **Update-if-exists** option uses DOI → QID resolution to target an existing item when possible. 223 | 224 | --- 225 | 226 | ## Localization 227 | 228 | Translations live in: 229 | 230 | ``` 231 | locale/ 232 | ├── ar/locale.po 233 | └── en/locale.po 234 | ``` 235 | 236 | Add more languages by creating `/locale.po` and translating the same keys. 237 | 238 | --- 239 | 240 | ## Directory Layout 241 | 242 | ``` 243 | quickstatements/ 244 | │ index.php 245 | │ QuickStatementsPlugin.inc.php 246 | │ QuickStatementsBuilder.inc.php 247 | │ version.xml 248 | │ README.md 249 | │ 250 | ├─ locale/ 251 | │ ├─ ar/locale.po 252 | │ └─ en/locale.po 253 | │ 254 | └─ templates/ 255 | │ index.tpl # full backend page (OJS layout) 256 | │ index_modal.tpl # export modal (AJAX) 257 | │ settings.tpl # settings form (page & modal) 258 | ``` 259 | 260 | **Core files** 261 | 262 | * `QuickStatementsPlugin.inc.php` — routing, UI wiring (page + modal), settings, export orchestration, QS API calls, and UI notifications. 263 | * `QuickStatementsBuilder.inc.php` — transforms OJS submissions into QuickStatements v1 commands or TSV; language handling; optional enrichment. 264 | 265 | --- 266 | 267 | ## Troubleshooting 268 | 269 | * **403 — “Requests must have a user agent”** 270 | Toolforge requires a clear **User-Agent**. The plugin sets one automatically: 271 | `OJS-QuickStatements-Plugin/1.0 (+https://your-host; contact admin@your-host)` 272 | 273 | * **404 when saving settings** 274 | Ensure settings POST to `ROUTE_COMPONENT` with `SettingsPluginGridHandler` and `verb=saveSettings`. The included templates do this. 275 | 276 | * **No fields in modal** 277 | The modal must call `manage?verb=exportForm` (or `settings`) and return a `JSONMessage`. The templates attach `$.pkp.controllers.form.AjaxFormHandler`. 278 | 279 | * **“No commands”** 280 | Nothing matched your filters. Confirm the items are **published** and that the chosen scope/IDs are correct. 281 | 282 | * **Existing items not updated** 283 | Make sure **Update if DOI exists** is checked and that DOIs match what’s in Wikidata (case-insensitive normalization is applied). 284 | 285 | --- 286 | 287 | ## Security Notes 288 | 289 | * Treat **qsToken** as a secret. Do **not** commit it or paste it publicly. 290 | * Restrict access to the settings page to appropriate OJS roles. 291 | * Strip tokens from logs or screenshots before sharing. 292 | 293 | --- 294 | 295 | ## Contributing 296 | 297 | Issues and PRs are welcome. Please: 298 | 299 | * Follow OJS routing conventions (`\Application::get()->getDispatcher()->url`). 300 | * Keep UI consistent with OJS patterns (backend layouts, `JSONMessage`, `AjaxFormHandler`). 301 | * Add new UI strings to **both** `locale/en/locale.po` and `locale/ar/locale.po`. 302 | 303 | --- 304 | 305 | ## Credits 306 | 307 | **By Saddam Al-Slfi** — *Queen Arwa University* 308 | 📧 **[saddamalsalfi@qau.edu.ye](mailto:saddamalsalfi@qau.edu.ye)** 309 | 310 | --- 311 | 312 | ## License 313 | 314 | This plugin is released under the **GNU General Public License v3 (GPL-3.0-or-later)**. 315 | **SPDX-License-Identifier:** `GPL-3.0-or-later` 316 | 317 | See the **LICENSE** file for the full license text. 318 | -------------------------------------------------------------------------------- /QuickStatementsPlugin.inc.php: -------------------------------------------------------------------------------- 1 | , Queen Arwa University 16 | * Repository: https://github.com/saddamalsalfi/ojs-quickstatements-export 17 | * Issues: https://github.com/saddamalsalfi/ojs-quickstatements-export/issues 18 | * 19 | * (c) 2025 Saddam Al-Slfi / Queen Arwa University. All rights reserved. 20 | * 21 | * License: GNU General Public License v3.0 or later 22 | * SPDX-License-Identifier: GPL-3.0-or-later 23 | * 24 | * This file is part of the "OJS QuickStatements Export" plugin. 25 | * See the LICENSE file distributed with this source for full terms. 26 | * 27 | * Security note: 28 | * - Escape all user-facing output (TemplateManager assigns, Smarty templates). 29 | * - Validate/normalize external inputs (DOIs, usernames, tokens). 30 | * - Provide a descriptive User-Agent when calling external APIs (Toolforge). 31 | */ 32 | 33 | 34 | use PKP\plugins\ImportExportPlugin; 35 | use APP\template\TemplateManager; 36 | use APP\facades\Repo; 37 | 38 | use PKP\linkAction\LinkAction; 39 | use PKP\linkAction\request\AjaxModal; 40 | use PKP\core\JSONMessage; 41 | 42 | class QuickStatementsPlugin extends ImportExportPlugin { 43 | 44 | public function register($category, $path, $mainContextId = null) { 45 | $ok = parent::register($category, $path, $mainContextId); 46 | if ($ok) { $this->addLocaleData(); } 47 | return $ok; 48 | } 49 | 50 | public function getName() { return 'quickstatements'; } 51 | 52 | public function getDisplayName() { 53 | $k = 'plugins.importexport.quickstatements.displayName'; 54 | $t = __($k); return $t !== $k ? $t : 'Wikidata QuickStatements Export'; 55 | } 56 | 57 | public function getDescription() { 58 | $k = 'plugins.importexport.quickstatements.description'; 59 | $t = __($k); return $t !== $k ? $t : 'Export issues or articles to Wikidata using QuickStatements v2.'; 60 | } 61 | 62 | /** أزرار أسفل الإضافة (مودالات) */ 63 | public function getActions($request, $actionArgs) { 64 | $router = $request->getRouter(); 65 | $actions = parent::getActions($request, $actionArgs); 66 | 67 | // الإعدادات 68 | $actions[] = new LinkAction( 69 | 'settings', 70 | new AjaxModal( 71 | $router->url( 72 | $request, 73 | null, null, 'manage', null, 74 | ['verb'=>'settings','plugin'=>$this->getName(),'category'=>$this->getCategory()] 75 | ), 76 | $this->getDisplayName() 77 | ), 78 | __('manager.plugins.settings') 79 | ); 80 | 81 | // التصدير 82 | $actions[] = new LinkAction( 83 | 'export', 84 | new AjaxModal( 85 | $router->url( 86 | $request, 87 | null, null, 'manage', null, 88 | ['verb'=>'exportForm','plugin'=>$this->getName(),'category'=>$this->getCategory()] 89 | ), 90 | $this->getDisplayName() 91 | ), 92 | __('common.export') 93 | ); 94 | 95 | return $actions; 96 | } 97 | 98 | /** العرض الكامل عندما تُفتح صفحة الإضافة من تبويب الأدوات */ 99 | public function display($args, $request) { 100 | $verb = $request->getUserVar('verb'); if (!$verb && is_array($args) && !empty($args)) $verb = $args[0]; 101 | 102 | if ($verb === 'settings') { 103 | echo $this->renderSettingsHtml($request, false, false); 104 | return; 105 | } 106 | 107 | if ($verb === 'saveSettings' && strtoupper($request->getRequestMethod()) === 'POST') { 108 | $this->validateCSRF($request); 109 | $context = $request->getContext(); $contextId = $context? $context->getId():null; 110 | 111 | $this->updateSetting($contextId, 'journalQid', trim((string)$request->getUserVar('journalQid'))); 112 | $this->updateSetting($contextId, 'prefLangs', trim((string)$request->getUserVar('prefLangs'))); 113 | $this->updateSetting($contextId, 'qsToken', trim((string)$request->getUserVar('qsToken'))); 114 | $this->updateSetting($contextId, 'qsUsername', trim((string)$request->getUserVar('qsUsername'))); 115 | $this->updateSetting($contextId, 'qsBatchPrefix', trim((string)$request->getUserVar('qsBatchPrefix'))); 116 | $this->updateSetting($contextId, 'qsAutoSubmit', (bool)$request->getUserVar('qsAutoSubmit')); 117 | 118 | echo $this->renderSettingsHtml($request, true, false); 119 | return; 120 | } 121 | 122 | if ($verb === 'export') { 123 | // نفّذ التصدير ثم أعِد عرض الصفحة مع إشعار HTML بدلاً من JSON الخام 124 | $resultHtml = $this->exportAndMaybeSendToQS($request, /*forModal*/false); 125 | $this->showExportPage($request, (string)$resultHtml); 126 | return; 127 | } 128 | 129 | // افتراضيًا: اعرض صفحة التصدير كاملة بنمط OJS 130 | $this->showExportPage($request); 131 | } 132 | 133 | /** مسارات المودال (Ajax) */ 134 | public function manage($args, $request) { 135 | $verb = $request->getUserVar('verb'); if (!$verb && !empty($args)) $verb = array_shift($args); 136 | 137 | switch ($verb) { 138 | case 'settings': { 139 | $html = $this->renderSettingsHtml($request, false, true); 140 | return new JSONMessage(true, $html); 141 | } 142 | case 'saveSettings': { 143 | $this->validateCSRF($request); 144 | $context = $request->getContext(); $contextId = $context? $context->getId():null; 145 | 146 | $this->updateSetting($contextId, 'journalQid', trim((string)$request->getUserVar('journalQid'))); 147 | $this->updateSetting($contextId, 'prefLangs', trim((string)$request->getUserVar('prefLangs'))); 148 | $this->updateSetting($contextId, 'qsToken', trim((string)$request->getUserVar('qsToken'))); 149 | $this->updateSetting($contextId, 'qsUsername', trim((string)$request->getUserVar('qsUsername'))); 150 | $this->updateSetting($contextId, 'qsBatchPrefix', trim((string)$request->getUserVar('qsBatchPrefix'))); 151 | $this->updateSetting($contextId, 'qsAutoSubmit', (bool)$request->getUserVar('qsAutoSubmit')); 152 | 153 | $html = $this->renderSettingsHtml($request, true, true); 154 | return new JSONMessage(true, $html); 155 | } 156 | case 'exportForm': { 157 | $html = $this->renderExportHtml($request, true); 158 | return new JSONMessage(true, $html); 159 | } 160 | case 'export': { 161 | // نفّذ التصدير للـ modal وأعد القالب مع إشعار النتيجة 162 | $resultHtml = $this->exportAndMaybeSendToQS($request, /*forModal*/true); 163 | if ($resultHtml !== null) { 164 | $html = $this->renderExportHtml($request, /*forModal*/true, $resultHtml); 165 | return new JSONMessage(true, $html); 166 | } 167 | // في حالة تنزيل ملف لن نصل هنا 168 | return true; 169 | } 170 | } 171 | return parent::manage($args, $request); 172 | } 173 | 174 | /** صفحة التصدير كاملة (layout backend) */ 175 | protected function showExportPage($request, $resultHtml = '') { 176 | $tm = TemplateManager::getManager($request); 177 | $context = $request->getContext(); 178 | $ctxPath = $context ? $context->getPath() : null; 179 | 180 | $dispatcher = \Application::get()->getDispatcher(); 181 | 182 | $exportAction = $dispatcher->url( 183 | $request, 184 | \PKP\core\PKPApplication::ROUTE_PAGE, 185 | $ctxPath, 186 | 'management', 187 | 'importexport', 188 | ['plugin', $this->getName()], 189 | ['verb' => 'export'] 190 | ); 191 | 192 | $settingsUrl = $dispatcher->url( 193 | $request, 194 | \PKP\core\PKPApplication::ROUTE_PAGE, 195 | $ctxPath, 196 | 'management', 197 | 'importexport', 198 | ['plugin', $this->getName()], 199 | ['verb' => 'settings'] 200 | ); 201 | 202 | $tm->assign([ 203 | 'plugin' => $this, 204 | 'pluginName' => $this->getName(), 205 | 'exportAction' => $exportAction, 206 | 'settingsUrl' => $settingsUrl, 207 | 'csrfToken' => $this->getCsrfTokenSafe($request), 208 | 'resultHtml' => (string)$resultHtml, 209 | ]); 210 | 211 | $tm->display($this->getTemplateResource('index.tpl')); 212 | } 213 | 214 | /** 215 | * يبني HTML لنموذج التصدير. 216 | * - $forModal=true ⇒ نستخدم رابط manage للـ modal. 217 | * - $resultHtml لإظهار إشعارات النجاح/الخطأ أعلى النموذج. 218 | */ 219 | protected function renderExportHtml($request, $forModal = false, $resultHtml = '') { 220 | $tm = TemplateManager::getManager($request); 221 | $context = $request->getContext(); 222 | $ctxPath = $context ? $context->getPath() : null; 223 | 224 | $router = $request->getRouter(); 225 | $dispatcher = \Application::get()->getDispatcher(); 226 | 227 | // رابط إجراء التصدير للصفحة الكاملة 228 | $exportActionPage = $dispatcher->url( 229 | $request, 230 | \PKP\core\PKPApplication::ROUTE_PAGE, 231 | $ctxPath, 232 | 'management', 233 | 'importexport', 234 | ['plugin', $this->getName()], 235 | ['verb' => 'export'] 236 | ); 237 | 238 | // رابط إجراء التصدير للمودال (إلى manage وليس component) 239 | $exportActionModal = $router->url( 240 | $request, 241 | null, null, 'manage', null, 242 | ['verb' => 'export', 'plugin' => $this->getName(), 'category' => $this->getCategory()] 243 | ); 244 | 245 | $tm->assign([ 246 | 'plugin' => $this, 247 | 'pluginName' => $this->getName(), 248 | 'exportAction' => $forModal ? $exportActionModal : $exportActionPage, 249 | 'resultHtml' => (string)$resultHtml, 250 | ]); 251 | 252 | $tpl = $forModal ? 'index_modal.tpl' : 'index.tpl'; 253 | return $tm->fetch($this->getTemplateResource($tpl)); 254 | } 255 | 256 | /** يبني HTML لرسائل نتيجة الرفع إلى QuickStatements */ 257 | protected function renderExportResultHtml(array $resp) { 258 | $http = isset($resp['http_code']) ? (int)$resp['http_code'] : 0; 259 | $json = isset($resp['json']) && is_array($resp['json']) ? $resp['json'] : []; 260 | $status = isset($json['status']) ? (string)$json['status'] : null; 261 | $batchId= isset($json['batch_id']) ? (int)$json['batch_id'] : 0; 262 | 263 | $okWithBatch = ($http >= 200 && $http < 300) && $status === 'OK' && $batchId > 0; 264 | $noCommands = ($http >= 200 && $http < 300) && $status === 'No commands'; 265 | 266 | $batchUrl = $batchId ? ('https://quickstatements.toolforge.org/#/batch/' . $batchId) : ''; 267 | 268 | ob_start(); 269 | if ($okWithBatch) { 270 | echo '
'; 271 | echo htmlspecialchars(__('plugins.importexport.quickstatements.notice.okTitle'), ENT_QUOTES, 'UTF-8') . ' — '; 272 | echo ''; 273 | echo htmlspecialchars(__('plugins.importexport.quickstatements.notice.viewBatch'), ENT_QUOTES, 'UTF-8') . ' #' . (int)$batchId . ''; 274 | echo '
'; 275 | } elseif ($noCommands) { 276 | echo '
'; 277 | echo htmlspecialchars(__('plugins.importexport.quickstatements.notice.noCommands'), ENT_QUOTES, 'UTF-8'); 278 | echo '
'; 279 | } else { 280 | echo ''; 289 | } 290 | return ob_get_clean(); 291 | } 292 | 293 | /** صفحة/مودال الإعدادات */ 294 | protected function renderSettingsHtml($request, $saved=false, $forModal=true) { 295 | $tm = TemplateManager::getManager($request); 296 | $context = $request->getContext(); $contextId = $context? $context->getId():null; 297 | 298 | $dispatcher = \Application::get()->getDispatcher(); 299 | $formAction = $dispatcher->url( 300 | $request, 301 | \PKP\core\PKPApplication::ROUTE_COMPONENT, 302 | null, 303 | 'grid.settings.plugins.SettingsPluginGridHandler', 304 | 'manage', 305 | null, 306 | ['verb'=>'saveSettings','plugin'=>$this->getName(),'category'=>$this->getCategory()] 307 | ); 308 | 309 | $tm->assign([ 310 | 'plugin' => $this, 311 | 'journalQid' => (string)$this->getSetting($contextId, 'journalQid'), 312 | 'prefLangs' => (string)$this->getSetting($contextId, 'prefLangs'), 313 | 'qsToken' => (string)$this->getSetting($contextId, 'qsToken'), 314 | 'qsUsername' => (string)$this->getSetting($contextId, 'qsUsername'), 315 | 'qsBatchPrefix' => (string)$this->getSetting($contextId, 'qsBatchPrefix'), 316 | 'qsAutoSubmit' => (bool)$this->getSetting($contextId, 'qsAutoSubmit'), 317 | 'saved' => $saved, 318 | 'isModal' => $forModal, 319 | 'formAction' => $formAction, 320 | 'csrfToken' => $this->getCsrfTokenSafe($request), 321 | ]); 322 | 323 | return $tm->fetch($this->getTemplateResource('settings.tpl')); 324 | } 325 | 326 | /** 327 | * تنفيذ التصدير لسيناريو الصفحة الكاملة أو المودال وإرجاع HTML إشعار عند الإرسال التلقائي، 328 | * أو تنزيل الملف عند عدم التفعيل (وينتهي التنفيذ). 329 | * 330 | * @return string|null HTML نتيجة (للعرض أعلى النموذج). عند تنزيل ملف لن تُعاد قيمة. 331 | */ 332 | protected function exportAndMaybeSendToQS($request, $forModal=false) { 333 | $context = $request->getContext(); 334 | $contextId = $context ? $context->getId() : null; 335 | 336 | $withLabels = (bool)$request->getUserVar('withLabels'); 337 | $fmt = $request->getUserVar('fmt') ?: 'tsv'; 338 | $mode = $request->getUserVar('mode') ?: 'all'; 339 | $idsRaw = (string)$request->getUserVar('ids'); 340 | 341 | // اجمع المقالات 342 | $subs = []; 343 | if ($mode === 'all') { 344 | $collector = Repo::submission()->getCollector() 345 | ->filterByContextIds([$contextId]) 346 | ->filterByStatus([\APP\submission\Submission::STATUS_PUBLISHED]); 347 | foreach ($collector->getMany() as $s) { $subs[] = $s; } 348 | } elseif ($mode === 'issues') { 349 | $issueIds = $this->parseIds($idsRaw); 350 | if (!empty($issueIds)) { 351 | $collector = Repo::submission()->getCollector() 352 | ->filterByContextIds([$contextId]) 353 | ->filterByIssueIds($issueIds) 354 | ->filterByStatus([\APP\submission\Submission::STATUS_PUBLISHED]); 355 | foreach ($collector->getMany() as $s) { $subs[] = $s; } 356 | } 357 | } elseif ($mode === 'ids') { 358 | $submissionIds = $this->parseIds($idsRaw); 359 | if (!empty($submissionIds)) { 360 | $collector = Repo::submission()->getCollector() 361 | ->filterByContextIds([$contextId]); 362 | if (method_exists($collector, 'filterByIds')) { 363 | $collector = $collector->filterByIds($submissionIds); 364 | foreach ($collector->getMany() as $s) { $subs[] = $s; } 365 | } else { 366 | foreach ($submissionIds as $sid) { 367 | $s = Repo::submission()->get((int)$sid); 368 | if ($s && (int)$s->getData('contextId') === (int)$contextId) { $subs[] = $s; } 369 | } 370 | } 371 | } 372 | } 373 | 374 | // ابنِ البيانات 375 | require_once($this->getPluginPath().'/QuickStatementsBuilder.inc.php'); 376 | $journalQid = (string)$this->getSetting($contextId, 'journalQid'); 377 | $prefLangs = (string)$this->getSetting($contextId, 'prefLangs'); 378 | $pl = []; 379 | foreach (explode(',', $prefLangs) as $p) { $p = trim($p); if ($p !== '') $pl[] = $p; } 380 | 381 | $builder = new QuickStatementsBuilder($journalQid, $pl); 382 | $opts = [ 383 | 'includeAuthors' => (bool)$request->getUserVar('includeAuthors'), 384 | 'resolveAffil' => (bool)$request->getUserVar('resolveAffil'), 385 | 'includeRefs' => (bool)$request->getUserVar('includeRefs'), 386 | 'resolveRefs' => (bool)$request->getUserVar('resolveRefs'), 387 | 'updateIfExists' => (bool)$request->getUserVar('updateIfExists'), 388 | ]; 389 | $rows = $builder->buildForSubmissions($context, $subs, $withLabels, $opts); 390 | 391 | // إعدادات QS 392 | $auto = (bool)$this->getSetting($contextId, 'qsAutoSubmit'); 393 | $username = (string)$this->getSetting($contextId, 'qsUsername'); 394 | $token = (string)$this->getSetting($contextId, 'qsToken'); 395 | $prefix = (string)$this->getSetting($contextId, 'qsBatchPrefix'); 396 | $canAuto = $auto && $username !== '' && $token !== ''; 397 | 398 | if ($canAuto) { 399 | // أوامر v1 كنص 400 | ob_start(); 401 | $builder->streamCommands($rows); 402 | $commands = (string)ob_get_clean(); 403 | 404 | // اسم الدفعة 405 | $ctxName = $context ? (string)$context->getLocalizedName() : 'OJS'; 406 | $batchName = trim(($prefix ? $prefix.' - ' : '') . $ctxName . ' - ' . date('Y-m-d H:i')); 407 | 408 | // إرسال إلى QS 409 | $resp = $this->importToQuickStatements($batchName, $username, $token, $commands, 'v1', false, $request); 410 | 411 | // HTML إشعار 412 | return $this->renderExportResultHtml($resp); 413 | } 414 | 415 | // غير تلقائي: تنزيل الملف 416 | if ($fmt === 'commands') { 417 | header('Content-Type: text/plain; charset=utf-8'); 418 | header('Content-Disposition: attachment; filename="quickstatements_commands.txt"'); 419 | $builder->streamCommands($rows); 420 | } else { 421 | header('Content-Type: text/tab-separated-values; charset=utf-8'); 422 | header('Content-Disposition: attachment; filename="quickstatements.tsv"'); 423 | $builder->streamTSV($rows); 424 | } 425 | exit; 426 | } 427 | 428 | /** 429 | * تنفيذ التصدير (أسلوب قديم للمودال). يُستخدم فقط من manage->export في بعض التركيبات. 430 | * إن كان auto-submit يعيد قالب المودال مضمّن فيه إشعار النتيجة، وإلا يبدأ تنزيل الملف. 431 | */ 432 | protected function exportNow($request, $returnHtmlForModal = false) { 433 | $context = $request->getContext(); $contextId = $context? $context->getId():null; 434 | $withLabels = (bool)$request->getUserVar('withLabels'); 435 | $fmt = $request->getUserVar('fmt') ?: 'tsv'; 436 | $mode = $request->getUserVar('mode') ?: 'all'; 437 | $idsRaw = (string)$request->getUserVar('ids'); 438 | 439 | $subs = []; 440 | if ($mode === 'all') { 441 | $collector = Repo::submission()->getCollector() 442 | ->filterByContextIds([$contextId]) 443 | ->filterByStatus([\APP\submission\Submission::STATUS_PUBLISHED]); 444 | foreach ($collector->getMany() as $s) { $subs[] = $s; } 445 | } elseif ($mode === 'issues') { 446 | $issueIds = $this->parseIds($idsRaw); 447 | if (!empty($issueIds)) { 448 | $collector = Repo::submission()->getCollector() 449 | ->filterByContextIds([$contextId]) 450 | ->filterByIssueIds($issueIds) 451 | ->filterByStatus([\APP\submission\Submission::STATUS_PUBLISHED]); 452 | foreach ($collector->getMany() as $s) { $subs[] = $s; } 453 | } 454 | } elseif ($mode === 'ids') { 455 | $submissionIds = $this->parseIds($idsRaw); 456 | if (!empty($submissionIds)) { 457 | $collector = Repo::submission()->getCollector() 458 | ->filterByContextIds([$contextId]); 459 | if (method_exists($collector, 'filterByIds')) { 460 | $collector = $collector->filterByIds($submissionIds); 461 | foreach ($collector->getMany() as $s) { $subs[] = $s; } 462 | } else { 463 | foreach ($submissionIds as $sid) { 464 | $s = Repo::submission()->get((int)$sid); 465 | if ($s && (int)$s->getData('contextId') === (int)$contextId) { $subs[] = $s; } 466 | } 467 | } 468 | } 469 | } 470 | 471 | require_once($this->getPluginPath().'/QuickStatementsBuilder.inc.php'); 472 | $journalQid = (string)$this->getSetting($contextId, 'journalQid'); 473 | $prefLangs = (string)$this->getSetting($contextId, 'prefLangs'); 474 | $pl = []; foreach (explode(',', $prefLangs) as $p) { $p = trim($p); if ($p !== '') $pl[] = $p; } 475 | 476 | $builder = new QuickStatementsBuilder($journalQid, $pl); 477 | $opts = [ 478 | 'includeAuthors' => (bool)$request->getUserVar('includeAuthors'), 479 | 'resolveAffil' => (bool)$request->getUserVar('resolveAffil'), 480 | 'includeRefs' => (bool)$request->getUserVar('includeRefs'), 481 | 'resolveRefs' => (bool)$request->getUserVar('resolveRefs'), 482 | 'updateIfExists' => (bool)$request->getUserVar('updateIfExists'), 483 | ]; 484 | $rows = $builder->buildForSubmissions($context, $subs, $withLabels, $opts); 485 | 486 | // مسار الإرسال التلقائي 487 | $auto = (bool)$this->getSetting($contextId, 'qsAutoSubmit'); 488 | if ($auto) { 489 | $username = (string)$this->getSetting($contextId, 'qsUsername'); 490 | $token = (string)$this->getSetting($contextId, 'qsToken'); 491 | $prefix = (string)$this->getSetting($contextId, 'qsBatchPrefix'); 492 | 493 | // أوامر v1 كنص 494 | ob_start(); 495 | $builder->streamCommands($rows); 496 | $commands = (string)ob_get_clean(); 497 | 498 | // اسم الدفعة 499 | $ctxName = $context ? (string)$context->getLocalizedName() : 'Batch'; 500 | $batch = trim(($prefix ? $prefix.' - ' : '') . $ctxName . ' - ' . date('H:i d-m-Y')); 501 | 502 | // أرسل إلى QS 503 | $resp = $this->importToQuickStatements($batch, $username, $token, $commands, 'v1', false, $request); 504 | 505 | if ($returnHtmlForModal) { 506 | $resultHtml = $this->renderExportResultHtml($resp); 507 | return $this->renderExportHtml($request, /*forModal*/ true, $resultHtml); 508 | } 509 | 510 | header('Content-Type: application/json; charset=utf-8'); 511 | echo json_encode($resp, JSON_UNESCAPED_UNICODE|JSON_UNESCAPED_SLASHES); 512 | exit; 513 | } 514 | 515 | // تنزيل ملف 516 | if ($fmt === 'commands') { 517 | header('Content-Type: text/plain; charset=utf-8'); 518 | header('Content-Disposition: attachment; filename="quickstatements_commands.txt"'); 519 | $builder->streamCommands($rows); 520 | } else { 521 | header('Content-Type: text/tab-separated-values; charset=utf-8'); 522 | header('Content-Disposition: attachment; filename="quickstatements.tsv"'); 523 | $builder->streamTSV($rows); 524 | } 525 | exit; 526 | } 527 | 528 | /** استدعاء QuickStatements API (مُحدّثة) */ 529 | protected function importToQuickStatements($batchName, $username, $token, $data, $format='v1', $temporary=false, $request=null) { 530 | $url = 'https://quickstatements.toolforge.org/api.php'; 531 | 532 | $host = ''; 533 | try { if ($request && method_exists($request, 'getServerHost')) { $host = $request->getServerHost(); } } catch (\Throwable $e) {} 534 | if ($host === '') { $host = (isset($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] : 'unknown-host'); } 535 | 536 | $ua = 'OJS-QuickStatements-Plugin/1.0 (+https://' . $host . '; contact journal@' . $host . ')'; 537 | 538 | $fields = [ 539 | 'action' => 'import', 540 | 'submit' => '1', 541 | 'format' => $format, 542 | 'username' => $username, 543 | 'batchname' => $batchName, 544 | 'token' => $token, 545 | 'temporary' => $temporary ? '1' : '0', 546 | 'openpage' => '0', 547 | 'data' => (string)$data, 548 | ]; 549 | $payload = http_build_query($fields, '', '&', PHP_QUERY_RFC3986); 550 | 551 | $ch = curl_init($url); 552 | curl_setopt($ch, CURLOPT_POST, true); 553 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 554 | curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); 555 | curl_setopt($ch, CURLOPT_HTTPHEADER, [ 556 | 'Accept: application/json', 557 | 'Content-Type: application/x-www-form-urlencoded', 558 | 'Expect:' 559 | ]); 560 | curl_setopt($ch, CURLOPT_USERAGENT, $ua); 561 | curl_setopt($ch, CURLOPT_TIMEOUT, 30); 562 | 563 | $body = curl_exec($ch); 564 | $http = curl_getinfo($ch, CURLINFO_HTTP_CODE); 565 | $err = curl_error($ch); 566 | curl_close($ch); 567 | 568 | $json = null; 569 | if ($body !== false) { 570 | $json = json_decode($body, true); 571 | } 572 | 573 | $ok = ($body !== false && $http >= 200 && $http < 300); 574 | if ($ok && is_array($json) && isset($json['status'])) { 575 | $ok = in_array($json['status'], ['OK', 'No commands'], true); 576 | } 577 | 578 | return [ 579 | 'ok' => $ok, 580 | 'http_code' => $http, 581 | 'body' => $body, 582 | 'error' => $err ?: null, 583 | 'json' => $json, 584 | 'debug' => [ 585 | 'format' => $format, 586 | 'bytes' => strlen((string)$data), 587 | 'first100' => mb_substr((string)$data, 0, 100, 'UTF-8'), 588 | 'userAgent'=> $ua, 589 | ], 590 | ]; 591 | } 592 | 593 | protected function parseIds($s) { 594 | $out = []; 595 | foreach (preg_split('/[\s,;]+/', (string)$s) as $p) { $p = trim($p); if ($p !== '' && ctype_digit($p)) $out[] = (int)$p; } 596 | return array_values(array_unique($out)); 597 | } 598 | 599 | protected function validateCSRF($request) { 600 | if (method_exists($request, 'checkCSRF')) { $request->checkCSRF(); return; } 601 | $token = $request->getUserVar('csrfToken'); 602 | if (!$token) { throw new \Exception('Invalid CSRF token'); } 603 | } 604 | 605 | protected function getCsrfTokenSafe($request) { 606 | try { 607 | $session = $request->getSession(); 608 | if ($session) { 609 | if (method_exists($session, 'getCSRFToken')) return (string)$session->getCSRFToken(); 610 | if (method_exists($session, 'get')) { $t = $session->get('csrfToken'); if ($t) return (string)$t; } 611 | if (method_exists($session, 'token')) return (string)$session->token(); 612 | } 613 | } catch (\Throwable $e) {} 614 | return ''; 615 | } 616 | 617 | public function executeCLI($scriptName, &$args) { return 0; } 618 | public function usage($scriptName) {} 619 | } 620 | -------------------------------------------------------------------------------- /QuickStatementsBuilder.inc.php: -------------------------------------------------------------------------------- 1 | , Queen Arwa University 15 | * Repository: https://github.com/saddamalsalfi/ojs-quickstatements-export 16 | * Issues: https://github.com/saddamalsalfi/ojs-quickstatements-export/issues 17 | * 18 | * (c) 2025 Saddam Al-Slfi / Queen Arwa University. All rights reserved. 19 | * 20 | * License: GNU General Public License v3.0 or later 21 | * SPDX-License-Identifier: GPL-3.0-or-later 22 | * 23 | * This file is part of the "OJS QuickStatements Export" plugin. 24 | * See the LICENSE file distributed with this source for full terms. 25 | * 26 | * Security note: 27 | * - Escape all user-facing output (TemplateManager assigns, Smarty templates). 28 | * - Validate/normalize external inputs (DOIs, usernames, tokens). 29 | * - Provide a descriptive User-Agent when calling external APIs (Toolforge). 30 | */ 31 | 32 | 33 | 34 | 35 | class QuickStatementsBuilder 36 | { 37 | /** @var string|null QID الخاص بالمجلة (P1433) */ 38 | protected $journalQid = null; 39 | 40 | /** @var array أكواد اللغات المفضلة بالترتيب (ar, en, ...) */ 41 | protected $preferredLangs = array(); 42 | 43 | /** @var array كاش بسيط لاستعلامات WDQS/API */ 44 | protected $wdqsCache = array(); 45 | 46 | public function __construct($journalQid = null, $preferredLangs = array()) 47 | { 48 | $this->journalQid = $journalQid; 49 | $pl = array(); 50 | foreach ((array)$preferredLangs as $l) { 51 | $l = strtolower(trim((string)$l)); 52 | if ($l !== '') { $pl[] = $l; } 53 | } 54 | $this->preferredLangs = $pl; 55 | } 56 | 57 | /* ========================= 58 | * دوال بناء البيانات 59 | * ========================= */ 60 | 61 | /** 62 | * يبني صفوف (rows) جاهزة للتصدير من لائحة submissions (أرقام أو كائنات) 63 | * $opts: 64 | * includeAuthors => bool 65 | * includeRefs => bool 66 | * resolveAffil => bool (محاولة تحويل الانتماء المؤسسي إلى QID عبر label) 67 | * resolveRefs => bool (محاولة تحويل DOIs إلى QIDs للمراجع) 68 | * updateIfExists => bool (إن وُجد QID لنفس DOI يبدأ التحديث به بدلاً من CREATE) 69 | * skipLabelsOnUpdate => bool (تخطي Lxx/Dxx عند التحديث؛ افتراضيًا true) 70 | */ 71 | public function buildForSubmissions($context, $submissions, $withLabels = true, $opts = array()) 72 | { 73 | $rows = array(); 74 | $firstRow = true; 75 | 76 | foreach ((array)$submissions as $submission) { 77 | 78 | // جلب كائن الـ submission عند تمرير ID 79 | if (!is_object($submission) || !method_exists($submission, 'getCurrentPublication')) { 80 | if (is_numeric($submission)) { 81 | $submission = \APP\facades\Repo::submission()->get((int)$submission); 82 | } elseif (is_array($submission) && isset($submission['id'])) { 83 | $submission = \APP\facades\Repo::submission()->get((int)$submission['id']); 84 | } 85 | } 86 | if (!$submission || !is_object($submission) || !method_exists($submission, 'getCurrentPublication')) { 87 | continue; 88 | } 89 | 90 | // Current publication 91 | $pub = $submission->getCurrentPublication(); 92 | if ($pub && !is_object($pub)) { 93 | $pub = \APP\facades\Repo::publication()->get((int)$pub); 94 | } 95 | if (!$pub) { 96 | $pubId = (int)$submission->getData('currentPublicationId'); 97 | if ($pubId) { $pub = \APP\facades\Repo::publication()->get($pubId); } 98 | } 99 | if (!$pub) { continue; } 100 | 101 | // العنوان واللغات 102 | $titlesRaw = $pub->getData('title'); 103 | $titles = array(); 104 | if (is_array($titlesRaw)) { 105 | $titles = $titlesRaw; 106 | } elseif (is_string($titlesRaw) && $titlesRaw !== '') { 107 | $lc = strtolower(substr((string)$context->getPrimaryLocale(), 0, 2)); 108 | $titles[$lc] = $titlesRaw; 109 | } 110 | 111 | $langs = $this->langsInOrder($this->preferredLangs, $titles); 112 | if (empty($langs)) { 113 | $pl = (string)$context->getPrimaryLocale(); 114 | $langs = array(strtolower(substr($pl, 0, 2))); 115 | } 116 | 117 | // التاريخ/المجلد/العدد/الصفحات 118 | $datePub = (string)$pub->getData('datePublished'); 119 | $dateSub = (string)$submission->getData('datePublished'); 120 | $date = $datePub ?: $dateSub; 121 | 122 | $issueId = $pub->getData('issueId'); 123 | $issue = $issueId ? \APP\facades\Repo::issue()->get($issueId) : null; 124 | $volume = $issue ? $issue->getVolume() : null; 125 | $number = $issue ? $issue->getNumber() : null; 126 | $pages = (string)$pub->getData('pages'); 127 | 128 | // DOI (بشكل متوافق مع 3.5) 129 | $doi = ''; 130 | if (method_exists($pub, 'getDoi')) { 131 | $tmp = $pub->getDoi(); // قد يكون string أو كائن 132 | if (is_object($tmp) && method_exists($tmp, 'getData')) { 133 | $doi = (string)$tmp->getData('value'); 134 | } elseif (is_string($tmp)) { 135 | $doi = $tmp; 136 | } 137 | } 138 | if (!$doi) { 139 | $doi = (string)$pub->getData('doi'); 140 | } 141 | 142 | // تحضير الصف 143 | $row = array(); 144 | 145 | // === التسميات (Labels / Descriptions) === 146 | if ($withLabels) { 147 | foreach ($langs as $lc) { 148 | if (!empty($titles[$lc])) { 149 | $row['L' . $lc] = $this->quote($this->sanitizeTitle($titles[$lc])); 150 | } 151 | } 152 | if (!empty($langs)) { 153 | $row['D' . $langs[0]] = $this->quote('scholarly article'); 154 | } 155 | } 156 | 157 | // === الخصائص الأساسية === 158 | $row['P31'] = 'Q13442814'; // instance of: scholarly article 159 | if (!empty($langs)) { 160 | $lc = $langs[0]; 161 | if (!empty($titles[$lc])) { 162 | $row['P1476'] = $lc . ':' . $this->quote($this->sanitizeTitle($titles[$lc])); // title (نظيف) 163 | } 164 | } 165 | if (!empty($date)) { $row['P577'] = $this->quickTime($date); } 166 | if (!empty($this->journalQid)) { $row['P1433'] = $this->journalQid; } 167 | if (!empty($volume)) { $row['P478'] = $this->quote((string)$volume); } 168 | if (!empty($number)) { $row['P433'] = $this->quote((string)$number); } 169 | if (!empty($pages)) { $row['P304'] = $this->quote($pages); } 170 | if (!empty($doi)) { $row['P356'] = $this->quote($doi); } 171 | if (!empty($langs)) { 172 | $qidLang = $this->languageQid($langs[0]); 173 | if ($qidLang) { $row['P407'] = $qidLang; } 174 | } 175 | 176 | // === الروابط === 177 | $galleyUrl = $this->onePublicGalleyUrl($context, $submission, $pub); 178 | if (!empty($galleyUrl)) { $row['P953'] = $this->quote($galleyUrl); } 179 | $landing = $this->articleLandingUrl($context, $submission); 180 | if (!empty($landing)) { $row['P856'] = $this->quote($landing); } 181 | 182 | // المؤلفون 183 | if (!empty($opts['includeAuthors'])) { 184 | $authors = array(); 185 | $a = $pub->getData('authors'); 186 | if (is_array($a) && !empty($a)) { 187 | $authors = $a; 188 | } 189 | if (empty($authors)) { 190 | try { 191 | $collector = \APP\facades\Repo::author() 192 | ->getCollector() 193 | ->filterByPublicationIds(array($pub->getId())); 194 | foreach ($collector->getMany() as $au) { 195 | $authors[] = $au; 196 | } 197 | } catch (\Throwable $e) {} 198 | } 199 | 200 | $i = 1; 201 | foreach ($authors as $au) { 202 | $name = ''; 203 | $affText = ''; 204 | if (is_object($au)) { 205 | if (method_exists($au, 'getFullName')) { 206 | $name = (string)$au->getFullName(); 207 | } 208 | if (method_exists($au, 'getLocalizedAffiliation')) { 209 | $affText = (string)$au->getLocalizedAffiliation(); 210 | } elseif (method_exists($au, 'getAffiliation')) { 211 | $affText = (string)$au->getAffiliation(); 212 | } 213 | } elseif (is_array($au)) { 214 | $name = isset($au['fullName']) 215 | ? (string)$au['fullName'] 216 | : trim((string)($au['givenName'] ?? '') . ' ' . (string)($au['familyName'] ?? '')); 217 | $affText = isset($au['affiliation']) ? (string)$au['affiliation'] : ''; 218 | } elseif (is_string($au)) { 219 | $name = trim($au); 220 | } 221 | 222 | $name = trim($name); 223 | if ($name === '') { continue; } 224 | 225 | if (!isset($row['__authors'])) { $row['__authors'] = array(); } 226 | $row['__authors'][] = array('name'=>$name,'aff'=>(string)$affText,'ordinal'=>$i); 227 | $i++; 228 | } 229 | } 230 | 231 | // المراجع: استخراج DOIs 232 | if (!empty($opts['includeRefs'])) { 233 | $citRaw = $pub->getData('citations'); 234 | $flat = $this->flattenCitations($citRaw); 235 | if (empty($flat)) { 236 | $citRaw2 = $pub->getData('citation'); 237 | $flat = $this->flattenCitations($citRaw2); 238 | } 239 | $dois = array(); 240 | foreach ($flat as $line) { 241 | if (!is_string($line)) { continue; } 242 | if (preg_match_all('~10\.\d{4,9}/[-._;()/:A-Za-z0-9]+~', $line, $m)) { 243 | foreach ($m[0] as $d) { $dois[] = rtrim($d, " \t\n\r\0\x0B.,;)"); } 244 | } 245 | } 246 | if (!empty($dois)) { 247 | $row['__refDois'] = array_values(array_unique($dois)); 248 | } 249 | } 250 | 251 | if ($firstRow) { 252 | if (!array_key_exists('skipLabelsOnUpdate', $opts)) { 253 | $opts['skipLabelsOnUpdate'] = true; 254 | } 255 | $row['__opts'] = (array)$opts; 256 | $firstRow = false; 257 | } 258 | 259 | $rows[] = $row; 260 | } 261 | 262 | return $rows; 263 | } 264 | 265 | /** تنظيف العنوان من HTML وتوحيد المسافات */ 266 | protected function sanitizeTitle($s) 267 | { 268 | $s = (string)$s; 269 | $s = html_entity_decode($s, ENT_QUOTES | ENT_HTML5, 'UTF-8'); 270 | $s = strip_tags($s); 271 | $s = preg_replace('~\s+~u', ' ', $s); 272 | return trim($s); 273 | } 274 | 275 | /** إخراج TSV (المستوى الأول؛ المؤلف الأول فقط) */ 276 | public function streamTSV($rows) 277 | { 278 | foreach ($rows as &$r) { 279 | if (isset($r['__authors']) && is_array($r['__authors']) && !empty($r['__authors'])) { 280 | $au = $r['__authors'][0]; 281 | $nm = '"' . str_replace('"', '\\"', (string)$au['name']) . '"'; 282 | $r['P2093'] = $nm; 283 | $r['P2093|P1545'] = '"' . strval($au['ordinal']) . '"'; 284 | } 285 | } unset($r); 286 | 287 | $allKeys = array(); 288 | foreach ($rows as $r) { 289 | foreach ($r as $k => $v) { 290 | if (strpos($k, '__') === 0) { continue; } 291 | if (is_array($v)) { continue; } 292 | if (!in_array($k, $allKeys, true)) { $allKeys[] = $k; } 293 | } 294 | } 295 | if (empty($allKeys)) { $allKeys = array('P31'); } 296 | 297 | echo implode("\t", $allKeys) . "\n"; 298 | foreach ($rows as $r) { 299 | $line = array(); 300 | foreach ($allKeys as $k) { 301 | $val = isset($r[$k]) ? $r[$k] : ''; 302 | if (is_array($val)) { $val = ''; } 303 | $line[] = (string)$val; 304 | } 305 | echo implode("\t", $line) . "\n"; 306 | } 307 | } 308 | 309 | /** 310 | * إخراج أوامر QuickStatements مع تحقق مسبق (API ثم WDQS) 311 | */ 312 | public function streamCommands($rows) 313 | { 314 | $opts = isset($rows[0]['__opts']) ? (array)$rows[0]['__opts'] : array(); 315 | $resolveAffil = !empty($opts['resolveAffil']); 316 | $resolveRefs = !empty($opts['resolveRefs']); 317 | $updateIfExists = !empty($opts['updateIfExists']); 318 | $skipLabelsOnUpdate = array_key_exists('skipLabelsOnUpdate', $opts) ? (bool)$opts['skipLabelsOnUpdate'] : true; 319 | 320 | foreach ($rows as $r) { 321 | $target = 'LAST'; 322 | $isCreate = true; 323 | 324 | // 1) إن كان هناك DOI: جرّب Wikidata API أولاً، ثم WDQS احتياطاً 325 | if ($updateIfExists && !empty($r['P356'])) { 326 | $doi = $this->normalizeDoi($this->stripQuotes((string)$r['P356'])); 327 | if ($doi !== '') { 328 | $qid = $this->wdApiFindByDOI($doi); 329 | if (!$qid) { $qid = $this->wdqsFindByDOI($doi); } 330 | if ($qid) { 331 | $target = $qid; 332 | $isCreate = false; 333 | echo "# matched by DOI via API: {$doi} -> {$qid}\n"; 334 | } else { 335 | echo "# no existing item found for DOI (API+WDQS): {$doi}\n"; 336 | } 337 | } 338 | } 339 | 340 | // 2) إن لم نجد: جرّب العنوان+المجلّة عبر API، ثم WDQS بالـmetadata 341 | if ($updateIfExists && $isCreate && !empty($r['P1476']) && !empty($r['P1433'])) { 342 | $mono = $this->parseMonolingual((string)$r['P1476']); // [lc, text] 343 | if ($mono) { 344 | list($lc, $title) = $mono; 345 | $journalQid = (string)$r['P1433']; 346 | $year = $this->extractYearFromQuickTime(isset($r['P577']) ? (string)$r['P577'] : ''); 347 | $vol = isset($r['P478']) ? $this->stripQuotes((string)$r['P478']) : ''; 348 | $iss = isset($r['P433']) ? $this->stripQuotes((string)$r['P433']) : ''; 349 | 350 | $qid2 = $this->wdApiFindByLabelAndJournal($title, $lc, $journalQid); 351 | if (!$qid2) { $qid2 = $this->wdqsFindByMetadata($title, $lc, $journalQid, $year, $vol, $iss); } 352 | if ($qid2) { 353 | $target = $qid2; 354 | $isCreate = false; 355 | echo "# matched by metadata via API/WDQS: {$qid2}\n"; 356 | } else { 357 | echo "# no match by metadata (title/journal) via API/WDQS\n"; 358 | } 359 | } 360 | } 361 | 362 | // طباعة CREATE فقط عند الإنشاء 363 | if ($isCreate) { 364 | echo "CREATE\n"; 365 | $target = 'LAST'; 366 | } 367 | 368 | // خصائص العنصر 369 | foreach ($r as $k => $v) { 370 | if (strpos($k, '__') === 0) { continue; } 371 | if (is_array($v)) { continue; } 372 | if ($v === '') { continue; } 373 | if (!$isCreate && $skipLabelsOnUpdate && preg_match('~^[LD][a-z]{2}$~', $k)) { 374 | continue; 375 | } 376 | echo "{$target}|{$k}|{$v}\n"; 377 | } 378 | 379 | // المؤلفون 380 | if (isset($r['__authors']) && is_array($r['__authors'])) { 381 | foreach ($r['__authors'] as $au) { 382 | $nm = '"' . str_replace('"', '\\"', (string)$au['name']) . '"'; 383 | $ord = '"' . strval($au['ordinal']) . '"'; 384 | $line = "{$target}|P2093|{$nm}|P1545|{$ord}"; 385 | if (!empty($au['aff']) && $resolveAffil) { 386 | $qidAff = $this->wdqsFindByLabel((string)$au['aff']); 387 | if ($qidAff) { $line .= "|P1416|{$qidAff}"; } 388 | } 389 | echo $line . "\n"; 390 | } 391 | } 392 | 393 | // المراجع 394 | if ($resolveRefs && isset($r['__refDois']) && is_array($r['__refDois'])) { 395 | foreach ($r['__refDois'] as $doiRaw) { 396 | $doi = $this->normalizeDoi((string)$doiRaw); 397 | if ($doi === '') { continue; } 398 | $qidRef = $this->wdApiFindByDOI($doi); 399 | if (!$qidRef) { $qidRef = $this->wdqsFindByDOI($doi); } 400 | if ($qidRef) { 401 | echo "{$target}|P2860|{$qidRef}\n"; 402 | } else { 403 | echo "# ref DOI not found (API+WDQS): {$doi}\n"; 404 | } 405 | } 406 | } 407 | 408 | echo "\n"; 409 | } 410 | } 411 | 412 | /* ========================= 413 | * أدوات مساعدة 414 | * ========================= */ 415 | 416 | protected function quote($s) 417 | { 418 | $s = (string)$s; 419 | $s = str_replace(array("\r", "\n"), ' ', $s); 420 | $s = trim($s); 421 | return '"' . str_replace('"', '\\"', $s) . '"'; 422 | } 423 | 424 | protected function stripQuotes($s) 425 | { 426 | $s = (string)$s; 427 | if (strlen($s) >= 2 && $s[0] === '"' && substr($s, -1) === '"') { 428 | return stripcslashes(substr($s, 1, -1)); 429 | } 430 | return $s; 431 | } 432 | 433 | protected function normalizeDoi($s) 434 | { 435 | $s = strtolower(trim((string)$s)); 436 | $s = preg_replace('~^(https?://(dx\\.)?doi\\.org/|doi:)~', '', $s); 437 | $s = rtrim($s, " \t\n\r\0\x0B.,;)"); 438 | return $s; 439 | } 440 | 441 | protected function quickTime($date) 442 | { 443 | $date = trim((string)$date); 444 | if ($date === '') { return ''; } 445 | 446 | if (preg_match('~^\d{4}-\d{2}-\d{2}$~', $date)) { return '+' . $date . 'T00:00:00Z/11'; } 447 | if (preg_match('~^\d{4}-\d{2}$~', $date)) { return '+' . $date . '-01T00:00:00Z/10'; } 448 | if (preg_match('~^\d{4}$~', $date)) { return '+' . $date . '-01-01T00:00:00Z/9'; } 449 | 450 | $ts = strtotime($date); 451 | if ($ts) { return '+' . date('Y-m-d', $ts) . 'T00:00:00Z/11'; } 452 | return ''; 453 | } 454 | 455 | protected function langsInOrder($preferred, $titles) 456 | { 457 | $out = array(); 458 | $preferred = array_values(array_unique(array_map('strval', (array)$preferred))); 459 | foreach ($preferred as $lc) { 460 | $lc = strtolower($lc); 461 | if (isset($titles[$lc]) && !in_array($lc, $out, true)) { $out[] = $lc; } 462 | } 463 | foreach ((array)$titles as $lc => $_v) { 464 | $lc = strtolower((string)$lc); 465 | if (!in_array($lc, $out, true)) { $out[] = $lc; } 466 | } 467 | return $out; 468 | } 469 | 470 | protected function languageQid($lc) 471 | { 472 | $lc = strtolower((string)$lc); 473 | $map = array( 474 | 'ar' => 'Q13955', 475 | 'en' => 'Q1860', 476 | 'fr' => 'Q150', 477 | 'de' => 'Q188', 478 | 'es' => 'Q1321', 479 | 'it' => 'Q652', 480 | 'ru' => 'Q7737', 481 | 'fa' => 'Q9168', 482 | 'tr' => 'Q256', 483 | ); 484 | return isset($map[$lc]) ? $map[$lc] : ''; 485 | } 486 | 487 | protected function onePublicGalleyUrl($context, $submission, $pub) 488 | { 489 | try { 490 | $galleys = (array)$pub->getData('galleys'); 491 | foreach ($galleys as $g) { 492 | if (is_object($g) && method_exists($g, 'getBestId')) { 493 | $dispatcher = \APP\Application::get()->getDispatcher(); 494 | $request = \APP\Application::get()->getRequest(); 495 | return $dispatcher->url( 496 | $request, 497 | \PKP\core\PKPApplication::ROUTE_PAGE, 498 | $context->getPath(), 499 | 'article', 500 | 'view', 501 | array($submission->getBestId(), $g->getBestId()) 502 | ); 503 | } 504 | } 505 | } catch (\Throwable $e) {} 506 | return ''; 507 | } 508 | 509 | protected function articleLandingUrl($context, $submission) 510 | { 511 | try { 512 | $dispatcher = \APP\Application::get()->getDispatcher(); 513 | $request = \APP\Application::get()->getRequest(); 514 | return $dispatcher->url( 515 | $request, 516 | \PKP\core\PKPApplication::ROUTE_PAGE, 517 | $context->getPath(), 518 | 'article', 519 | 'view', 520 | array($submission->getBestId()) 521 | ); 522 | } catch (\Throwable $e) { 523 | return ''; 524 | } 525 | } 526 | 527 | protected function flattenCitations($raw) 528 | { 529 | $out = array(); 530 | 531 | if (is_string($raw)) { 532 | $raw = trim($raw); 533 | if ($raw !== '') { 534 | $parts = preg_split('~\r?\n~', $raw); 535 | foreach ($parts as $p) { $p = trim($p); if ($p !== '') { $out[] = $p; } } 536 | } 537 | return $out; 538 | } 539 | 540 | if (is_array($raw)) { 541 | foreach ($raw as $c) { 542 | if (is_string($c)) { 543 | $c = trim($c); 544 | if ($c !== '') { $out[] = $c; } 545 | continue; 546 | } 547 | if (is_object($c)) { 548 | try { 549 | if (method_exists($c, 'getRawCitation')) { 550 | $t = (string)$c->getRawCitation(); $t = trim($t); 551 | if ($t !== '') { $out[] = $t; continue; } 552 | } 553 | if (method_exists($c, 'getCitation')) { 554 | $t = (string)$c->getCitation(); $t = trim($t); 555 | if ($t !== '') { $out[] = $t; continue; } 556 | } 557 | if (method_exists($c, 'getData')) { 558 | $t = (string)$c->getData('rawCitation'); $t = trim($t); 559 | if ($t !== '') { $out[] = $t; continue; } 560 | } 561 | } catch (\Throwable $e) {} 562 | } 563 | } 564 | return $out; 565 | } 566 | 567 | if (is_object($raw)) { 568 | try { 569 | if (method_exists($raw, 'getRawCitation')) { 570 | $t = trim((string)$raw->getRawCitation()); if ($t !== '') { $out[] = $t; } 571 | } elseif (method_exists($raw, 'getCitation')) { 572 | $t = trim((string)$raw->getCitation()); if ($t !== '') { $out[] = $t; } 573 | } elseif (method_exists($raw, 'getData')) { 574 | $t = trim((string)$raw->getData('rawCitation')); if ($t !== '') { $out[] = $t; } 575 | } 576 | } catch (\Throwable $e) {} 577 | } 578 | 579 | return $out; 580 | } 581 | 582 | /* ========================= 583 | * استعلامات WDQS للمطابقة (احتياطيًا) 584 | * ========================= */ 585 | 586 | protected function wdqsFindByDOI($doi) 587 | { 588 | $doi = $this->normalizeDoi($doi); 589 | if ($doi === '') { return ''; } 590 | 591 | $cacheKey = 'doi:' . $doi; 592 | if (isset($this->wdqsCache[$cacheKey])) { 593 | return $this->wdqsCache[$cacheKey]; 594 | } 595 | 596 | $query = 'SELECT ?item WHERE { ' . 597 | ' ?item wdt:P356 ?d . ' . 598 | ' FILTER(LCASE(STR(?d)) = "' . addslashes($doi) . '") ' . 599 | '} LIMIT 1'; 600 | 601 | $qid = $this->runWdqsReturnQid($query); 602 | $this->wdqsCache[$cacheKey] = $qid; 603 | return $qid; 604 | } 605 | 606 | protected function wdqsFindByLabel($label) 607 | { 608 | $label = trim((string)$label); 609 | if ($label === '') { return ''; } 610 | $langs = array_unique(array_merge(['ar','en'], $this->preferredLangs)); 611 | foreach ($langs as $lc) { 612 | $qid = $this->wdqsFindByLabelLang($label, $lc); 613 | if ($qid) { return $qid; } 614 | } 615 | return ''; 616 | } 617 | 618 | protected function wdqsFindByLabelLang($label, $lc) 619 | { 620 | $lc = strtolower((string)$lc); 621 | $labelNorm = strtolower(trim((string)$label)); 622 | if ($labelNorm === '' || $lc === '') { return ''; } 623 | 624 | $cacheKey = 'label:' . $lc . ':' . $labelNorm; 625 | if (isset($this->wdqsCache[$cacheKey])) { 626 | return $this->wdqsCache[$cacheKey]; 627 | } 628 | 629 | $query = 'SELECT ?item WHERE { ' . 630 | ' ?item rdfs:label ?l . ' . 631 | ' FILTER(LANG(?l) = "' . addslashes($lc) . '") . ' . 632 | ' FILTER(LCASE(STR(?l)) = "' . addslashes($labelNorm) . '") ' . 633 | '} LIMIT 1'; 634 | 635 | $qid = $this->runWdqsReturnQid($query); 636 | $this->wdqsCache[$cacheKey] = $qid; 637 | return $qid; 638 | } 639 | 640 | protected function wdqsFindByMetadata($title, $lc, $journalQid, $year = '', $volume = '', $issue = '') 641 | { 642 | $title = $this->sanitizeTitle($title); 643 | $lc = strtolower(trim((string)$lc)); 644 | $journalQid = trim((string)$journalQid); 645 | if ($title === '' || $lc === '' || $journalQid === '') { return ''; } 646 | 647 | $titleLC = strtolower($title); 648 | 649 | $cacheKey = 'meta:' . $lc . ':' . md5($titleLC) . ':' . $journalQid . ':' . $year . ':' . $volume . ':' . $issue; 650 | if (isset($this->wdqsCache[$cacheKey])) { 651 | return $this->wdqsCache[$cacheKey]; 652 | } 653 | 654 | $filters = array(); 655 | $filters[] = ' ?item wdt:P31 wd:Q13442814 .'; 656 | $filters[] = ' ?item wdt:P1433 wd:' . addslashes($journalQid) . ' .'; 657 | $filters[] = ' ?item p:P1476 ?st .'; 658 | $filters[] = ' ?st ps:P1476 ?t .'; 659 | $filters[] = ' FILTER(LANG(?t) = "' . addslashes($lc) . '") .'; 660 | $filters[] = ' FILTER(LCASE(STR(?t)) = "' . addslashes($titleLC) . '") .'; 661 | 662 | if ($year !== '') { 663 | $filters[] = ' OPTIONAL { ?item wdt:P577 ?d . }'; 664 | $filters[] = ' FILTER(!BOUND(?d) || YEAR(?d) = ' . intval($year) . ') .'; 665 | } 666 | if ($volume !== '') { 667 | $filters[] = ' OPTIONAL { ?item wdt:P478 ?vol . }'; 668 | $filters[] = ' FILTER(!BOUND(?vol) || STR(?vol) = "' . addslashes($volume) . '") .'; 669 | } 670 | if ($issue !== '') { 671 | $filters[] = ' OPTIONAL { ?item wdt:P433 ?iss . }'; 672 | $filters[] = ' FILTER(!BOUND(?iss) || STR(?iss) = "' . addslashes($issue) . '") .'; 673 | } 674 | 675 | $query = "SELECT ?item WHERE {\n" . implode("\n", $filters) . "\n} LIMIT 1"; 676 | 677 | $qid = $this->runWdqsReturnQid($query); 678 | $this->wdqsCache[$cacheKey] = $qid; 679 | return $qid; 680 | } 681 | 682 | protected function runWdqsReturnQid($query) 683 | { 684 | $url = 'https://query.wikidata.org/sparql?format=json&query=' . urlencode($query); 685 | try { 686 | $ch = curl_init($url); 687 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 688 | curl_setopt($ch, CURLOPT_USERAGENT, 'OJS-QuickStatements-Exporter/1.1'); 689 | curl_setopt($ch, CURLOPT_TIMEOUT, 8); 690 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/sparql-results+json']); 691 | $res = curl_exec($ch); 692 | if ($res === false) { return ''; } 693 | $data = json_decode($res, true); 694 | if (!isset($data['results']['bindings'][0]['item']['value'])) { return ''; } 695 | $uri = $data['results']['bindings'][0]['item']['value']; 696 | if (preg_match('~Q(\d+)$~', $uri, $m)) { return 'Q' . $m[1]; } 697 | } catch (\Throwable $e) {} 698 | return ''; 699 | } 700 | 701 | /* ========================= 702 | * Wikidata API (Action API) — تحقق فوري 703 | * ========================= */ 704 | 705 | /** ابحث بالـDOI عبر API باستخدام haswbstatement:P356=... (تجربة بدون/مع اقتباس) */ 706 | protected function wdApiFindByDOI($doi) 707 | { 708 | $doi = $this->normalizeDoi($doi); 709 | if ($doi === '') { return ''; } 710 | 711 | // المحاولة 1: بدون اقتباس 712 | $q = 'haswbstatement:P356=' . $doi; 713 | $qid = $this->wdApiSingleResultQid($q); 714 | if ($qid) { return $qid; } 715 | 716 | // المحاولة 2: مع اقتباس (بعض القيم الحساسة للرموز تعمل أفضل) 717 | $q = 'haswbstatement:P356="' . $this->escapeForSrsearch($doi) . '"'; 718 | return $this->wdApiSingleResultQid($q); 719 | } 720 | 721 | /** ابحث بعنوان + لغة + المجلّة */ 722 | protected function wdApiFindByLabelAndJournal($title, $lc, $journalQid) 723 | { 724 | $title = $this->sanitizeTitle($title); 725 | $lc = strtolower(trim((string)$lc)); 726 | $journalQid = trim((string)$journalQid); 727 | if ($title === '' || $lc === '' || $journalQid === '') { return ''; } 728 | 729 | // نقيّد بنوع "مقال علمي" + المجلة + label.:"النص" 730 | $parts = array( 731 | 'haswbstatement:P31=Q13442814', 732 | 'haswbstatement:P1433=' . $journalQid, 733 | 'label.' . $lc . ':"' . $this->escapeForSrsearch($title) . '"' 734 | ); 735 | $q = implode(' ', $parts); 736 | return $this->wdApiSingleResultQid($q); 737 | } 738 | 739 | /** نفّذ استعلام list=search وأعد QID واحد إن وُجد */ 740 | protected function wdApiSingleResultQid($srsearch) 741 | { 742 | // نطلب نتيجة واحدة فقط 743 | $url = 'https://www.wikidata.org/w/api.php?action=query&format=json&list=search' 744 | . '&srnamespace=0&srlimit=1&srinfo=totalhits&srprop=' 745 | . '&srsearch=' . urlencode($srsearch); 746 | 747 | $data = $this->httpGetJson($url); 748 | if (!$data || !isset($data['query']['search'][0]['title'])) { return ''; } 749 | $t = (string)$data['query']['search'][0]['title']; // مثل "Q12345" 750 | if (preg_match('~^Q\d+$~', $t)) { return $t; } 751 | return ''; 752 | } 753 | 754 | /** استدعاء GET يعيد JSON */ 755 | protected function httpGetJson($url) 756 | { 757 | try { 758 | $ch = curl_init($url); 759 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 760 | curl_setopt($ch, CURLOPT_USERAGENT, 'OJS-QuickStatements-Exporter/1.1'); 761 | curl_setopt($ch, CURLOPT_TIMEOUT, 8); 762 | curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']); 763 | $res = curl_exec($ch); 764 | if ($res === false) { return null; } 765 | $data = json_decode($res, true); 766 | return is_array($data) ? $data : null; 767 | } catch (\Throwable $e) { 768 | return null; 769 | } 770 | } 771 | 772 | /** للهروب داخل srsearch (داخل علامات اقتباس) */ 773 | protected function escapeForSrsearch($s) 774 | { 775 | $s = (string)$s; 776 | return str_replace('"', '\"', $s); 777 | } 778 | 779 | /* ======== أدوات تحليل قيم الحقول ======== */ 780 | 781 | /** تفكيك قيمة P1476 بنمط quickstatements: ar:"النص" => [ 'ar', 'النص' ] */ 782 | protected function parseMonolingual($v) 783 | { 784 | $v = (string)$v; 785 | if (!preg_match('~^([a-z]{2,3}):"(.*)"$~us', $v, $m)) { 786 | return null; 787 | } 788 | $lc = strtolower($m[1]); 789 | $text = stripcslashes($m[2]); 790 | return array($lc, $text); 791 | } 792 | 793 | /** استخراج السنة من وقت بصيغة QuickStatements مثل +2011-12-30T00:00:00Z/11 */ 794 | protected function extractYearFromQuickTime($qsTime) 795 | { 796 | if (preg_match('~\+(\d{4})~', (string)$qsTime, $m)) { 797 | return $m[1]; 798 | } 799 | return ''; 800 | } 801 | } 802 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | SPDX-License-Identifier: GPL-3.0-or-later 2 | 3 | GNU GENERAL PUBLIC LICENSE 4 | Version 3, 29 June 2007 5 | 6 | Copyright (C) 2007 Free Software Foundation, Inc. 7 | Everyone is permitted to copy and distribute verbatim copies 8 | of this license document, but changing it is not allowed. 9 | 10 | Preamble 11 | 12 | The GNU General Public License is a free, copyleft license for 13 | software and other kinds of works. 14 | 15 | The licenses for most software and other practical works are designed 16 | to take away your freedom to share and change the works. By contrast, 17 | the GNU General Public License is intended to guarantee your freedom to 18 | share and change all versions of a program--to make sure it remains free 19 | software for all its users. We, the Free Software Foundation, use the 20 | GNU General Public License for most of our software; it applies also to 21 | any other work released this way by its authors. You can apply it to 22 | your programs, too. 23 | 24 | When we speak of free software, we are referring to freedom, not 25 | price. Our General Public Licenses are designed to make sure that you 26 | have the freedom to distribute copies of free software (and charge for 27 | them if you wish), that you receive source code or can get it if you 28 | want it, that you can change the software or use pieces of it in new 29 | free programs, and that you know you can do these things. 30 | 31 | To protect your rights, we need to prevent others from denying you 32 | these rights or asking you to surrender the rights. Therefore, you have 33 | certain responsibilities if you distribute copies of the software, or if 34 | you modify it: responsibilities to respect the freedom of others. 35 | 36 | For example, if you distribute copies of such a program, whether 37 | gratis or for a fee, you must pass on to the recipients the same 38 | freedoms that you received. You must make sure that they, too, receive 39 | or can get the source code. And you must show them these terms so they 40 | know their rights. 41 | 42 | Developers that use the GNU GPL protect your rights with two steps: 43 | (1) assert copyright on the software, and (2) offer you this License 44 | giving you legal permission to copy, distribute and/or modify it. 45 | 46 | For the developers' and authors' protection, the GPL clearly explains 47 | that there is no warranty for this free software. For both users' and 48 | authors' sake, the GPL requires that modified versions be marked as 49 | changed, so that their problems will not be attributed erroneously to 50 | authors of previous versions. 51 | 52 | Some devices are designed to deny users access to install or run 53 | modified versions of the software inside them, although the manufacturer 54 | can do so. This is fundamentally incompatible with the aim of 55 | protecting users' freedom to change the software. The systematic 56 | pattern of such abuse occurs in the area of products for individuals to 57 | use, which is precisely where it is most unacceptable. Therefore, we 58 | have designed this version of the GPL to prohibit the practice for those 59 | products. If such problems arise substantially in other domains, we 60 | stand ready to extend this provision to those domains in future versions 61 | of the GPL, as needed to protect the freedom of users. 62 | 63 | Finally, every program is threatened constantly by software patents. 64 | States should not allow patents to restrict development and use of 65 | software on general-purpose computers, but in those that do, we wish to 66 | avoid the special danger that patents applied to a free program could 67 | make it effectively proprietary. To prevent this, the GPL assures that 68 | patents cannot be used to render the program non-free. 69 | 70 | The precise terms and conditions for copying, distribution and 71 | modification follow. 72 | 73 | TERMS AND CONDITIONS 74 | 75 | 0. Definitions. 76 | 77 | "This License" refers to version 3 of the GNU General Public License. 78 | 79 | "Copyright" also means copyright-like laws that apply to other kinds of 80 | works, such as semiconductor masks. 81 | 82 | "The Program" refers to any copyrightable work licensed under this 83 | License. Each licensee is addressed as "you". "Licensees" and 84 | "recipients" may be individuals or organizations. 85 | 86 | To "modify" a work means to copy from or adapt all or part of the work 87 | in a fashion requiring copyright permission, other than the making of an 88 | exact copy. The resulting work is called a "modified version" of the 89 | earlier work or a work "based on" the earlier work. 90 | 91 | A "covered work" means either the unmodified Program or a work based 92 | on the Program. 93 | 94 | To "propagate" a work means to do anything with it that, without 95 | permission, would make you directly or secondarily liable for 96 | infringement under applicable copyright law, except executing it on a 97 | computer or modifying a private copy. Propagation includes copying, 98 | distribution (with or without modification), making available to the 99 | public, and in some countries other activities as well. 100 | 101 | To "convey" a work means any kind of propagation that enables other 102 | parties to make or receive copies. Mere interaction with a user through 103 | a computer network, with no transfer of a copy, is not conveying. 104 | 105 | An interactive user interface displays "Appropriate Legal Notices" 106 | to the extent that it includes a convenient and prominently visible 107 | feature that (1) displays an appropriate copyright notice, and (2) 108 | tells the user that there is no warranty for the work (except to the 109 | extent that warranties are provided), that licensees may convey the 110 | work under this License, and how to view a copy of this License. If 111 | the interface presents a list of user commands or options, such as a 112 | menu, a prominent item in the list meets this criterion. 113 | 114 | 1. Source Code. 115 | 116 | The "source code" for a work means the preferred form of the work 117 | for making modifications to it. "Object code" means any non-source 118 | form of a work. 119 | 120 | A "Standard Interface" means an interface that either is an official 121 | standard defined by a recognized standards body, or, in the case of 122 | interfaces specified for a particular programming language, one that 123 | is widely used among developers working in that language. 124 | 125 | The "System Libraries" of an executable work include anything, other 126 | than the work as a whole, that (a) is included in the normal form of 127 | packaging a Major Component, but which is not part of that Major 128 | Component, and (b) serves only to enable use of the work with that 129 | Major Component, or to implement a Standard Interface for which an 130 | implementation is available to the public in source code form. A 131 | "Major Component", in this context, means a major essential component 132 | (kernel, window system, and so on) of the specific operating system 133 | (if any) on which the executable work runs, or a compiler used to 134 | produce the work, or an object code interpreter used to run it. 135 | 136 | The "Corresponding Source" for a work in object code form means all 137 | the source code needed to generate, install, and (for an executable 138 | work) run the object code and to modify the work, including scripts to 139 | control those activities. However, it does not include the work's 140 | System Libraries, or general-purpose tools or generally available free 141 | programs which are used unmodified in performing those activities but 142 | which are not part of the work. For example, Corresponding Source 143 | includes interface definition files associated with source files for 144 | the work, and the source code for shared libraries and dynamically 145 | linked subprograms that the work is specifically designed to require, 146 | such as by intimate data communication or control flow between those 147 | subprograms and other parts of the work. 148 | 149 | The Corresponding Source need not include anything that users 150 | can regenerate automatically from other parts of the Corresponding 151 | Source. 152 | 153 | The Corresponding Source for a work in source code form is that 154 | same work. 155 | 156 | 2. Basic Permissions. 157 | 158 | All rights granted under this License are granted for the term of 159 | copyright on the Program, and are irrevocable provided the stated 160 | conditions are met. This License explicitly affirms your unlimited 161 | permission to run the unmodified Program. The output from running a 162 | covered work is covered by this License only if the output, given its 163 | content, constitutes a covered work. This License acknowledges your 164 | rights of fair use or other equivalent, as provided by copyright law. 165 | 166 | You may make, run and propagate covered works that you do not 167 | convey, without conditions so long as your license otherwise remains 168 | in force. You may convey covered works to others for the sole purpose 169 | of having them make modifications exclusively for you, or provide you 170 | with facilities for running those works, provided that you comply with 171 | the terms of this License in conveying all material for which you do 172 | not control copyright. Those thus making or running the covered works 173 | for you must do so exclusively on your behalf, under your direction 174 | and control, on terms that prohibit them from making any copies of 175 | your copyrighted material outside their relationship with you. 176 | 177 | Conveying under any other circumstances is permitted solely under 178 | the conditions stated below. Sublicensing is not allowed; section 10 179 | makes it unnecessary. 180 | 181 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 182 | 183 | No covered work shall be deemed part of an effective technological 184 | measure under any applicable law fulfilling obligations under article 11 185 | of the WIPO copyright treaty adopted on 20 December 1996, or similar 186 | laws prohibiting or restricting circumvention of such measures. 187 | 188 | When you convey a covered work, you waive any legal power to forbid 189 | circumvention of technological measures to the extent such circumvention 190 | is effected by exercising rights under this License with respect to 191 | the covered work, and you disclaim any intention to limit operation or 192 | modification of the work as a means of enforcing, against the work's 193 | users, your or third parties' legal rights to forbid circumvention of 194 | technological measures. 195 | 196 | 4. Conveying Verbatim Copies. 197 | 198 | You may convey verbatim copies of the Program's source code as you 199 | receive it, in any medium, provided that you conspicuously and 200 | appropriately publish on each copy an appropriate copyright notice; 201 | keep intact all notices stating that this License and any 202 | non-permissive terms added in accord with section 7 apply to the code; 203 | keep intact all notices of the absence of any warranty; and give all 204 | recipients a copy of this License along with the Program. 205 | 206 | You may charge any price or no price for each copy that you convey, 207 | and you may offer support or warranty protection for a fee. 208 | 209 | 5. Conveying Modified Source Versions. 210 | 211 | You may convey a work based on the Program, or the modifications to 212 | produce it from the Program, in the form of source code under the 213 | terms of section 4, provided that you also meet all of these conditions: 214 | 215 | a) The work must carry prominent notices stating that you modified 216 | it, and giving a relevant date. 217 | 218 | b) The work must carry prominent notices stating that it is 219 | released under this License and any conditions added under section 7. 220 | This requirement modifies the requirement in section 4 to 221 | "keep intact all notices". 222 | 223 | c) You must license the entire work, as a whole, under this 224 | License to anyone who comes into possession of a copy. This 225 | License will therefore apply, along with any applicable section 7 226 | additional terms, to the whole of the work, and all its parts, 227 | regardless of how they are packaged. This License gives no 228 | permission to license the work in any other way, but it does not 229 | invalidate such permission if you have separately received it. 230 | 231 | d) If the work has interactive user interfaces, each must display 232 | Appropriate Legal Notices; however, if the Program has interactive 233 | interfaces that do not display Appropriate Legal Notices, your 234 | work need not make them do so. 235 | 236 | A compilation of a covered work with other separate and independent 237 | works, which are not by their nature extensions of the covered work, 238 | and which are not combined with it such as to form a larger program, 239 | in or on a volume of a storage or distribution medium, is called an 240 | "aggregate" if the compilation and its resulting copyright are not 241 | used to limit the access or legal rights of the compilation's users 242 | beyond what the individual works permit. Inclusion of a covered work 243 | in an aggregate does not cause this License to apply to the other 244 | parts of the aggregate. 245 | 246 | 6. Conveying Non-Source Forms. 247 | 248 | You may convey a covered work in object code form under the terms 249 | of sections 4 and 5, provided that you also convey the 250 | machine-readable Corresponding Source under the terms of this License, 251 | in one of these ways: 252 | 253 | a) Convey the object code in, or embodied in, a physical product 254 | (including a physical distribution medium), accompanied by the 255 | Corresponding Source fixed on a durable physical medium 256 | customarily used for software interchange. 257 | 258 | b) Convey the object code in, or embodied in, a physical product 259 | (including a physical distribution medium), accompanied by a 260 | written offer, valid for at least three years and valid for as 261 | long as you offer spare parts or customer support for that product 262 | model, to give anyone who possesses the object code either (1) a 263 | copy of the Corresponding Source for all the software in the 264 | product that is covered by this License, on a durable physical 265 | medium customarily used for software interchange, for a price no 266 | more than your reasonable cost of physically performing this 267 | conveying of source, or (2) access to copy the Corresponding 268 | Source from a network server at no charge. 269 | 270 | c) Convey individual copies of the object code with a copy of the 271 | written offer to provide the Corresponding Source. This 272 | alternative is allowed only occasionally and noncommercially, and 273 | only if you received the object code with such an offer, in accord 274 | with subsection 6b. 275 | 276 | d) Convey the object code by offering access from a designated 277 | place (gratis or for a charge), and offer equivalent access to the 278 | Corresponding Source in the same way through the same place at no 279 | further charge. You need not require recipients to copy the 280 | Corresponding Source along with the object code. If the place to 281 | copy the object code is a network server, the Corresponding Source 282 | may be on a different server (operated by you or a third party) 283 | that supports equivalent copying facilities, provided you maintain 284 | clear directions next to the object code saying where to find the 285 | Corresponding Source. Regardless of what server hosts the 286 | Corresponding Source, you remain obligated to ensure that it is 287 | available for as long as needed to satisfy these requirements. 288 | 289 | e) Convey the object code using peer-to-peer transmission, provided 290 | you inform other peers where the object code and Corresponding 291 | Source of the work are being offered to the general public at no 292 | charge under subsection 6d. 293 | 294 | A separable portion of the object code, whose source code is excluded 295 | from the Corresponding Source as a System Library, need not be 296 | included in conveying the object code work. 297 | 298 | A "User Product" is either (1) a "consumer product", which means any 299 | tangible personal property which is normally used for personal, family, 300 | or household purposes, or (2) anything designed or sold for incorporation 301 | into a dwelling. In determining whether a product is a consumer product, 302 | doubtful cases shall be resolved in favor of coverage. For a particular 303 | product received by a particular user, "normally used" refers to a 304 | typical or common use of that class of product, regardless of the status 305 | of the particular user or of the way in which the particular user 306 | actually uses, or expects or is expected to use, the product. A product 307 | is a consumer product regardless of whether the product has substantial 308 | commercial, industrial or non-consumer uses, unless such uses represent 309 | the only significant mode of use of the product. 310 | 311 | "Installation Information" for a User Product means any methods, 312 | procedures, authorization keys, or other information required to install 313 | and execute modified versions of a covered work in that User Product from 314 | a modified version of its Corresponding Source. The information must 315 | suffice to ensure that the continued functioning of the modified object 316 | code is in no case prevented or interfered with solely because 317 | modification has been made. 318 | 319 | If you convey an object code work under this section in, or with, or 320 | specifically for use in, a User Product, and the conveying occurs as 321 | part of a transaction in which the right of possession and use of the 322 | User Product is transferred to the recipient in perpetuity or for a 323 | fixed term (regardless of how the transaction is characterized), the 324 | Corresponding Source conveyed under this section must be accompanied 325 | by the Installation Information. But this requirement does not apply 326 | if neither you nor any third party retains the ability to install 327 | modified object code on the User Product (for example, the work has 328 | been installed in ROM). 329 | 330 | The requirement to provide Installation Information does not include a 331 | requirement to continue to provide support service, warranty, or updates 332 | for a work that has been modified or installed by the recipient, or for 333 | the User Product in which it has been modified or installed. Access to a 334 | network may be denied when the modification itself materially and 335 | adversely affects the operation of the network or violates the rules and 336 | protocols for communication across the network. 337 | 338 | Corresponding Source conveyed, and Installation Information provided, 339 | in accord with this section must be in a format that is publicly 340 | documented (and with an implementation available to the public in 341 | source code form), and must require no special password or key for 342 | unpacking, reading or copying. 343 | 344 | 7. Additional Terms. 345 | 346 | "Additional permissions" are terms that supplement the terms of this 347 | License by making exceptions from one or more of its conditions. 348 | Additional permissions that are applicable to the entire Program shall 349 | be treated as though they were included in this License, to the extent 350 | that they are valid under applicable law. If additional permissions 351 | apply only to part of the Program, that part may be used separately 352 | under those permissions, but the entire Program remains governed by 353 | this License without regard to the additional permissions. 354 | 355 | When you convey a copy of a covered work, you may at your option 356 | remove any additional permissions from that copy, or from any part of 357 | it. (Additional permissions may be written to require their own 358 | removal in certain cases when you modify the work.) You may place 359 | additional permissions on material, added by you to a covered work, 360 | for which you have or can give appropriate copyright permission. 361 | 362 | Notwithstanding any other provision of this License, for material you 363 | add to a covered work, you may (if authorized by the copyright holders of 364 | that material) supplement the terms of this License with terms: 365 | 366 | a) Disclaiming warranty or limiting liability differently from the 367 | terms of sections 15 and 16 of this License; or 368 | 369 | b) Requiring preservation of specified reasonable legal notices or 370 | author attributions in that material or in the Appropriate Legal 371 | Notices displayed by works containing it; or 372 | 373 | c) Prohibiting misrepresentation of the origin of that material, or 374 | requiring that modified versions of such material be marked in 375 | reasonable ways as different from the original version; or 376 | 377 | d) Limiting the use for publicity purposes of names of licensors or 378 | authors of the material; or 379 | 380 | e) Declining to grant rights under trademark law for use of some 381 | trade names, trademarks, or service marks; or 382 | 383 | f) Requiring indemnification of licensors and authors of that 384 | material by anyone who conveys the material (or modified versions of 385 | it) with contractual assumptions of liability to the recipient, for 386 | any liability that these contractual assumptions directly impose on 387 | those licensors and authors. 388 | 389 | All other non-permissive additional terms are considered "further 390 | restrictions" within the meaning of section 10. If the Program as you 391 | received it, or any part of it, contains a notice stating that it is 392 | governed by this License along with a term that is a further restriction, 393 | you may remove that term. If a license document contains a further 394 | restriction but permits relicensing or conveying under this License, you 395 | may add to a covered work material governed by the terms of that license 396 | document, provided that the further restriction does not survive such 397 | relicensing or conveying. 398 | 399 | If you add terms to a covered work in accord with this section, you 400 | must place, in the relevant source files, a statement of the additional 401 | terms that apply to those files, or a notice indicating where to find 402 | the applicable terms. 403 | 404 | Additional terms, permissive or non-permissive, may be stated in the 405 | form of a separately written license, or stated as exceptions; the above 406 | requirements apply either way. 407 | 408 | 8. Termination. 409 | 410 | You may not propagate or modify a covered work except as expressly 411 | provided under this License. Any attempt otherwise to propagate or 412 | modify it is void, and will automatically terminate your rights under 413 | this License (including any patent licenses granted under the third 414 | paragraph of section 11). 415 | 416 | However, if you cease all violation of this License, then your 417 | license from a particular copyright holder is reinstated (a) 418 | provisionally, unless and until the copyright holder explicitly and 419 | finally terminates your license, and (b) permanently, if the copyright 420 | holder fails to notify you of the violation by some reasonable means 421 | prior to 60 days after the cessation. 422 | 423 | Moreover, your license from a particular copyright holder is 424 | reinstated permanently if the copyright holder notifies you of the 425 | violation by some reasonable means, this is the first time you have 426 | received notice of violation of this License (for any work) from that 427 | copyright holder, and you cure the violation prior to 30 days after 428 | your receipt of the notice. 429 | 430 | Termination of your rights under this section does not terminate the 431 | licenses of parties who have received copies or rights from you under 432 | this License. If your rights have been terminated and not permanently 433 | reinstated, you do not qualify to receive new licenses for the same 434 | material under section 10. 435 | 436 | 9. Acceptance Not Required for Having Copies. 437 | 438 | You are not required to accept this License in order to receive or 439 | run a copy of the Program. Ancillary propagation of a covered work 440 | occurring solely as a consequence of using peer-to-peer transmission 441 | to receive a copy likewise does not require acceptance. However, 442 | nothing other than this License grants you permission to propagate or 443 | modify any covered work. These actions infringe copyright if you do 444 | not accept this License. Therefore, by modifying or propagating a 445 | covered work, you indicate your acceptance of this License to do so. 446 | 447 | 10. Automatic Licensing of Downstream Recipients. 448 | 449 | Each time you convey a covered work, the recipient automatically 450 | receives a license from the original licensors, to run, modify and 451 | propagate that work, subject to this License. You are not responsible 452 | for enforcing compliance by third parties with this License. 453 | 454 | An "entity transaction" is a transaction transferring control of an 455 | organization, or substantially all assets of one, or subdividing an 456 | organization, or merging organizations. If propagation of a covered 457 | work results from an entity transaction, each party to that transaction 458 | who receives a copy of the work also receives whatever licenses to the 459 | work the party's predecessor in interest had or could give under the 460 | previous paragraph, plus a right to possession of the Corresponding 461 | Source of the work from the predecessor in interest, if the predecessor 462 | has it or can get it with reasonable efforts. 463 | 464 | You may not impose any further restrictions on the exercise of the 465 | rights granted or affirmed under this License. For example, you may 466 | not impose a license fee, royalty, or other charge for exercise of 467 | rights granted under this License, and you may not initiate litigation 468 | (including a cross-claim or counterclaim in a lawsuit) alleging that 469 | any patent claim is infringed by making, using, selling, offering for 470 | sale, or importing the Program or any portion of it. 471 | 472 | 11. Patents. 473 | 474 | A "contributor" is a copyright holder who authorizes use under this 475 | License of the Program or a work on which the Program is based. The 476 | work thus licensed is called the contributor's "contributor version". 477 | 478 | A contributor's "essential patent claims" are all patent claims 479 | owned or controlled by the contributor, whether already acquired or 480 | hereafter acquired, that would be infringed by some manner, permitted 481 | by this License, of making, using, or selling its contributor version, 482 | but do not include claims that would be infringed only as a consequence 483 | of further modification of the contributor version. For purposes of 484 | this definition, "control" includes the right to grant patent sublicenses 485 | in a manner consistent with the requirements of this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is conditioned 523 | on the non-exercise of one or more of the rights that are specifically 524 | granted under this License. You may not convey a covered work if you 525 | are a party to an arrangement with a third party that is in the business 526 | of distributing software, under which you make payment to the third 527 | party based on the extent of your activity of conveying the work, and 528 | under which the third party grants, to any of the parties who would 529 | receive the covered work from you, a discriminatory patent license 530 | (a) in connection with copies of the covered work conveyed by you 531 | (or copies made from those copies), or (b) primarily for and in 532 | connection with specific products or compilations that contain the 533 | covered work, unless you entered into that arrangement, or that patent 534 | license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may otherwise 538 | be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the Program 571 | specifies that a certain numbered version of the GNU General Public License 572 | "or any later version" applies to it, you have the option of following the 573 | terms and conditions either of that numbered version or of any later 574 | version published by the Free Software Foundation. If the Program does not 575 | specify a version number of the GNU General Public License, you may choose 576 | any version ever published by the Free Software Foundation. 577 | 578 | If the Program specifies that a proxy can decide which future versions of 579 | the GNU General Public License can be used, that proxy's public statement 580 | of acceptance of a version permanently authorizes you to choose that version 581 | for the Program. 582 | 583 | Later license versions may give you additional or different permissions. 584 | However, no additional obligations are imposed on any author or copyright 585 | holder as a result of your choosing to follow a later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU General Public License for more details. 644 | 645 | You should have received a copy of the GNU General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If the program does terminal interaction, make it output a short 651 | notice like this when it starts in an interactive mode: 652 | 653 | Copyright (C) 654 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 655 | This is free software, and you are welcome to redistribute it 656 | under certain conditions; type `show c' for details. 657 | 658 | The hypothetical commands `show w' and `show c' should show the appropriate 659 | parts of the General Public License. Of course, your program's commands 660 | might be different; for a GUI interface, you would use an "about box". 661 | 662 | You should also get your employer (if you work as a programmer) or school, 663 | if any, to sign a "copyright disclaimer" for the program, if necessary. 664 | For more information on this, and how to apply and follow the GNU GPL, see 665 | . 666 | 667 | The GNU General Public License does not permit incorporating your program 668 | into proprietary programs. If your program is a subroutine library, you may 669 | consider it more useful to permit linking proprietary applications with the 670 | library. If this is what you want to do, use the GNU Lesser General 671 | Public License instead of this License. But first, please read 672 | . 673 | --------------------------------------------------------------------------------