diff --git a/.eslintrc b/.eslintrc
index 23842686c..3d30bfcdf 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -47,6 +47,8 @@
"no-shadow": "off",
"@typescript-eslint/no-shadow": ["error"],
// Allow usage of Gutenberg experimental components.
- "@wordpress/no-unsafe-wp-apis": "off"
+ "@wordpress/no-unsafe-wp-apis": "off",
+ // Allow link tag in JSDocs.
+ "jsdoc/check-tag-names": ["error", { "definedTags": ["link"] }]
}
}
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index b4060de72..48b620f92 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -6,7 +6,7 @@ updates:
directory: '/'
schedule:
interval: 'daily'
- open-pull-requests-limit: 15
+ open-pull-requests-limit: 50
- package-ecosystem: 'composer'
reviewers:
diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml
index 8b0bfe79b..d12a2b370 100644
--- a/.github/workflows/node.js.yml
+++ b/.github/workflows/node.js.yml
@@ -21,17 +21,16 @@ jobs:
build:
runs-on: ubuntu-20.04
- strategy:
- matrix:
- node-version: [16.x]
-
steps:
- uses: actions/checkout@v4
- - name: Use Node.js ${{ matrix.node-version }}
+ - name: Read .nvmrc
+ run: echo "NODE_VERSION=$(cat .nvmrc)" >> $GITHUB_ENV
+
+ - name: Use Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v4.0.2
with:
- node-version: ${{ matrix.node-version }}
+ node-version: ${{ env.NODE_VERSION }}
cache: npm
- name: Install JavaScript dependencies
diff --git a/.nvmrc b/.nvmrc
index 3c032078a..29ef05d4d 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-18
+18.12
diff --git a/CHANGELOG.md b/CHANGELOG.md
index fbabd3a59..e2f87fb01 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [3.16.0](https://github.com/Parsely/wp-parsely/compare/3.15.0...3.16.0) - 2024-07-08
+
+### Added
+
+- Content Helper: Implement AI feature permissions ([#2604](https://github.com/Parsely/wp-parsely/pull/2604))
+- Settings page: Add "Content Helper" tab ([#2554](https://github.com/Parsely/wp-parsely/pull/2554))
+- PCH Smart Linking: Add Inbound Smart Links support ([#2553](https://github.com/Parsely/wp-parsely/pull/2553))
+- PCH Smart Linking: Add Smart Linking back-end ([#2544](https://github.com/Parsely/wp-parsely/pull/2544))
+- PCH Smart Linking: Implement Smart Links Reviewing step ([#2507](https://github.com/Parsely/wp-parsely/pull/2507))
+
+### Changed
+
+- PCH: Initialize Content Helper features on 'rest_api_init' ([#2606](https://github.com/Parsely/wp-parsely/pull/2606))
+- PCH Sidebar: Move initialization to admin_init ([#2578](https://github.com/Parsely/wp-parsely/pull/2578))
+- PCH Excerpt Suggestions: Allow editing when reviewing ([#2506](https://github.com/Parsely/wp-parsely/pull/2506))
+
+### Removed
+
+- PCH Smart Linking: Remove Link Length option from the UI ([#2508](https://github.com/Parsely/wp-parsely/pull/2508))
+
+### Fixed
+
+- Fix some untranslatable strings ([#2612](https://github.com/Parsely/wp-parsely/pull/2612))
+- PCH: Fix potential fatal with 'enqueue_settings_assets' strict parameter ([#2610](https://github.com/Parsely/wp-parsely/pull/2610))
+- PCH Excerpt Suggestions: Improve error handling ([#2609](https://github.com/Parsely/wp-parsely/pull/2609))
+- Content Helper: Fix non-dismissible Notices ([#2600](https://github.com/Parsely/wp-parsely/pull/2600))
+- ContentHelperError: Avoid double prefix in error messages ([#2580](https://github.com/Parsely/wp-parsely/pull/2580))
+- PCH Sidebar: Show detailed error messages in Snackbar Notices ([#2579](https://github.com/Parsely/wp-parsely/pull/2579))
+- PCH Smart Linking: Prevent duplicate Smart Links ([#2571](https://github.com/Parsely/wp-parsely/pull/2571))
+- PCH Smart Linking: Prevent Smart Linking from running on Freeform (Classic) blocks ([#2570](https://github.com/Parsely/wp-parsely/pull/2570))
+- PCH Smart Linking: Fix issue when processing a single block ([#2547](https://github.com/Parsely/wp-parsely/pull/2547))
+
+### Dependency Updates
+
+- The list of all dependency updates for this release is available [here](https://github.com/Parsely/wp-parsely/pulls?q=is%3Apr+is%3Amerged+milestone%3A3.16.0+label%3A%22Component%3A+Dependencies%22).
+
## [3.15.0](https://github.com/Parsely/wp-parsely/compare/3.14.5...3.15.0) - 2024-05-14
### Changed
diff --git a/README.md b/README.md
index 452f8f793..217ddfa77 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Parse.ly
-Stable tag: 3.15.0
+Stable tag: 3.16.0
Requires at least: 5.2
Tested up to: 6.5
Requires PHP: 7.2
diff --git a/build/admin-settings-rtl.css b/build/admin-settings-rtl.css
new file mode 100644
index 000000000..591479bcd
--- /dev/null
+++ b/build/admin-settings-rtl.css
@@ -0,0 +1 @@
+#wp-parsely-dashboard-widget,.components-button[aria-controls="wp-parsely-block-editor-sidebar:wp-parsely-content-helper"],.editor-post-excerpt,.settings_page_parsely,.wp-parsely-block-overlay,.wp-parsely-content-helper,.wp-parsely-excerpt-generator,.wp-parsely-panel,.wp-parsely-popover,.wp-parsely-preview-editor,.wp-parsely-smart-linking-close-dialog,.wp-parsely-smart-linking-review-modal,.wp-parsely-suggested-title-modal{--base-font:"source-sans-pro",arial,sans-serif;--numeric-font:"ff-din-round-web",sans-serif;--parsely-green-components:107,42%,46%;--parsely-green:hsl(var(--parsely-green-components));--parsely-green-10:#c7ecb1;--parsely-green-65:#2a691b;--gray-200:#f7f8f9;--gray-300:#edeeef;--gray-400:#d7dbdf;--gray-500:#959da5;--gray-600:#586069;--gray-700:#444d56;--gray-900:#24292e;--blue-500:#44a8e5;--blue-550:#2596db;--green-500:#7bc01b;--green-900:#3d6307;--ref-direct:205,13%,52%;--ref-internal:161,91%,41%;--ref-social:210,72%,41%;--ref-search:42,100%,50%;--ref-other:3,76%,58%;--base-text:var(--gray-900);--base-text-2:var(--gray-600);--base-3:var(--gray-400);--border:var(--gray-400);--data:var(--green-500);--control:var(--blue-500);--grid-unit-5:0.25rem;--grid-unit-10:0.5rem;--grid-unit-15:0.75rem;--grid-unit-20:1rem;--grid-unit-25:1.25rem;--grid-unit-30:1.5rem;--grid-unit-40:2rem;--font-size--smaller:0.688rem;--font-size--small:0.75rem;--font-size--medium:0.875rem;--font-size--large:1rem;--font-size--extra-large:1.2rem;--black:#000;--sidebar-black:#1e1e1e;--sidebar-white:#f0f0f0}.settings_page_parsely{--padding-default:15px}.settings_page_parsely fieldset.user-role-permissions label{margin-left:1rem!important}.settings_page_parsely .disabled-before-posting label:not(.prevent-disable),.settings_page_parsely .disabled-before-posting p,.settings_page_parsely .disabled-before-posting th,.settings_page_parsely fieldset:disabled label:not(.prevent-disable),.settings_page_parsely fieldset:disabled p,.settings_page_parsely fieldset:disabled th,.settings_page_parsely tr:has(fieldset:disabled) label:not(.prevent-disable),.settings_page_parsely tr:has(fieldset:disabled) p,.settings_page_parsely tr:has(fieldset:disabled) th{color:var(--gray-500);cursor:default}.settings_page_parsely #track-post-types{max-width:550px;width:100%}.settings_page_parsely #track-post-types td,.settings_page_parsely #track-post-types th{display:table-cell;padding:var(--padding-default);text-align:center;vertical-align:middle}.settings_page_parsely #track-post-types td{padding:0}.settings_page_parsely #track-post-types td label{display:inline-block;margin:0!important;padding-bottom:var(--padding-default);padding-top:var(--padding-default);width:100%}.settings_page_parsely #track-post-types thead th:nth-child(3){word-break:break-word}.settings_page_parsely .managed-option-badge{border:.0625rem solid var(--green-500);border-radius:.25rem;color:var(--green-500)!important;display:inline-block;font-size:.625rem;padding:.125rem .25rem;text-decoration:none}.settings_page_parsely a.managed-option-badge{cursor:pointer!important}.settings_page_parsely a.managed-option-badge:hover{background-color:var(--green-500);color:#fff!important}@media only screen and (max-width:380px){.settings_page_parsely #track-post-types td,.settings_page_parsely #track-post-types th{padding-right:10px;padding-left:10px}.settings_page_parsely #track-post-types th:first-child{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
diff --git a/build/admin-settings.asset.php b/build/admin-settings.asset.php
index 6e9a92c3e..a0dab1c53 100644
--- a/build/admin-settings.asset.php
+++ b/build/admin-settings.asset.php
@@ -1 +1 @@
- array(), 'version' => '54de3040eda45ac58dbf');
+ array(), 'version' => '3f06d1e94aa796c8627c');
diff --git a/build/admin-settings.css b/build/admin-settings.css
index 2ce0634eb..6fa3b3f90 100644
--- a/build/admin-settings.css
+++ b/build/admin-settings.css
@@ -1 +1 @@
-#wp-parsely-dashboard-widget,.components-button[aria-controls="wp-parsely-block-editor-sidebar:wp-parsely-content-helper"],.editor-post-excerpt,.settings_page_parsely,.wp-parsely-block-overlay,.wp-parsely-content-helper,.wp-parsely-excerpt-generator,.wp-parsely-panel,.wp-parsely-popover,.wp-parsely-suggested-title-modal{--base-font:"source-sans-pro",arial,sans-serif;--numeric-font:"ff-din-round-web",sans-serif;--parsely-green:#59a744;--parsely-green-10:#c7ecb1;--parsely-green-65:#2a691b;--gray-200:#f7f8f9;--gray-300:#edeeef;--gray-400:#d7dbdf;--gray-500:#959da5;--gray-600:#586069;--gray-700:#444d56;--gray-900:#24292e;--blue-500:#44a8e5;--blue-550:#2596db;--green-500:#7bc01b;--green-900:#3d6307;--ref-direct:205,13%,52%;--ref-internal:161,91%,41%;--ref-social:210,72%,41%;--ref-search:42,100%,50%;--ref-other:3,76%,58%;--base-text:var(--gray-900);--base-text-2:var(--gray-600);--base-3:var(--gray-400);--border:var(--gray-400);--data:var(--green-500);--control:var(--blue-500);--grid-unit-5:0.25rem;--grid-unit-10:0.5rem;--grid-unit-15:0.75rem;--grid-unit-20:1rem;--font-size--smaller:0.688rem;--font-size--small:0.75rem;--font-size--medium:0.875rem;--font-size--large:1rem;--font-size--extra-large:1.2rem;--black:#000;--sidebar-black:#1e1e1e;--sidebar-white:#f0f0f0}.settings_page_parsely{--padding-default:15px}.settings_page_parsely #track-post-types{max-width:550px;width:100%}.settings_page_parsely #track-post-types td,.settings_page_parsely #track-post-types th{display:table-cell;padding:var(--padding-default);text-align:center;vertical-align:middle}.settings_page_parsely #track-post-types td{padding:0}.settings_page_parsely #track-post-types td label{display:inline-block;margin:0!important;padding-bottom:var(--padding-default);padding-top:var(--padding-default);width:100%}.settings_page_parsely #track-post-types thead th:nth-child(3){word-break:break-word}.settings_page_parsely fieldset:disabled *,.settings_page_parsely tr:has(fieldset:disabled) *{color:var(--gray-500);cursor:default}.settings_page_parsely .managed-option-badge{border:.0625rem solid var(--green-500);border-radius:.25rem;color:var(--green-500)!important;display:inline-block;font-size:.625rem;padding:.125rem .25rem;text-decoration:none}.settings_page_parsely a.managed-option-badge{cursor:pointer!important}.settings_page_parsely a.managed-option-badge:hover{background-color:var(--green-500);color:#fff!important}@media only screen and (max-width:380px){.settings_page_parsely #track-post-types td,.settings_page_parsely #track-post-types th{padding-left:10px;padding-right:10px}.settings_page_parsely #track-post-types th:first-child{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
+#wp-parsely-dashboard-widget,.components-button[aria-controls="wp-parsely-block-editor-sidebar:wp-parsely-content-helper"],.editor-post-excerpt,.settings_page_parsely,.wp-parsely-block-overlay,.wp-parsely-content-helper,.wp-parsely-excerpt-generator,.wp-parsely-panel,.wp-parsely-popover,.wp-parsely-preview-editor,.wp-parsely-smart-linking-close-dialog,.wp-parsely-smart-linking-review-modal,.wp-parsely-suggested-title-modal{--base-font:"source-sans-pro",arial,sans-serif;--numeric-font:"ff-din-round-web",sans-serif;--parsely-green-components:107,42%,46%;--parsely-green:hsl(var(--parsely-green-components));--parsely-green-10:#c7ecb1;--parsely-green-65:#2a691b;--gray-200:#f7f8f9;--gray-300:#edeeef;--gray-400:#d7dbdf;--gray-500:#959da5;--gray-600:#586069;--gray-700:#444d56;--gray-900:#24292e;--blue-500:#44a8e5;--blue-550:#2596db;--green-500:#7bc01b;--green-900:#3d6307;--ref-direct:205,13%,52%;--ref-internal:161,91%,41%;--ref-social:210,72%,41%;--ref-search:42,100%,50%;--ref-other:3,76%,58%;--base-text:var(--gray-900);--base-text-2:var(--gray-600);--base-3:var(--gray-400);--border:var(--gray-400);--data:var(--green-500);--control:var(--blue-500);--grid-unit-5:0.25rem;--grid-unit-10:0.5rem;--grid-unit-15:0.75rem;--grid-unit-20:1rem;--grid-unit-25:1.25rem;--grid-unit-30:1.5rem;--grid-unit-40:2rem;--font-size--smaller:0.688rem;--font-size--small:0.75rem;--font-size--medium:0.875rem;--font-size--large:1rem;--font-size--extra-large:1.2rem;--black:#000;--sidebar-black:#1e1e1e;--sidebar-white:#f0f0f0}.settings_page_parsely{--padding-default:15px}.settings_page_parsely fieldset.user-role-permissions label{margin-right:1rem!important}.settings_page_parsely .disabled-before-posting label:not(.prevent-disable),.settings_page_parsely .disabled-before-posting p,.settings_page_parsely .disabled-before-posting th,.settings_page_parsely fieldset:disabled label:not(.prevent-disable),.settings_page_parsely fieldset:disabled p,.settings_page_parsely fieldset:disabled th,.settings_page_parsely tr:has(fieldset:disabled) label:not(.prevent-disable),.settings_page_parsely tr:has(fieldset:disabled) p,.settings_page_parsely tr:has(fieldset:disabled) th{color:var(--gray-500);cursor:default}.settings_page_parsely #track-post-types{max-width:550px;width:100%}.settings_page_parsely #track-post-types td,.settings_page_parsely #track-post-types th{display:table-cell;padding:var(--padding-default);text-align:center;vertical-align:middle}.settings_page_parsely #track-post-types td{padding:0}.settings_page_parsely #track-post-types td label{display:inline-block;margin:0!important;padding-bottom:var(--padding-default);padding-top:var(--padding-default);width:100%}.settings_page_parsely #track-post-types thead th:nth-child(3){word-break:break-word}.settings_page_parsely .managed-option-badge{border:.0625rem solid var(--green-500);border-radius:.25rem;color:var(--green-500)!important;display:inline-block;font-size:.625rem;padding:.125rem .25rem;text-decoration:none}.settings_page_parsely a.managed-option-badge{cursor:pointer!important}.settings_page_parsely a.managed-option-badge:hover{background-color:var(--green-500);color:#fff!important}@media only screen and (max-width:380px){.settings_page_parsely #track-post-types td,.settings_page_parsely #track-post-types th{padding-left:10px;padding-right:10px}.settings_page_parsely #track-post-types th:first-child{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}}
diff --git a/build/admin-settings.js b/build/admin-settings.js
index 41b96d5b8..9922fcdbe 100644
--- a/build/admin-settings.js
+++ b/build/admin-settings.js
@@ -1 +1 @@
-!function(){"use strict";!function(){function t(){var t,e,n=""!==location.hash?location.hash.substring(1):"basic-section";null===(t=document.querySelectorAll(".nav-tab"))||void 0===t||t.forEach((function(t){t.classList.contains(n+"-tab")?t.classList.add("nav-tab-active"):t.classList.remove("nav-tab-active")})),null===(e=document.querySelectorAll(".tab-content"))||void 0===e||e.forEach((function(t){t.classList.contains(n)?t.setAttribute("style","display: initial"):t.setAttribute("style","display: none")}));var i=document.querySelector('form[name="parsely"]');i&&(i.removeAttribute("hidden"),i.setAttribute("action","options.php#".concat(n)))}function e(t){var e=t.target.dataset.option,n=window.wp.media({multiple:!1,library:{type:"image"}});n.on("select",(function(){var t=n.state().get("selection").first().toJSON().url,i="#media-single-image-"+e+" input.file-path",a=document.querySelector(i);a&&(a.value=t)})),n.open()}document.addEventListener("DOMContentLoaded",(function(){var n;t(),window.addEventListener("hashchange",t),null===(n=document.querySelector(".media-single-image button.browse"))||void 0===n||n.addEventListener("click",e)}))}()}();
\ No newline at end of file
+!function(){"use strict";!function(){function e(){var e,t,n=""!==location.hash?location.hash.substring(1):"basic-section";null===(e=document.querySelectorAll(".nav-tab"))||void 0===e||e.forEach((function(e){e.classList.contains(n+"-tab")?e.classList.add("nav-tab-active"):e.classList.remove("nav-tab-active")})),null===(t=document.querySelectorAll(".tab-content"))||void 0===t||t.forEach((function(e){e.classList.contains(n)?e.setAttribute("style","display: initial"):e.setAttribute("style","display: none")}));var i=document.querySelector('form[name="parsely"]');i&&(i.removeAttribute("hidden"),i.setAttribute("action","options.php#".concat(n)))}function t(e){var t=e.target.dataset.option,n=window.wp.media({multiple:!1,library:{type:"image"}});n.on("select",(function(){var e=n.state().get("selection").first().toJSON().url,i="#media-single-image-"+t+" input.file-path",o=document.querySelector(i);o&&(o.value=e)})),n.open()}document.addEventListener("DOMContentLoaded",(function(){var n;!function(){var e=document.querySelector("input#content_helper_ai_features_enabled"),t=document.querySelectorAll("input#content_helper_smart_linking_enabled, input#content_helper_title_suggestions_enabled, input#content_helper_excerpt_suggestions_enabled"),n=document.querySelectorAll("div.content-helper-section fieldset");function i(){e&&(e.checked?n.forEach((function(e){c(e,!1),t.forEach((function(e){o(e)}))})):(n.forEach((function(t){t.querySelector("#".concat(e.id))||c(t)})),document.querySelectorAll("label.prevent-disable").forEach((function(e){a(e,!1)}))))}function o(e){var t,n,i=null===(n=null===(t=e.closest("fieldset"))||void 0===t?void 0:t.nextSibling)||void 0===n?void 0:n.nextSibling;e.checked?c([e,i],!1):(c(i),a(e.parentElement))}function a(e,t){void 0===t&&(t=!0),t?e.classList.add("prevent-disable"):e.classList.remove("prevent-disable")}function c(e,t){void 0===t&&(t=!0),Array.isArray(e)||(e=[e]),e.forEach((function(e){t?e.setAttribute("disabled","disabled"):e.removeAttribute("disabled")}))}(function(){var e;null===(e=document.querySelector('.wp-admin form[name="parsely"]'))||void 0===e||e.addEventListener("submit",(function(){var e=".wp-admin .content-helper-section fieldset";document.querySelectorAll("".concat(e,"[disabled]")).forEach((function(t){var n,i;null===(i=null===(n=t.parentElement)||void 0===n?void 0:n.parentElement)||void 0===i||i.classList.add("disabled-before-posting"),t.querySelectorAll("".concat(e,' label input[type="checkbox"]')).forEach((function(e){e.classList.add("disabled")})),t.removeAttribute("disabled")}))}))})(),i(),null==e||e.addEventListener("change",(function(){i()})),t.forEach((function(e){e.addEventListener("change",(function(){o(e)}))}))}(),e(),window.addEventListener("hashchange",e),null===(n=document.querySelector(".media-single-image button.browse"))||void 0===n||n.addEventListener("click",t)}))}()}();
\ No newline at end of file
diff --git a/build/blocks/recommendations/edit-rtl.css b/build/blocks/recommendations/edit-rtl.css
new file mode 100644
index 000000000..350d40038
--- /dev/null
+++ b/build/blocks/recommendations/edit-rtl.css
@@ -0,0 +1 @@
+.parsely-recommendations-card>div{height:unset}.parsely-recommendations-link{pointer-events:none}
diff --git a/build/blocks/recommendations/edit.asset.php b/build/blocks/recommendations/edit.asset.php
index 3320beb03..0f36c73fc 100644
--- a/build/blocks/recommendations/edit.asset.php
+++ b/build/blocks/recommendations/edit.asset.php
@@ -1 +1 @@
- array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '8daeb223e80bc2f56720');
+ array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '393b4c8be66f3bde527e');
diff --git a/build/blocks/recommendations/edit.js b/build/blocks/recommendations/edit.js
index 5cf434a2c..06593e46c 100644
--- a/build/blocks/recommendations/edit.js
+++ b/build/blocks/recommendations/edit.js
@@ -1 +1 @@
-!function(){"use strict";var e,n={271:function(e,n,r){var t,o,a=r(848),i=window.wp.blockEditor,l=window.wp.blocks,s=window.wp.i18n,c=window.wp.components,u=JSON.parse('{"UU":"wp-parsely/recommendations","uK":{"imagestyle":{"type":"string","default":"original"},"limit":{"type":"number","default":3},"openlinksinnewtab":{"type":"boolean","default":false},"showimages":{"type":"boolean","default":true},"sort":{"type":"string","default":"score"},"title":{"type":"string","default":"Related Content"}}}'),d=window.wp.element;(o=t||(t={}))[o.Error=0]="Error",o[o.Loaded=1]="Loaded",o[o.Recommendations=2]="Recommendations";var p=function(){return p=Object.assign||function(e){for(var n,r=1,t=arguments.length;r ".concat(this.message," '.concat((0,o.__)("Hint:","wp-parsely")," ").concat(e," ".concat(this.message," '.concat((0,o.__)("Hint:","wp-parsely")," ").concat(e," ".concat(this.message," '.concat((0,c.__)("Hint:","wp-parsely")," ").concat(e," ".concat(this.message," '.concat((0,p.__)("Hint:","wp-parsely")," ").concat(e,"
".concat(this.message,"
").concat(this.hint?this.hint:"")}))},r.prototype.Hint=function(e){return''.concat((0,u.__)("Hint:","wp-parsely")," ").concat(e,"
")},r}(Error),A=function(){function e(){}return e.prototype.generateExcerpt=function(e,r){return n=this,a=void 0,o=function(){var n,a,s;return function(e,t){var r,n,a,s,o={label:0,sent:function(){if(1&a[0])throw a[1];return a[1]},trys:[],ops:[]};return s={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(s[Symbol.iterator]=function(){return this}),s;function i(i){return function(l){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;s&&(s=0,i[0]&&(o=0)),o;)try{if(r=1,n&&(a=2&i[0]?n.return:i[0]?n.throw||((a=n.return)&&a.call(n),0):n.next)&&!(a=a.call(n,i[1])).done)return a;switch(n=0,a&&(i=[2&i[0],a.value]),i[0]){case 0:case 1:a=i;break;case 4:return o.label++,{value:i[1],done:!1};case 5:o.label++,n=i[1],i=[0];continue;case 7:i=o.ops.pop(),o.trys.pop();continue;default:if(!((a=(a=o.trys).length>0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0&&a[a.length-1])||6!==i[0]&&2!==i[0])){o=0;continue}if(3===i[0]&&(!a||i[1]>a[0]&&i[1]0,R=(0,d.count)(s||k,"words",{}),C=(0,u.sprintf)( +!function(){"use strict";var e={20:function(e,t,r){var n=r(609),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),s=Object.prototype.hasOwnProperty,i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,r){var n,a={},c=null,u=null;for(n in void 0!==r&&(c=""+r),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)s.call(t,n)&&!l.hasOwnProperty(n)&&(a[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===a[n]&&(a[n]=t[n]);return{$$typeof:o,type:e,key:c,ref:u,props:a,_owner:i.current}}t.Fragment=a,t.jsx=c,t.jsxs=c},848:function(e,t,r){e.exports=r(20)},609:function(e){e.exports=window.React}},t={};function r(n){var o=t[n];if(void 0!==o)return o.exports;var a=t[n]={exports:{}};return e[n](a,a.exports,r),a.exports}r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,{a:t}),t},r.d=function(e,t){for(var n in t)r.o(t,n)&&!r.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},function(){var e,t,n=window.wp.data,o=window.wp.hooks,a=window.wp.plugins,s=((0,n.dispatch)("core/block-editor"),(0,n.dispatch)("core/editor"),(0,n.dispatch)("core/edit-post")),i=r(848),l=window.wp.components,c=window.wp.editPost,u=window.wp.editor,p=window.wp.element,d=window.wp.i18n,y=window.wp.wordcount,h=window.wp.primitives,f=(0,i.jsx)(h.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,i.jsx)(h.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})}),w=function(){function e(){this._tkq=[],this.isLoaded=!1,this.isEnabled=!1,"undefined"!=typeof wpParselyTracksTelemetry&&(this.isEnabled=!0,this.loadTrackingLibrary())}return e.getInstance=function(){return window.wpParselyTelemetryInstance||Object.defineProperty(window,"wpParselyTelemetryInstance",{value:new e,writable:!1,configurable:!1,enumerable:!1}),window.wpParselyTelemetryInstance},e.prototype.loadTrackingLibrary=function(){var e=this,t=document.createElement("script");t.async=!0,t.src="//stats.wp.com/w.js",t.onload=function(){e.isLoaded=!0,e._tkq=window._tkq||[]},document.head.appendChild(t)},e.trackEvent=function(t){return r=this,n=arguments,a=function(t,r){var n;return void 0===r&&(r={}),function(e,t){var r,n,o,a,s={label:0,sent:function(){if(1&o[0])throw o[1];return o[1]},trys:[],ops:[]};return a={next:i(0),throw:i(1),return:i(2)},"function"==typeof Symbol&&(a[Symbol.iterator]=function(){return this}),a;function i(i){return function(l){return function(i){if(r)throw new TypeError("Generator is already executing.");for(;a&&(a=0,i[0]&&(s=0)),s;)try{if(r=1,n&&(o=2&i[0]?n.return:i[0]?n.throw||((o=n.return)&&o.call(n),0):n.next)&&!(o=o.call(n,i[1])).done)return o;switch(n=0,o&&(i=[2&i[0],o.value]),i[0]){case 0:case 1:o=i;break;case 4:return s.label++,{value:i[1],done:!1};case 5:s.label++,n=i[1],i=[0];continue;case 7:i=s.ops.pop(),s.trys.pop();continue;default:if(!((o=(o=s.trys).length>0&&o[o.length-1])||6!==i[0]&&2!==i[0])){s=0;continue}if(3===i[0]&&(!o||i[1]>o[0]&&i[1]".concat(this.message,"
").concat(this.hint?this.hint:"")}))},r.prototype.Hint=function(e){return''.concat((0,d.__)("Hint:","wp-parsely")," ").concat(e,"
")},r.prototype.createErrorSnackbar=function(){/' . __( 'Unable to fetch paragraph. DOMDocument is not available.', 'wp-parsely' ) . '
', + 'is_first_paragraph' => true, + 'is_last_paragraph' => true, + ); + } + + libxml_use_internal_errors( true ); + + $dom = new DOMDocument(); + $dom->loadHTML( mb_convert_encoding( $content, 'HTML-ENTITIES', 'UTF-8' ), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD ); + + $errors = libxml_get_errors(); + if ( count( $errors ) > 0 ) { + libxml_clear_errors(); + return array( + 'paragraph' => + '' . __( 'Unable to fetch paragraph. Error loading HTML.', 'wp-parsely' ) . '
', + 'is_first_paragraph' => true, + 'is_last_paragraph' => true, + ); + } + + // Fetch all paragraph tags. + $paragraphs = $dom->getElementsByTagName( 'p' ); + + $is_first_paragraph = true; + $is_last_paragraph = false; + + foreach ( $paragraphs as $p ) { + // Check each anchor tag within the paragraph. + $anchors = $p->getElementsByTagName( 'a' ); + foreach ( $anchors as $anchor ) { + // Check if the data-smartlink attribute contains the UID. + if ( $anchor->hasAttribute( 'data-smartlink' ) && stripos( $anchor->getAttribute( 'data-smartlink' ), $this->uid ) !== false ) { + // Save the outer HTML of the paragraph. + $is_first_paragraph = $p === $paragraphs->item( 0 ); + $is_last_paragraph = $p === $paragraphs->item( $paragraphs->length - 1 ); + $paragraph = $dom->saveHTML( $p ); + break 2; + } + } + } + + if ( false === $paragraph ) { + $paragraph = '' . __( 'Unable to fetch paragraph.', 'wp-parsely' ) . '
'; + } + + $this->paragraph_data = array( + 'paragraph' => $paragraph, + 'is_first_paragraph' => $is_first_paragraph, + 'is_last_paragraph' => $is_last_paragraph, + ); + + return $this->paragraph_data; + } + + /** + * Creates a new instance of an Inbound Smart Link from a Smart Link object. + * + * This is used to convert a Smart Link object to an Inbound Smart Link object. + * + * @since 3.16.0 + * + * @param Smart_Link $smart_link The Smart Link object. + * @return Inbound_Smart_Link The Inbound Smart Link object. + */ + public static function from_smart_link( Smart_Link $smart_link ): Inbound_Smart_Link { + $inbound_smart_link = new self( '', '', '', 0 ); + $reflection_class = new ReflectionClass( $smart_link ); + + foreach ( $reflection_class->getProperties() as $property ) { + // Make the property accessible. + $property->setAccessible( true ); + $value = $property->getValue( $smart_link ); + // Copy the property value. + $property->setValue( $inbound_smart_link, $value ); + } + + return $inbound_smart_link; + } +} diff --git a/src/Models/class-smart-link.php b/src/Models/class-smart-link.php new file mode 100644 index 000000000..12c736844 --- /dev/null +++ b/src/Models/class-smart-link.php @@ -0,0 +1,659 @@ +set_href( $href ); + $this->title = $title; + $this->text = $text; + $this->offset = $offset; + $this->source_post_id = $post_id; + + parent::__construct(); + } + + /** + * Gets the smart link post object by UID. + * + * @since 3.16.0 + * + * @param string $uid The UID of the smart link. + * @return int The ID of the smart link post object. + */ + private function get_smart_link_object_by_uid( string $uid ): int { + $cached = wp_cache_get( $uid . $this->source_post_id, 'wp_parsely_smart_link_id' ); + if ( is_int( $cached ) && 0 !== $cached ) { + return $cached; + } + + $smart_links = new \WP_Query( + array( + 'post_type' => 'parsely_smart_link', + 'fields' => 'ids', // Only get the post IDs to improve performance. + 'posts_per_page' => 1, + 'title' => $uid, + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + 'tax_query' => array( + array( + 'taxonomy' => 'smart_link_source', + 'include_children' => false, // Performance optimization. + 'field' => 'name', + 'terms' => (string) $this->source_post_id, + ), + ), + ) + ); + + if ( $smart_links->have_posts() && is_int( $smart_links->posts[0] ) ) { + wp_cache_set( + $uid . $this->source_post_id, + $smart_links->posts[0], + 'wp_parsely_smart_link_id' + ); + return $smart_links->posts[0]; + } + + return 0; + } + + /** + * Loads the smart link post object. + * + * @since 3.16.0 + * + * @return bool True if the smart link was loaded successfully, false otherwise. + */ + private function load(): bool { + if ( 0 === $this->smart_link_id ) { + // Try to get the smart link id from the UID. + $this->smart_link_id = $this->get_smart_link_object_by_uid( $this->uid ); + if ( 0 === $this->smart_link_id ) { + $this->exists = false; + return false; + } + } + + $smart_link = get_post( $this->smart_link_id ); + + if ( null === $smart_link ) { + $this->exists = false; + return false; + } + + $this->exists = true; + $this->applied = true; + + $this->uid = $smart_link->post_title; + + // Load the Smart Link properties from the post meta. + $this->title = $this->get_string_meta( '_smart_link_title' ); + $this->href = $this->get_string_meta( '_smart_link_href' ); + $this->text = $this->get_string_meta( '_smart_link_text' ); + $this->offset = $this->get_int_meta( '_smart_link_offset' ); + + // Load the source post ID. + $source_terms = wp_get_post_terms( $this->smart_link_id, 'smart_link_source' ); + if ( ! is_wp_error( $source_terms ) && count( $source_terms ) > 0 ) { + $source_term = $source_terms[0]; + $this->source_post_id = (int) $source_term->name; + } + + // Load the destination post ID. + $destination_terms = wp_get_post_terms( $this->smart_link_id, 'smart_link_destination' ); + if ( ! is_wp_error( $destination_terms ) && count( $destination_terms ) > 0 ) { + $destination_term = $destination_terms[0]; + if ( 'external' !== $destination_term->slug ) { + $this->destination_post_id = (int) $destination_term->name; + } + } + + // If the destination post ID is not set, try to get it from the URL. + if ( 0 === $this->destination_post_id ) { + $this->destination_post_id = $this->get_post_id_by_url( $this->href ); + } + + // Get the post type of the destination post. + $post_type = get_post_type( $this->destination_post_id ); + if ( false !== $post_type ) { + $post_type_object = get_post_type_object( $post_type ); + if ( null !== $post_type_object ) { + $this->destination_post_type = $post_type_object->labels->singular_name; + } + } else { + $this->destination_post_type = 'external'; + } + + return true; + } + + /** + * Saves the smart link to the post meta. + * + * @since 3.16.0 + * + * @return bool True if the smart link was saved successfully, false otherwise. + */ + public function save(): bool { + if ( 0 === $this->source_post_id ) { + return false; + } + + if ( ! $this->exists() ) { + // Create the post object. + $post_id = wp_insert_post( + array( + 'post_type' => 'parsely_smart_link', + 'post_title' => $this->uid, + 'post_status' => 'publish', + ) + ); + + if ( 0 === $post_id ) { + return false; + } + + $this->smart_link_id = $post_id; + wp_cache_set( $this->uid . $this->source_post_id, $post_id, 'wp_parsely_smart_link_id' ); + } + + // Update UID. + wp_update_post( + array( + 'ID' => $this->smart_link_id, + 'post_title' => $this->uid, + ) + ); + + // Update the smart link meta. + $meta = array( + '_smart_link_title' => $this->title, + '_smart_link_href' => $this->href, + '_smart_link_text' => $this->text, + '_smart_link_offset' => $this->offset, + ); + foreach ( $meta as $key => $value ) { + update_post_meta( $this->smart_link_id, $key, $value ); + } + + // Add the source term. + wp_set_post_terms( $this->smart_link_id, (string) $this->source_post_id, 'smart_link_source' ); + + // Add the destination term. + if ( 0 !== $this->destination_post_id ) { + wp_set_post_terms( $this->smart_link_id, (string) $this->destination_post_id, 'smart_link_destination' ); + } else { + wp_set_post_terms( $this->smart_link_id, 'external', 'smart_link_destination' ); + } + + $this->applied = true; + $this->exists = true; + + return true; + } + + /** + * Removes the smart link from the database. + * + * @since 3.16.0 + * + * @return bool True if the smart link was removed successfully, false otherwise. + */ + public function delete(): bool { + if ( 0 === $this->smart_link_id ) { + return false; + } + + // Delete the post object. + $deleted = wp_delete_post( $this->smart_link_id, true ); + + if ( false !== $deleted && null !== $deleted && is_a( $deleted, 'WP_Post' ) ) { + $this->smart_link_id = 0; + $this->exists = false; + wp_cache_delete( $this->uid . $this->source_post_id, 'wp_parsely_smart_link_id' ); + return true; + } + + return false; + } + + /** + * Checks if the smart link is saved in the database. + * + * @since 3.16.0 + * + * @return bool True if the smart link exists, false otherwise. + */ + public function exists(): bool { + if ( $this->exists ) { + return true; + } + + // Try to find a smart link with the same UID. + $smart_link_id = $this->get_smart_link_object_by_uid( $this->uid ); + + if ( 0 !== $smart_link_id ) { + $this->exists = true; + $this->smart_link_id = $smart_link_id; + return true; + } + + $this->exists = false; + $this->smart_link_id = 0; + return false; + } + + /** + * Gets a string meta value from the smart link post. + * + * @since 3.16.0 + * + * @param string $meta_key The meta key to get the value for. + * @param string $default_value The default value to return if the meta value is not a string. + * @return string The meta value. + */ + private function get_string_meta( string $meta_key, string $default_value = '' ): string { + $meta_value = get_post_meta( $this->smart_link_id, $meta_key, true ); + return is_string( $meta_value ) ? $meta_value : $default_value; + } + + /** + * Gets an integer meta value from the smart link post. + * + * @since 3.16.0 + * + * @param string $meta_key The meta key to get the value for. + * @param int $default_value The default value to return if the meta value is not an integer. + * @return int The meta value. + */ + private function get_int_meta( string $meta_key, int $default_value = 0 ): int { + $meta_value = get_post_meta( $this->smart_link_id, $meta_key, true ); + return is_int( $meta_value ) ? $meta_value : $default_value; + } + + /** + * Gets the post ID by URL. + * + * @since 3.16.0 + * + * @param string $url The URL to get the post ID for. + * @return int The post ID of the URL, 0 if not found. + */ + private function get_post_id_by_url( string $url ): int { + $cache = wp_cache_get( $url, 'wp_parsely_smart_link_url_to_postid' ); + if ( is_integer( $cache ) ) { + return $cache; + } + + if ( function_exists( 'wpcom_vip_url_to_postid' ) ) { + $post_id = wpcom_vip_url_to_postid( $url ); + } else { + // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.url_to_postid_url_to_postid + $post_id = url_to_postid( $url ); + wp_cache_set( $url, $post_id, 'wp_parsely_smart_link_url_to_postid' ); + } + + return $post_id; + } + + /** + * Sets the source post ID. + * + * @since 3.16.0 + * + * @param int $source_post_id The source post ID. + */ + public function set_source_post_id( int $source_post_id ): void { + $this->source_post_id = $source_post_id; + } + + /** + * Sets the UID of the smart link. + * + * @since 3.16.0 + * + * @param string $uid The UID of the smart link. + */ + public function set_uid( string $uid ): void { + $this->uid = $uid; + } + + /** + * Sets the href of the smart link. + * + * @since 3.16.0 + * + * @param string $href The href of the smart link. + */ + public function set_href( string $href ): void { + $this->href = $href; + $this->destination_post_id = $this->get_post_id_by_url( $href ); + + if ( 0 !== $this->destination_post_id ) { + $post_type = get_post_type( $this->destination_post_id ); + $this->destination_post_type = false !== $post_type ? $post_type : 'external'; + } + } + + /** + * Generates a unique ID for the suggested link. + * + * It takes the href, title, text, and offset properties and concatenates + * them to create a unique ID. This ID is hashed to ensure it is unique. + * + * @since 3.16.0 + * + * @return string The unique ID. + */ + protected function generate_uid(): string { + return md5( $this->href . $this->title . $this->text . $this->offset ); + } + + /** + * Serializes the model to a JSON string. + * + * @since 3.16.0 + * + * @return array