String locator - Version 2.6.0

Version Description

(2022-07-20) = * Added database search feature. * Added tools for quickly replacing data in the search results. * Added many more filters and actions. * Added hardening of file path checks. * Removed one-time donation notice. * Removed jQuery dependency in favor of vanilla JavaScript code. * Separated search class into a base class for extenders. * Fixed bug with code viewer sizes when resizing your window. * Fixed bug in the list view if special characters were in the search string. * Fixed a bug where RegEx search validation may have a false positive check for invalid patterns. * Fixed missing translator function if Javascript is missing. * Improved capability checks for displaying the search interface when editing is disabled.

Download this release

Release Info

Developer Clorith
Plugin Icon 128x128 String locator
Version 2.6.0
Comparing to
See all releases

Code changes from version 2.5.0 to 2.6.0

Files changed (43) hide show
  1. build/string-locator-replace.asset.php +1 -0
  2. build/string-locator-replace.css +1 -0
  3. build/string-locator-replace.js +1 -0
  4. build/string-locator-search.asset.php +1 -1
  5. build/string-locator-search.js +1 -1
  6. build/string-locator.asset.php +1 -1
  7. build/string-locator.css +1 -1
  8. build/string-locator.js +1 -1
  9. changelog.txt +9 -0
  10. includes/Base/class-rest.php +34 -0
  11. includes/Base/class-search.php +142 -0
  12. includes/Extension/SQL/Tests/class-serialized-data.php +139 -0
  13. includes/Extension/SQL/class-edit.php +95 -0
  14. includes/Extension/SQL/class-save.php +145 -0
  15. includes/Extension/SQL/class-search.php +367 -0
  16. includes/Extension/SQL/sql.php +16 -0
  17. includes/Extension/SQL/views/editor/sql.php +251 -0
  18. includes/Extension/SQL/views/template/search.php +41 -0
  19. includes/Extension/SearchReplace/REST/class-replace.php +182 -0
  20. includes/Extension/SearchReplace/Replace/class-file.php +122 -0
  21. includes/Extension/SearchReplace/Replace/class-sql.php +264 -0
  22. includes/Extension/SearchReplace/class-replace.php +99 -0
  23. includes/Extension/SearchReplace/search-replace.php +16 -0
  24. includes/Extension/SearchReplace/template/error-notice.php +17 -0
  25. includes/Extension/SearchReplace/views/replace-form.php +55 -0
  26. includes/REST/class-base.php +0 -17
  27. includes/REST/class-clean.php +4 -2
  28. includes/REST/class-directory-structure.php +20 -7
  29. includes/REST/class-save.php +5 -3
  30. includes/REST/class-search.php +8 -5
  31. includes/Tests/class-loopback.php +1 -1
  32. includes/Tests/class-smart-scan.php +1 -1
  33. includes/class-directory-iterator.php +2 -2
  34. includes/class-save.php +4 -4
  35. includes/class-search.php +6 -156
  36. includes/class-string-locator.php +107 -88
  37. readme.txt +20 -14
  38. string-locator.php +19 -6
  39. views/assets/instawp-logo.svg +12 -0
  40. views/{editor.php → editors/default.php} +24 -7
  41. views/search.php +49 -38
  42. views/templates/instawp.php +21 -0
  43. views/templates/search-default.php +41 -0
build/string-locator-replace.asset.php ADDED
@@ -0,0 +1 @@
 
1
+ <?php return array('dependencies' => array(), 'version' => '1c81e6d76abd42194431');
build/string-locator-replace.css ADDED
@@ -0,0 +1 @@
 
1
+ #string-locator-replace-form{background-color:#fff;border:1px solid #c3c4c7;display:none;padding:10px;width:100%}#string-locator-replace-form.visible{display:block}#string-locator-replace-form h2{margin-top:0}
build/string-locator-replace.js ADDED
@@ -0,0 +1 @@
 
1
+ !function(){"use strict";document.addEventListener("DOMContentLoaded",(function(){const e=document.getElementById("string-locator-replace-new-string"),t=document.getElementById("string-locator-replace-loopback-check"),n=document.getElementById("string-locator-toggle-replace-controls"),r=document.getElementById("string-locator-replace-form"),c=document.getElementById("string-locator-replace-form"),o=document.getElementById("string-locator-search-results-table"),a=document.getElementById("string-locator-search-notices"),l=document.getElementById("string-locator-progress-wrapper"),s=document.getElementById("string-locator-search-progress"),i=document.getElementById("string-locator-feedback-text"),d=document.getElementById("string-locator-string"),g=document.getElementById("string-locator-regex"),p=document.getElementById("string-locator-replace-button-all"),u=document.getElementById("string-locator-replace-button-selected"),m=wp.template("string-locator-replace-error");let y,E,f=!1,h=!1;function b(n){const r=new FormData,c={...y[n].dataset};if(s.value=n,h||y[n].getElementsByClassName("check-column-box")[0].checked){r.append("_wpnonce",stringLocatorReplace.rest_nonce),r.append("replace_nonce",stringLocatorReplace.replace_nonce),r.append("replace_string",e.value),r.append("search_string",d.value),r.append("search_regex",g.checked),r.append("replace_loopback",t.checked);for(const e in c)r.append(e,c[e]);fetch(stringLocatorReplace.url.replace,{method:"POST",body:r}).then((e=>e.json())).then((function(e){null!=e&&e.success?(E=y[n].getElementsByTagName("td")[0],!0!==e.data.replace_string&&(E.innerHTML=e.data.replace_string),n<y.length-1?b(n+1):(l.style.display="none",h=!1)):B(e)})).catch((function(e){B(e)}))}else n<y.length-1?b(n+1):(l.style.display="none",h=!1)}function B(e){a.style.display="block",l.style.display="none",h=!1,a.innerHTML+=m(e)}function L(){if(!f)return p.dispatchEvent(new Event("click")),!1;y=o.getElementsByTagName("tbody")[0].getElementsByTagName("tr"),l.style.display="block",i.innerText=stringLocatorReplace.string.replace_started,a.innerHTML="",s.value=0,s.setAttribute("max",y.length),f=!1,b(0)}c.addEventListener("submit",(function(e){return e.preventDefault(),L(),!1})),p.addEventListener("click",(function(){if(!confirm(stringLocatorReplace.string.confirm_all))return!1;h=!0,f=!0,L()})),u.addEventListener("click",(function(){h=!1,f=!0,L()})),n.addEventListener("click",(function(){r.classList.toggle("visible"),r.classList.contains("visible")?(n.setAttribute("aria-expanded","true"),n.innerText=stringLocatorReplace.string.button_hide):(n.setAttribute("aria-expanded","false"),n.innerText=stringLocatorReplace.string.button_show)}))}))}();
build/string-locator-search.asset.php CHANGED
@@ -1 +1 @@
1
- <?php return array('dependencies' => array(), 'version' => 'c872ff59b6ee90fa0f7b500730f6afd8');
1
+ <?php return array('dependencies' => array(), 'version' => 'd53f3966489b06eec2a1');
build/string-locator-search.js CHANGED
@@ -1 +1 @@
1
- jQuery(document).ready((function(t){let r=!1;const o=wp.template("string-locator-search-result");function s(r,o,s){t(".notices").append('<div class="notice notice-'+s+' is-dismissible"><p><strong>'+r+"</strong><br />"+o+"</p></div>")}function a(o,a){r=!1,t(".string-locator-feedback").hide(),s(o,a,"error")}function e(n,c){if(c>=n||!r)return t("#string-locator-feedback-text").html(string_locator.saving_results_string),r=!1,t("#string-locator-feedback-text").text(""),t.post(string_locator.url.clean,{_wpnonce:string_locator.rest_nonce},(function(){t(".string-locator-feedback").hide(),t("tbody",".tools_page_string-locator").is(":empty")&&t("tbody",".tools_page_string-locator").html('<tr><td colspan="3">'+string_locator.search_no_results+"</td></tr>")})).fail((function(t,r,o){a(t.status+" "+o,string_locator.search_error)})),!1;const i={filenum:c,_wpnonce:string_locator.rest_nonce};t.post(string_locator.url.search,i,(function(r){if(!r.success){if(!1===r.data.continue)return a(string_locator.warning_title,r.data.message),!1;s(string_locator.warning_title,r.data.message,"warning")}void 0!==r.data.search&&(t("#string-locator-search-progress").val(r.data.filenum),t("#string-locator-feedback-text").html(string_locator.search_current_prefix+r.data.next_file),function(r){if(t(".no-items",".tools_page_string-locator").is(":visible")&&t(".no-items",".tools_page_string-locator").hide(),Array!==r.constructor)return!1;r.forEach((function(r){if(r)for(let s=0,a=r.length;s<a;s++){const a=r[s];void 0!==a.stringresult&&t("tbody",".tools_page_string-locator").append(o(a))}}))}(r.data.search));const c=r.data.filenum+1;e(n,c)}),"json").fail((function(t,r,o){a(t.status+" "+o,string_locator.search_error)}))}t("#string-locator-search-form").on("submit",(function(o){o.preventDefault(),t("#string-locator-feedback-text").text(string_locator.search_preparing),t(".string-locator-feedback").show(),r=!0,t(".notices").html(""),t("#string-locator-search-progress").removeAttr("value"),t("tbody",".tools_page_string-locator").html("");const n={directory:t("#string-locator-search").val(),search:t("#string-locator-string").val(),regex:t("#string-locator-regex").is(":checked"),_wpnonce:string_locator.rest_nonce};t("table.tools_page_string-locator").show(),t.post(string_locator.url.directory_structure,n,(function(r){r.success?(t("#string-locator-search-progress").attr("max",r.data.total).val(r.data.current),t("#string-locator-feedback-text").text(string_locator.search_started),e(r.data.total,0)):s(r.data,"alert")}),"json").fail((function(t,r,o){a(t.status+" "+o,string_locator.search_error)}))}))}));
1
+ document.addEventListener("DOMContentLoaded",(function(){let t,e,n=!1,r="";const a=document.getElementById("string-locator-search-notices"),o=document.getElementById("string-locator-progress-wrapper"),c=document.getElementById("string-locator-search-progress"),s=document.getElementById("string-locator-feedback-text"),i=document.getElementById("string-locator-search-form"),l=document.getElementById("string-locator-search"),d=document.getElementById("string-locator-string"),u=document.getElementById("string-locator-regex"),g=document.getElementById("string-locator-search-results-table-wrapper"),_=document.getElementById("string-locator-search-results-table"),h=document.getElementsByTagName("tbody")[0];function m(t,e,n){a.innerHTML+='<div class="notice notice-'+n+' is-dismissible"><p><strong>'+t+"</strong><br />"+e+"</p></div>"}function p(t,e){n=!1,o.style.display="none",m(t,e,"error")}function y(){a.innerHTML="",c.removeAttribute("value"),h.innerHTML=""}function f(a,i){if(e=new FormData,i>=a||!n)return s.innerHTML=string_locator.saving_results_string,n=!1,e=new FormData,s.innerText="",e.append("_wpnonce",string_locator.rest_nonce),fetch(string_locator.url.clean,{method:"POST",body:e}).then((function(){o.style.display="none",h.getElementsByTagName("tr").length<1&&(h.innerHTML='<tr><td colspan="3">'+string_locator.search_no_results+"</td></tr>")})).catch((function(t){p(t,string_locator.search_error)})),!1;e.append("filenum",i),e.append("_wpnonce",string_locator.rest_nonce),fetch(string_locator.url.search,{method:"POST",body:e}).then((t=>t.json())).then((function(e){if(!e.success){if(!1===e.data.continue)return p(string_locator.warning_title,e.data.message),!1;m(string_locator.warning_title,e.data.message,"warning")}void 0!==e.data.search&&(c.value=e.data.filenum,s.innerHTML=string_locator.search_current_prefix+e.data.next_file,r=void 0!==e.data.type?e.data.type:"",function(e){if(Array!==e.constructor)return!1;t=wp.template("string-locator-search-result"+(""!==r?"-"+r:"")),e.forEach((function(e){if(e)for(let n=0,r=e.length;n<r;n++){const r=e[n];void 0!==r.stringresult&&(h.innerHTML+=t(r))}}))}(e.data.search));const n=e.data.filenum+1;f(a,n)})).catch((function(t){p(t,string_locator.search_error)}))}i.addEventListener("submit",(function(t){if(t.preventDefault(),"sql"===l.value)return void function(t){t.preventDefault(),e=new FormData,s.innerText=string_locator.search_preparing,o.style.display="block",n=!0,y();const r=JSON.stringify({directory:l.value,search:d.value,regex:u.checked});_.style.display="table",g.style.display="block",e.append("data",r),e.append("_wpnonce",string_locator.rest_nonce),fetch(string_locator.url.directory_structure,{method:"POST",body:e}).then((t=>t.json())).then((function(t){t.success?(c.setAttribute("max",t.data.total),c.value=t.data.current,s.innerText=string_locator.search_started,f(t.data.total,0)):m("",t.data,"alert")})).catch((function(t){p(t,string_locator.search_error)}))}(t);e=new FormData,s.innerText=string_locator.search_preparing,o.style.display="block",n=!0,y();const r=JSON.stringify({directory:l.value,search:d.value,regex:u.checked});_.style.display="table",g.style.display="block",e.append("data",r),e.append("_wpnonce",string_locator.rest_nonce),fetch(string_locator.url.directory_structure,{method:"POST",body:e}).then((t=>t.json())).then((function(t){t.success?(c.setAttribute("max",t.data.total),c.value=t.data.current,s.innerText=string_locator.search_started,f(t.data.total,0)):m("",t.data,"alert")})).catch((function(t){p(t,string_locator.search_error)}))}))}));
build/string-locator.asset.php CHANGED
@@ -1 +1 @@
1
- <?php return array('dependencies' => array(), 'version' => '1d5bc3a2fc5a7730308a2b7bc3219295');
1
+ <?php return array('dependencies' => array(), 'version' => 'c39f5990e5961dddae02');
build/string-locator.css CHANGED
@@ -1 +1 @@
1
- .wrap>h1{margin-bottom:15px}.string-locator-italics{font-style:italic}.string-locator-feedback{background:#fff;display:inline-block;text-align:center;width:100%}.string-locator-feedback.hide{display:none}.string-locator-feedback progress{height:1.5em;width:100%}.string-locator-feedback #string-locator-feedback-text{display:inline-block;text-align:center;width:100%}body.tools_page_string-locator.file-edit-screen #wpcontent{padding-left:0}body.tools_page_string-locator.file-edit-screen #wpfooter{display:none}body.tools_page_string-locator.file-edit-screen #wpbody-content{padding-bottom:0}table.tools_page_string-locator{display:none}table.tools_page_string-locator.restore{display:table}table .string{width:60%}table .filename{width:20%}table .line,table .position{width:10%}body.wp-admin.tools_page_string-locator #wpbody-content>.notice{display:none}.string-locator-editor-wrapper{grid-gap:0;-ms-grid-columns:80% 20%;display:-ms-grid;display:grid;grid-template-columns:80% 20%;height:100%;width:100%}.string-locator-editor-wrapper .notice,.string-locator-editor-wrapper .string-locator-header{align-items:stretch;background:#fff;border-bottom:1px solid #e2e4e7;display:flex;flex-direction:row;height:40px;justify-content:space-between;left:0;padding:4px 2px;position:-webkit-sticky;position:sticky;right:0;top:0;z-index:30}@media(min-width:600px){.string-locator-editor-wrapper .notice,.string-locator-editor-wrapper .string-locator-header{padding:8px;position:fixed;top:46px}}@media(min-width:782px){.string-locator-editor-wrapper .notice,.string-locator-editor-wrapper .string-locator-header{left:160px;top:32px}}.string-locator-editor-wrapper .notice .title,.string-locator-editor-wrapper .string-locator-header .title{font-size:16px}.string-locator-editor-wrapper .notice>div,.string-locator-editor-wrapper .string-locator-header>div{align-items:center;display:inline-flex}.string-locator-editor-wrapper .notice .button,.string-locator-editor-wrapper .string-locator-header .button{margin:0 3px 0 12px}.string-locator-editor-wrapper .notice{display:block;height:-webkit-fit-content;height:-moz-fit-content;height:fit-content;margin:0;top:89px}.string-locator-editor-wrapper .notice.is-dismissible{position:-webkit-sticky;position:sticky}.string-locator-editor-wrapper .string-locator-editor{margin-top:57px}.string-locator-editor-wrapper .string-locator-sidebar{background:#fff;border-left:1px solid #e2e4e7;margin-top:57px}.string-locator-editor-wrapper .string-locator-sidebar .string-locator-panel{border-top:1px solid #e2e4e7;padding-bottom:10px}.string-locator-editor-wrapper .string-locator-sidebar .string-locator-panel:first-of-type{border-top:none}.string-locator-editor-wrapper .string-locator-sidebar .string-locator-panel .title{border:none;box-shadow:none;color:#191e23;font-weight:600;margin:0;padding:15px}.string-locator-editor-wrapper .string-locator-sidebar .string-locator-panel .row{padding:5px 15px}.string-locator-editor-wrapper .CodeMirror .CodeMirror-activeline .CodeMirror-activeline-background,.string-locator-editor-wrapper .CodeMirror .CodeMirror-activeline .CodeMirror-gutter-background{background-color:#cfe4ff}
1
+ .wrap>h1{margin-bottom:15px}.string-locator-italics{font-style:italic}.string-locator-feedback{background:#fff;display:inline-block;text-align:center;width:100%}.string-locator-feedback.hide{display:none}.string-locator-feedback progress{height:1.5em;width:100%}.string-locator-feedback #string-locator-feedback-text{display:inline-block;text-align:center;width:100%}body.tools_page_string-locator.file-edit-screen #wpcontent{padding-left:0}body.tools_page_string-locator.file-edit-screen #wpfooter{display:none}body.tools_page_string-locator.file-edit-screen #wpbody-content{padding-bottom:0}#string-locator-search-results-table-wrapper{display:none}#string-locator-search-results-table-wrapper.restore{display:block}#string-locator-search-results-table-wrapper .tablenav{height:auto;min-height:30px}table.tools_page_string-locator{display:none}table.tools_page_string-locator.restore{display:table}table thead .manage-column.check-column{padding:11px 0 10px 3px}table .string{width:60%}table .filename{width:20%}table .line,table .position{width:10%}body.wp-admin.tools_page_string-locator #wpbody-content>.notice{display:none}.string-locator-editor-wrapper{grid-gap:0;-ms-grid-columns:80% 20%;display:-ms-grid;display:grid;grid-template-columns:80% 20%;height:100%;width:100%}.string-locator-editor-wrapper .notice,.string-locator-editor-wrapper .string-locator-header{align-items:stretch;background:#fff;border-bottom:1px solid #e2e4e7;display:flex;flex-direction:row;height:40px;justify-content:space-between;left:0;padding:4px 2px;position:-webkit-sticky;position:sticky;right:0;top:0;z-index:30}@media(min-width:600px){.string-locator-editor-wrapper .notice,.string-locator-editor-wrapper .string-locator-header{padding:8px;position:fixed;top:46px}}@media(min-width:782px){.string-locator-editor-wrapper .notice,.string-locator-editor-wrapper .string-locator-header{left:160px;top:32px}}.string-locator-editor-wrapper .notice .title,.string-locator-editor-wrapper .string-locator-header .title{font-size:16px}.string-locator-editor-wrapper .notice>div,.string-locator-editor-wrapper .string-locator-header>div{align-items:center;display:inline-flex}.string-locator-editor-wrapper .notice .button,.string-locator-editor-wrapper .string-locator-header .button{margin:0 3px 0 12px}.string-locator-editor-wrapper .notice{display:block;height:-webkit-fit-content;height:-moz-fit-content;height:fit-content;margin:0;top:89px}.string-locator-editor-wrapper .notice.is-dismissible{position:-webkit-sticky;position:sticky}.string-locator-editor-wrapper .string-locator-editor{margin-top:57px}.string-locator-editor-wrapper .string-locator-sidebar{background:#fff;border-left:1px solid #e2e4e7;margin-top:57px}.string-locator-editor-wrapper .string-locator-sidebar .string-locator-panel{border-top:1px solid #e2e4e7;padding-bottom:10px}.string-locator-editor-wrapper .string-locator-sidebar .string-locator-panel:first-of-type{border-top:none}.string-locator-editor-wrapper .string-locator-sidebar .string-locator-panel .title{border:none;box-shadow:none;color:#191e23;font-weight:600;margin:0;padding:15px}.string-locator-editor-wrapper .string-locator-sidebar .string-locator-panel .row{padding:5px 15px}.string-locator-editor-wrapper .CodeMirror .CodeMirror-activeline .CodeMirror-activeline-background,.string-locator-editor-wrapper .CodeMirror .CodeMirror-activeline .CodeMirror-gutter-background{background-color:#cfe4ff}
build/string-locator.js CHANGED
@@ -1 +1 @@
1
- !function(){"use strict";jQuery(document).ready((function(t){let o;if(!1!==string_locator.CodeMirror&&""!==string_locator.CodeMirror){o=wp.codeEditor.initialize("code-editor",string_locator.CodeMirror);const e=wp.template("string-locator-alert");function r(t){const o=Math.max(document.documentElement.clientHeight,window.innerHeight||0)-89;t.setSize(null,parseInt(o))}t(".string-locator-editor").on("click",".string-locator-edit-goto",(function(e){e.preventDefault(),o.codemirror.scrollIntoView(parseInt(t(this).data("goto-line"))),o.codemirror.setCursor(parseInt(t(this).data("goto-line")-1),t(this).data("goto-linepos"))})),t("body").on("submit","#string-locator-edit-form",(function(o){const r=t("#string-locator-notices");return t.post(string_locator.url.save,t(this).serialize()).always((function(o){void 0===o.notices?r.append(e({type:"error",message:o.responseText})):t.each(o.notices,(function(){r.append(e(this))}))})),o.preventDefault(),!1})),r(o.codemirror),o.codemirror.scrollIntoView(parseInt(string_locator.goto_line)),o.codemirror.setCursor(parseInt(string_locator.goto_line-1),parseInt(string_locator.goto_linepos)),window.addEventListener("resize",r(o.codemirror))}else o=t("#code-editor"),o.css("width",t(".string-locator-edit-wrap").width()),o.css("height",parseInt(Math.max(document.documentElement.clientHeight,window.innerHeight||0)-89));t("#string-locator-notices").on("click",".notice-dismiss",(function(o){return t(this).closest(".notice").slideUp(400,"swing",(function(){t(this).remove()})),o.preventDefault(),!1}))}))}();
1
+ !function(){"use strict";document.addEventListener("DOMContentLoaded",(function(){let t,e;if(!1!==string_locator.CodeMirror&&""!==string_locator.CodeMirror){t=wp.codeEditor.initialize("code-editor",string_locator.CodeMirror);const o=wp.template("string-locator-alert");function n(){const e=Math.max(document.documentElement.clientHeight,window.innerHeight||0)-89;t.codemirror.setSize(null,parseInt(e))}document.addEventListener("click",(function(e){const o=e.target;o.classList.contains("string-locator-edit-goto")&&(e.preventDefault(),t.codemirror.scrollIntoView(parseInt(o.dataset.gotoLine)),t.codemirror.setCursor(parseInt(o.dataset.gotoLine-1),o.dataset.gotoLinepos))})),document.getElementById("string-locator-edit-form").addEventListener("submit",(function(t){const n=document.getElementById("string-locator-notices");return e=new FormData(this),fetch(string_locator.url.save,{method:"POST",body:e}).then((t=>t.json())).then((function(t){void 0===t.notices?n.innerHTML+=o({type:"error",message:t.responseText}):t.notices.forEach((function(t){n.innerHTML+=o(t)}))})),t.preventDefault(),!1})),n(),t.codemirror.scrollIntoView(parseInt(string_locator.goto_line)),t.codemirror.setCursor(parseInt(string_locator.goto_line-1),parseInt(string_locator.goto_linepos)),window.onresize=n}else t=document.getElementById("code-editor"),t.style.width=document.getElementsByClassName("string-locator-edit-wrap")[0].offsetWidth,t.style.height=parseInt(Math.max(document.documentElement.clientHeight,window.innerHeight||0)-89)}))}();
changelog.txt CHANGED
@@ -1,3 +1,12 @@
 
 
 
 
 
 
 
 
 
1
  = 2.4.2 =
2
  * Fixed the option to restore previous search.
3
  * Fixed respecting text capitalization in previews when doing a non-regex search.
1
+ = 2.5.0 (2022-02-27) =
2
+ * Fixed a bug where content would have slashes stripped unexpectedly.
3
+ * Improved table spacing on search results.
4
+ * Improved loopback checks to also check admin access.
5
+ * Hardened the search iterator so users can't accidentally perform unexpected directory traversal.
6
+ * Introduced actions and filters in various places to enable extenders, and future enhancements.
7
+ * Moved all ajax requests to dedicated REST endpoints.
8
+ * Refactored file structure.
9
+
10
  = 2.4.2 =
11
  * Fixed the option to restore previous search.
12
  * Fixed respecting text capitalization in previews when doing a non-regex search.
includes/Base/class-rest.php ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Base class for all REST API controllers.
4
+ */
5
+
6
+ namespace StringLocator\Base;
7
+
8
+ use StringLocator\String_Locator;
9
+
10
+ /**
11
+ * Base REST class.
12
+ */
13
+ class REST extends \WP_REST_Controller {
14
+
15
+ protected $namespace = 'string-locator/v1';
16
+
17
+ /**
18
+ * Class constructor.
19
+ */
20
+ public function __construct() {
21
+ add_action( 'rest_api_init', array( $this, 'register_rest_route' ) );
22
+ }
23
+
24
+ /**
25
+ * Generic helper function to check if the current user
26
+ * meets the minimum access requirements of the plugin.
27
+ *
28
+ * @return bool
29
+ */
30
+ public function permission_callback() {
31
+ return current_user_can( String_Locator::$default_capability );
32
+ }
33
+
34
+ }
includes/Base/class-search.php ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Base class for handling Search requests.
4
+ */
5
+
6
+ namespace StringLocator\Base;
7
+
8
+ /**
9
+ * Search class.
10
+ */
11
+ class Search {
12
+ /**
13
+ * The server-configured max time a script can run.
14
+ *
15
+ * @var int
16
+ */
17
+ protected $max_execution_time = null;
18
+
19
+ /**
20
+ * The current time when our script started executing.
21
+ *
22
+ * @var float
23
+ */
24
+ protected $start_execution_timer = 0;
25
+
26
+ /**
27
+ * The server-configured max amount of memory a script can use.
28
+ *
29
+ * @var int
30
+ */
31
+ protected $max_memory_consumption = 0;
32
+
33
+ /**
34
+ * The path to the currently editable file.
35
+ *
36
+ * @var string
37
+ */
38
+ protected $path_to_use = '';
39
+
40
+ /**
41
+ * Class constructor.
42
+ */
43
+ public function __construct() {
44
+ /**
45
+ * Define class variables requiring expressions
46
+ */
47
+ $this->path_to_use = ( is_multisite() ? 'network/admin.php' : 'tools.php' );
48
+
49
+ $this->max_execution_time = absint( ini_get( 'max_execution_time' ) );
50
+ $this->start_execution_timer = microtime( true );
51
+
52
+ if ( $this->max_execution_time > 30 ) {
53
+ $this->max_execution_time = 30;
54
+ }
55
+
56
+ $this->set_memory_limit();
57
+
58
+ add_action( 'string_locator_search_templates', array( $this, 'add_search_response_template' ) );
59
+ }
60
+
61
+ /**
62
+ * Load an underscores template file to be used in the search response.
63
+ *
64
+ * @return void
65
+ */
66
+ public function add_search_response_template() {
67
+ require_once STRING_LOCATOR_PLUGIN_DIR . '/views/templates/search-default.php';
68
+ }
69
+
70
+ /**
71
+ * Sets up the memory limit variables.
72
+ *
73
+ * @return void
74
+ * @since 2.0.0
75
+ *
76
+ */
77
+ function set_memory_limit() {
78
+ $memory_limit = ini_get( 'memory_limit' );
79
+
80
+ $this->max_memory_consumption = absint( $memory_limit );
81
+
82
+ if ( strstr( $memory_limit, 'k' ) ) {
83
+ $this->max_memory_consumption = ( str_replace( 'k', '', $memory_limit ) * 1000 );
84
+ }
85
+ if ( strstr( $memory_limit, 'M' ) ) {
86
+ $this->max_memory_consumption = ( str_replace( 'M', '', $memory_limit ) * 1000000 );
87
+ }
88
+ if ( strstr( $memory_limit, 'G' ) ) {
89
+ $this->max_memory_consumption = ( str_replace( 'G', '', $memory_limit ) * 1000000000 );
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Check if the script is about to exceed the max execution time.
95
+ *
96
+ * @return bool
97
+ * @since 1.9.0
98
+ *
99
+ */
100
+ function nearing_execution_limit() {
101
+ // Max execution time is 0 or -1 (infinite) in server config
102
+ if ( 0 === $this->max_execution_time || - 1 === $this->max_execution_time ) {
103
+ return false;
104
+ }
105
+
106
+ $back_compat_filter = apply_filters( 'string-locator-extra-search-delay', 2 ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
107
+
108
+ $built_in_delay = apply_filters( 'string_locator_extra_search_delay', $back_compat_filter );
109
+ $execution_time = ( microtime( true ) - $this->start_execution_timer + $built_in_delay );
110
+
111
+ if ( $execution_time >= $this->max_execution_time ) {
112
+ return $execution_time;
113
+ }
114
+
115
+ return false;
116
+ }
117
+
118
+ /**
119
+ * Check if the script is about to exceed the server memory limit.
120
+ *
121
+ * @return bool
122
+ * @since 2.0.0
123
+ *
124
+ */
125
+ function nearing_memory_limit() {
126
+ // Check if the memory limit is set t o0 or -1 (infinite) in server config
127
+ if ( 0 === $this->max_memory_consumption || - 1 === $this->max_memory_consumption ) {
128
+ return false;
129
+ }
130
+
131
+ // We give our selves a 256k memory buffer, as we need to close off the script properly as well
132
+ $back_compat_filter = apply_filters( 'string-locator-extra-memory-buffer', 256000 ); //phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
133
+ $built_in_buffer = apply_filters( 'string_locator_extra_memory_buffer', $back_compat_filter );
134
+ $memory_use = ( memory_get_usage( true ) + $built_in_buffer );
135
+
136
+ if ( $memory_use >= $this->max_memory_consumption ) {
137
+ return $memory_use;
138
+ }
139
+
140
+ return false;
141
+ }
142
+ }
includes/Extension/SQL/Tests/class-serialized-data.php ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Class for testing serialized data on SQL saves.
4
+ */
5
+
6
+ namespace StringLocator\Extensions\SQL\Tests;
7
+
8
+ /**
9
+ * Serialized_Data class.
10
+ */
11
+ class Serialized_Data {
12
+
13
+ /**
14
+ * The content that will be scanned.
15
+ *
16
+ * @var string
17
+ */
18
+ private $content = '';
19
+
20
+ /**
21
+ * An array holding any errors returned during testing.
22
+ *
23
+ * @var array
24
+ */
25
+ public $errors = array();
26
+
27
+ /**
28
+ * SmartScan constructor.
29
+ */
30
+ public function __construct() {
31
+ add_action( 'string_locator_editor_checks', array( $this, 'print_checks_option' ) );
32
+
33
+ add_filter( 'string_locator_pre_save', array( $this, 'maybe_perform_test' ), 10, 2 );
34
+ add_filter( 'string_locator_pre_save_fail_notice', array( $this, 'return_failure_notices' ) );
35
+ }
36
+
37
+ /**
38
+ * Combine reported errors with any new notices.
39
+ *
40
+ * @param $notices
41
+ *
42
+ * @return mixed
43
+ */
44
+ public function return_failure_notices( $notices ) {
45
+ if ( empty( $this->errors ) ) {
46
+ return $notices;
47
+ }
48
+
49
+ return array_merge(
50
+ $notices,
51
+ $this->errors
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Conditionally run the test tool.
57
+ *
58
+ * @param bool $can_save A boolean value if the test has passed or failed.
59
+ * @param string $content The content being modified.
60
+ *
61
+ * @return bool|mixed|null
62
+ */
63
+ public function maybe_perform_test( $can_save, $content ) {
64
+ // If another addon has determined the file can not be saved, bail early.
65
+ if ( ! $can_save ) {
66
+ return $can_save;
67
+ }
68
+
69
+ // Do not perform a smart scan if the option for it is disabled.
70
+ if ( ! isset( $_POST['string-locator-validate-serialized-data'] ) ) {
71
+ return $can_save;
72
+ }
73
+
74
+ return $this->run( $content );
75
+ }
76
+
77
+ /**
78
+ * Output a checkbox to enable or disable this feature from within the editor interface.
79
+ *
80
+ * @return void
81
+ */
82
+ public function print_checks_option() {
83
+ if ( ! isset( $_GET['file-type'] ) || 'sql' !== $_GET['file-type'] ) {
84
+ return;
85
+ }
86
+ ?>
87
+
88
+ <div class="row">
89
+ <label>
90
+ <input type="checkbox" name="string-locator-validate-serialized-data" checked="checked">
91
+ <?php esc_html_e( 'If the SQL data is serialized, validate it before saving.', 'string-locator' ); ?>
92
+ </label>
93
+ </div>
94
+
95
+ <?php
96
+ }
97
+
98
+ /**
99
+ * A helper function to return any errors.
100
+ *
101
+ * @return array
102
+ */
103
+ public function get_errors() {
104
+ return $this->errors;
105
+ }
106
+
107
+ /**
108
+ * Main test runner.
109
+ *
110
+ * @param string $content The content to scan.
111
+ *
112
+ * @return bool
113
+ */
114
+ public function run( $content ) {
115
+ $this->content = $content;
116
+
117
+ // Reset the stored errors for a fresh run.
118
+ $this->errors = array();
119
+
120
+ if ( \is_serialized( $this->content ) ) {
121
+ $test_data = @unserialize( $this->content );
122
+
123
+ if ( 'b:0;' !== $this->content && false === $test_data ) {
124
+ $this->errors[] = array(
125
+ 'type' => 'error',
126
+ 'message' => __( 'The data is meant to be serialized with PHP, but is no longer valid.', 'string-locator' ),
127
+ );
128
+ }
129
+ }
130
+
131
+ if ( ! empty( $this->errors ) ) {
132
+ return false;
133
+ }
134
+
135
+ return true;
136
+ }
137
+ }
138
+
139
+ new Serialized_Data();
includes/Extension/SQL/class-edit.php ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Class to handle the edit page.
4
+ */
5
+
6
+ namespace StringLocator\Extension\SQL;
7
+
8
+ use StringLocator\String_Locator;
9
+
10
+ /**
11
+ * Edit class.
12
+ */
13
+ class Edit {
14
+
15
+ /**
16
+ * Class constructor.
17
+ */
18
+ public function __construct() {
19
+ add_filter( 'string_locator_view', array( $this, 'sql_edit_page' ) );
20
+
21
+ add_filter( 'admin_body_class', array( $this, 'admin_body_class' ) );
22
+ add_filter( 'string_locator_editor_fields', array( $this, 'editor_form_fields' ) );
23
+ }
24
+
25
+ /**
26
+ * Generate form fields that need ot be a part of the editor interface for this data type.
27
+ *
28
+ * @param array $fields An array of form fields to output in hidden elements.
29
+ *
30
+ * @return array
31
+ */
32
+ public function editor_form_fields( $fields ) {
33
+ if ( isset( $_GET['file-type'] ) && 'sql' === $_GET['file-type'] ) {
34
+ $fields = array_merge(
35
+ array(
36
+ 'sql-column' => $_GET['sql-column'],
37
+ 'sql-table' => $_GET['sql-table'],
38
+ 'sql-primary-column' => $_GET['sql-primary-column'],
39
+ 'sql-primary-type' => $_GET['sql-primary-type'],
40
+ 'sql-primary-key' => $_GET['sql-primary-key'],
41
+ ),
42
+ $fields
43
+ );
44
+ }
45
+
46
+ return $fields;
47
+ }
48
+
49
+ /**
50
+ * Append a helper class ot the wp-admin body class.
51
+ *
52
+ * @param string $class The classes for the admin body class.
53
+ *
54
+ * @return string
55
+ */
56
+ public function admin_body_class( $class ) {
57
+ if ( isset( $_GET['file-type'] ) && 'sql' === $_GET['file-type'] && current_user_can( String_Locator::$default_capability ) ) {
58
+ $class .= ' file-edit-screen';
59
+ }
60
+
61
+ return $class;
62
+ }
63
+
64
+ /**
65
+ * Conditionally filter the editor interface for SQL files.
66
+ *
67
+ * @param string $include_path The path to the editor interface.
68
+ *
69
+ * @return string
70
+ */
71
+ public function sql_edit_page( $include_path ) {
72
+ if ( ! isset( $_GET['file-type'] ) || 'sql' !== $_GET['file-type'] || ! current_user_can( String_Locator::$default_capability ) ) {
73
+ return $include_path;
74
+ }
75
+
76
+ // Validate the table name.
77
+ if ( ! isset( $_GET['sql-table'] ) || ! validate_sql_fields( $_GET['sql-table'] ) ) {
78
+ return $include_path;
79
+ }
80
+
81
+ // Validate the primary column
82
+ if ( ! isset( $_GET['sql-primary-column'] ) || ! validate_sql_fields( $_GET['sql-primary-column'] ) ) {
83
+ return $include_path;
84
+ }
85
+
86
+ // A primary key needs to be provided, this could be anything so we just make sure it is set and not empty.
87
+ if ( ! isset( $_GET['sql-primary-key'] ) || empty( $_GET['sql-primary-key'] ) ) {
88
+ return $include_path;
89
+ }
90
+
91
+ return STRING_LOCATOR_PLUGIN_DIR . '/includes/Extension/SQL/views/editor/sql.php';
92
+ }
93
+ }
94
+
95
+ new Edit();
includes/Extension/SQL/class-save.php ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace StringLocator\Extension\SQL;
4
+
5
+ /**
6
+ * Save class.
7
+ */
8
+ class Save {
9
+
10
+ private $override_save = false;
11
+
12
+ /**
13
+ * Class constructor.
14
+ */
15
+ public function __construct() {
16
+ add_filter( 'string_locator_save_params', array( $this, 'check_save_parameters' ) );
17
+ add_filter( 'string_locator_save_handler', array( $this, 'maybe_handle_save' ) );
18
+ }
19
+
20
+ /**
21
+ * Check the save parameters to determine if the SQL handler should take over the save request.
22
+ *
23
+ * @param array $parameters An array of REST API request parameters.
24
+ *
25
+ * @return array
26
+ */
27
+ public function check_save_parameters( $parameters ) {
28
+ if ( isset( $parameters['file-type'] ) && 'sql' === $parameters['file-type'] ) {
29
+ $this->override_save = true;
30
+ }
31
+
32
+ return $parameters;
33
+ }
34
+
35
+ /**
36
+ * Override the save handler if the parameters indicate that the SQL handler should take over.
37
+ *
38
+ * @param mixed $handler The class handling the save request.
39
+ *
40
+ * @return self|mixed
41
+ */
42
+ public function maybe_handle_save( $handler ) {
43
+ if ( ! $this->override_save ) {
44
+ return $handler;
45
+ }
46
+
47
+ return $this;
48
+ }
49
+
50
+ /**
51
+ * Funciton to trigger the save behavior.
52
+ *
53
+ * @param array $params An array of save parameters.
54
+ *
55
+ * @return array|\array[][]
56
+ */
57
+ public function save( $params ) {
58
+ global $wpdb;
59
+
60
+ $content = $params['string-locator-editor-content'];
61
+
62
+ /**
63
+ * Filter if the save process should be performed or not.
64
+ *
65
+ * @attr bool $can_save Can the save be carried out.
66
+ * @attr string $content The content to save.
67
+ * @attr string $path Path to the file being edited.
68
+ */
69
+ $can_save = apply_filters( 'string_locator_pre_save', true, $content, 'sql' );
70
+
71
+ if ( ! $can_save ) {
72
+ return array(
73
+ 'notices' => apply_filters( 'string_locator_pre_save_fail_notice', array() ),
74
+ );
75
+ }
76
+
77
+ if ( 'int' === $params['sql-primary-type'] ) {
78
+ $original = $wpdb->get_var(
79
+ $wpdb->prepare(
80
+ 'SELECT ' . $params['sql-column'] . ' FROM ' . $params['sql-table'] . ' WHERE ' . $params['sql-primary-column'] . ' = %d LIMIT 1', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is not possible to prepare a table or column name, but these are instead validated in `/includes/Search/class-sql.php` before reaching this point.
81
+ $params['sql-primary-key']
82
+ )
83
+ );
84
+ } else {
85
+ $original = $wpdb->get_var(
86
+ $wpdb->prepare(
87
+ 'SELECT ' . $params['sql-column'] . ' FROM ' . $params['sql-table'] . ' WHERE ' . $params['sql-primary-column'] . ' = %s LIMIT 1', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is not possible to prepare a table or column name, but these are instead validated in `/includes/Search/class-sql.php` before reaching this point.
88
+ $params['sql-primary-key']
89
+ )
90
+ );
91
+ }
92
+
93
+ $wpdb->update(
94
+ $params['sql-table'],
95
+ array(
96
+ $params['sql-column'] => $content,
97
+ ),
98
+ array(
99
+ $params['sql-primary-column'] => $params['sql-primary-key'],
100
+ )
101
+ );
102
+
103
+ /**
104
+ * Filter if the save process completed as it should or if warnings should be returned.
105
+ *
106
+ * @attr bool $save_successful Boolean indicating if the save was successful.
107
+ * @attr string $content The edited content.
108
+ * @attr string $original The original content.
109
+ * @attr string $path The path to the file being edited.
110
+ */
111
+ $save_successful = apply_filters( 'string_locator_post_save', true, $content, $original, 'sql' );
112
+
113
+ /**
114
+ * Check the status of the site after making our edits.
115
+ * If the site fails, revert the changes to return the sites to its original state
116
+ */
117
+ if ( ! $save_successful ) {
118
+ $wpdb->update(
119
+ $params['sql-table'],
120
+ array(
121
+ $params['sql-column'] => $original,
122
+ ),
123
+ array(
124
+ $params['sql-primary-column'] => $params['sql-primary-key'],
125
+ )
126
+ );
127
+
128
+ return array(
129
+ 'notices' => apply_filters( 'string_locator_post_save_fail_notice', array() ),
130
+ );
131
+ }
132
+
133
+ return array(
134
+ 'notices' => array(
135
+ array(
136
+ 'type' => 'success',
137
+ 'message' => __( 'The database entry has been updated.', 'string-locator' ),
138
+ ),
139
+ ),
140
+ );
141
+ }
142
+
143
+ }
144
+
145
+ new Save();
includes/Extension/SQL/class-search.php ADDED
@@ -0,0 +1,367 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace StringLocator\Extension\SQL;
4
+
5
+ use StringLocator\Base\Search as SearchBase;
6
+ use StringLocator\String_Locator;
7
+
8
+ /**
9
+ * Search class.
10
+ */
11
+ class Search extends SearchBase {
12
+
13
+ /**
14
+ * Class constructor.
15
+ */
16
+ public function __construct() {
17
+ add_filter( 'string_locator_search_sources_markup', array( $this, 'add_search_options' ), 11, 2 );
18
+
19
+ add_filter( 'string_locator_search_handler', array( $this, 'maybe_perform_sql_search' ), 10, 2 );
20
+ add_filter( 'string_locator_directory_iterator_short_circuit', array( $this, 'maybe_short_circuit_directory_iterator' ), 10, 2 );
21
+
22
+ add_filter( 'string_locator_restore_search_row', array( $this, 'restore_sql_search' ), 10, 2 );
23
+
24
+ parent::__construct();
25
+ }
26
+
27
+ /**
28
+ * Handle markup output when restoring a search result.
29
+ *
30
+ * @param string $row The result row.
31
+ * @param object $item The search result item for this row.
32
+ *
33
+ * @return string
34
+ */
35
+ public function restore_sql_search( $row, $item ) {
36
+ if ( ! isset( $item->primary_key ) ) {
37
+ return $row;
38
+ }
39
+
40
+ $row = sprintf(
41
+ '<tr data-type="sql" data-primary-key="%d" data-primary-column="%s" data-primary-type="%s" data-table-name="%s" data-column-name="%s">
42
+ <th scope="row" class="check-column">
43
+ <input type="checkbox" name="string-locator-replace-checked[]" class="check-column-box">
44
+ </th>
45
+ <td>
46
+ %s
47
+ <div class="row-actions">
48
+ %s
49
+ </div>
50
+ </td>
51
+ <td>
52
+ %s
53
+ </td>
54
+ <td>
55
+ %d
56
+ </td>
57
+ <td>
58
+ %d
59
+ </td>
60
+ </tr>',
61
+ esc_attr( $item->primary_key ),
62
+ esc_attr( $item->primary_column ),
63
+ esc_attr( $item->primary_type ),
64
+ esc_attr( $item->table ),
65
+ esc_attr( $item->column ),
66
+ $item->stringresult,
67
+ ( ! current_user_can( String_Locator::$default_capability ) ? '' : sprintf(
68
+ '<span class="edit"><a href="%1$s" aria-label="%2$s">%2$s</a></span>',
69
+ esc_url( $item->editurl ),
70
+ // translators: The row-action edit link label.
71
+ esc_html__( 'Edit', 'string-locator' )
72
+ ) ),
73
+ ( ! current_user_can( String_Locator::$default_capability ) ? $item->filename : sprintf(
74
+ '<a href="%s">%s</a>',
75
+ esc_url( $item->editurl ),
76
+ esc_html( $item->filename )
77
+ ) ),
78
+ esc_html( $item->primary_key ),
79
+ esc_html( $item->linepos )
80
+ );
81
+
82
+ return $row;
83
+ }
84
+
85
+ /**
86
+ * Add SQL items as search options.
87
+ *
88
+ * @param string $searchers The markup for the existing search options.
89
+ * @param string $search_location The currently selected search option, when restoring a search.
90
+ *
91
+ * @return string
92
+ */
93
+ public function add_search_options( $searchers, $search_location ) {
94
+ ob_start();
95
+ ?>
96
+
97
+ <optgroup label="<?php esc_attr_e( 'Database', 'string-locator' ); ?>">
98
+ <option value="sql"<?php echo ( 'sql' === $search_location ? ' selected="selected"' : '' ); ?>><?php esc_html_e( 'All database tables', 'string-locator' ); ?></option>
99
+ </optgroup>
100
+
101
+ <?php
102
+
103
+ $searchers .= ob_get_clean();
104
+
105
+ return $searchers;
106
+ }
107
+
108
+ /**
109
+ * Output the underscores template used for SQL search results.
110
+ *
111
+ * @return void
112
+ */
113
+ public function add_search_response_template() {
114
+ require_once STRING_LOCATOR_PLUGIN_DIR . '/includes/Extension/SQL/views/template/search.php';
115
+ }
116
+
117
+ /**
118
+ * Allow the SQL search to ignore the DirectoryIterator request used to build search bases for files.
119
+ *
120
+ * @param bool $short_circuit Whether to short-circuit the DirectoryIterator request.
121
+ * @param \WP_REST_Response $request The WP_REST_Request for this API call.
122
+ *
123
+ * @return array
124
+ */
125
+ public function maybe_short_circuit_directory_iterator( $short_circuit, $request ) {
126
+ $data = json_decode( $request->get_param( 'data' ) );
127
+
128
+ if ( 'sql' === $data->directory ) {
129
+ $store = (object) array(
130
+ 'type' => 'sql',
131
+ 'search' => $data->search,
132
+ 'directory' => 'sql',
133
+ 'chunks' => 1,
134
+ 'regex' => $data->regex,
135
+ );
136
+
137
+ set_transient( 'string-locator-search-overview', $store );
138
+ update_option( 'string-locator-search-history', array(), false );
139
+
140
+ return array(
141
+ 'success' => true,
142
+ 'data' => array(
143
+ 'chunks' => 1,
144
+ 'current' => 0,
145
+ 'regex' => $data->regex,
146
+ 'total' => 1,
147
+ ),
148
+ );
149
+ }
150
+
151
+ return $short_circuit;
152
+ }
153
+
154
+ /**
155
+ * Conditionally override the search handler.
156
+ *
157
+ * @param mixed $handler The currently active class for handling the search request.
158
+ * @param \WP_REST_Request $request The request received by the REST API handler.
159
+ *
160
+ * @return $this
161
+ */
162
+ public function maybe_perform_sql_search( $handler, $request ) {
163
+ $search_data = get_transient( 'string-locator-search-overview' );
164
+
165
+ if ( empty( $search_data ) || ! isset( $search_data->type ) || 'sql' !== $search_data->type ) {
166
+ return $handler;
167
+ }
168
+
169
+ return $this;
170
+ }
171
+
172
+ /**
173
+ * Run the search.
174
+ *
175
+ * @param int $filenum An integer representing where in the line you are when doing batch searches.
176
+ *
177
+ * @return array
178
+ */
179
+ public function run( $filenum ) {
180
+ global $wpdb;
181
+
182
+ $response = array(
183
+ 'search' => array(),
184
+ 'filenum' => absint( $filenum ),
185
+ 'current' => 0,
186
+ 'total' => 0,
187
+ 'type' => 'sql',
188
+ );
189
+
190
+ $scan_data = get_transient( 'string-locator-search-overview' );
191
+
192
+ $is_regex = false;
193
+ if ( isset( $scan_data->regex ) ) {
194
+ $is_regex = String_Locator::absbool( $scan_data->regex );
195
+ }
196
+
197
+ if ( $is_regex ) {
198
+ if ( false === @preg_match( $scan_data->search, '' ) ) { // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
199
+ wp_send_json_error(
200
+ array(
201
+ 'continue' => false,
202
+ 'message' => sprintf(
203
+ /* translators: %s: The search string used. */
204
+ __( 'Your search string, <strong>%s</strong>, is not a valid pattern, and the search has been aborted.', 'string-locator' ),
205
+ esc_html( $scan_data->search )
206
+ ),
207
+ )
208
+ );
209
+ }
210
+ }
211
+
212
+ // Get al ist of all available tables to search through.
213
+ $tables = $wpdb->get_results( 'SHOW TABLES' );
214
+
215
+ $identifier_name = 'Tables_in_' . DB_NAME;
216
+
217
+ if ( ! validate_sql_fields( $identifier_name ) ) {
218
+ wp_send_json_error(
219
+ array(
220
+ 'continue' => false,
221
+ 'message' => sprintf(
222
+ /* translators: %s: The search string used. */
223
+ __( 'The table identifier, combined with your database name, <strong>%s</strong>, is not a valid SQL pattern, and the search has been aborted.', 'string-locator' ),
224
+ esc_html( $identifier_name )
225
+ ),
226
+ )
227
+ );
228
+ }
229
+
230
+ $match_count = 0;
231
+
232
+ $search_results = array();
233
+
234
+ foreach ( $tables as $table ) {
235
+ $table_name = $table->{ $identifier_name };
236
+
237
+ $columns = $wpdb->get_results( 'DESCRIBE ' . $table_name ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- The table name is validated earlier for SQLi, and needs to be dynamic due to relying on `DB_NAME`.
238
+
239
+ $primary_column = null;
240
+ $primary_type = null;
241
+
242
+ // Initial loop only gets primary data.
243
+ foreach ( $columns as $column ) {
244
+ if ( 'PRI' === $column->Key ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Object property name is returned by the MySQL database.
245
+ $primary_column = $column->Field; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Object property name is returned by the MySQL database.
246
+ $primary_type = ( stristr( $column->Type, 'int' ) ? 'int' : 'str' ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Object property name is returned by the MySQL database.
247
+ }
248
+ }
249
+
250
+ foreach ( $columns as $column ) {
251
+ $column_name = $column->Field; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Object property name is returned by the MySQL database.
252
+
253
+ if ( $is_regex ) {
254
+ $matches = $wpdb->get_results(
255
+ $wpdb->prepare(
256
+ 'SELECT ' . $column_name . ' AS column_name, ' . $primary_column . ' as primary_column FROM ' . $table_name . ' WHERE ' . $column_name . ' REGEXP %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is not possible to prepare a table or column name, but these are instead validated in `/includes/Search/class-sql.php` before reaching this point.
257
+ $scan_data->search
258
+ )
259
+ );
260
+ } else {
261
+ $matches = $wpdb->get_results(
262
+ $wpdb->prepare(
263
+ 'SELECT ' . $column_name . ' AS column_name, ' . $primary_column . ' as primary_column FROM ' . $table_name . ' WHERE ' . $column_name . ' LIKE %s', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is not possible to prepare a table or column name, but these are instead validated in `/includes/Search/class-sql.php` before reaching this point.
264
+ '%' . $wpdb->esc_like( $scan_data->search ) . '%'
265
+ )
266
+ );
267
+ }
268
+
269
+ if ( is_wp_error( $matches ) ) {
270
+ wp_send_json_error(
271
+ array(
272
+ 'continue' => false,
273
+ 'message' => sprintf(
274
+ /* translators: 1: The search string used. 2: The error received */
275
+ __( 'Your search for <strong>%1$s</strong> led to an SQL error, and the search has been aborted. The error encountered was: %2$s', 'string-locator' ),
276
+ esc_html( $scan_data->search ),
277
+ esc_html( $matches->get_error_message() )
278
+ ),
279
+ )
280
+ );
281
+ }
282
+
283
+ foreach ( $matches as $match ) {
284
+ $match_count++;
285
+
286
+ $string = $scan_data->search;
287
+ $string_preview = $match->column_name;
288
+
289
+ $string_location = 0;
290
+
291
+ $string_preview = String_Locator::create_preview( $string_preview, $string, $is_regex );
292
+
293
+ $editurl = $this->create_edit_link( $table_name, $column_name, $primary_column, $primary_type, $match );
294
+
295
+ $search_results[] = array(
296
+ 'ID' => $match_count,
297
+ 'table' => $table_name,
298
+ 'column' => $column_name,
299
+ 'primary_key' => $match->primary_column,
300
+ 'primary_type' => $primary_type,
301
+ 'primary_column' => $primary_column,
302
+ 'filename' => sprintf(
303
+ '`%s`.`%s`',
304
+ $table_name,
305
+ $column_name
306
+ ),
307
+ 'filename_raw' => sprintf(
308
+ '`%s`.`%s`',
309
+ $table_name,
310
+ $column_name
311
+ ),
312
+ 'editurl' => ( current_user_can( String_Locator::$default_capability ) ? $editurl : false ),
313
+ 'stringresult' => $string_preview,
314
+ 'linepos' => $string_location,
315
+ 'linenum' => 0,
316
+ );
317
+ }
318
+ }
319
+ }
320
+
321
+ if ( ! empty( $search_results ) ) {
322
+ $history = get_option( 'string-locator-search-history', array() );
323
+ $history = array_merge( $history, $search_results );
324
+ update_option( 'string-locator-search-history', $history, false );
325
+ }
326
+
327
+ $response['search'] = array(
328
+ $search_results,
329
+ );
330
+
331
+ return $response;
332
+ }
333
+
334
+ /**
335
+ * Generate a link to the editor interface for a search result.
336
+ *
337
+ * @param string $table_name The table name where a match was found.
338
+ * @param string $column_name The column name where a match was found.
339
+ * @param string $primary_column The primary column from the table having a match.
340
+ * @param string $primary_type The type of the primary column.
341
+ * @param object $match An object containing details of the match.
342
+ *
343
+ * @return string
344
+ */
345
+ public function create_edit_link( $table_name, $column_name, $primary_column, $primary_type, $match ) {
346
+ return add_query_arg(
347
+ array(
348
+ 'page' => 'string-locator',
349
+ 'edit-file' => true,
350
+ 'file-type' => 'sql',
351
+ 'file-reference' => sprintf(
352
+ '`%s`.`%s`',
353
+ $table_name,
354
+ $column_name
355
+ ),
356
+ 'sql-column' => $column_name,
357
+ 'sql-table' => $table_name,
358
+ 'sql-primary-column' => $primary_column,
359
+ 'sql-primary-type' => $primary_type,
360
+ 'sql-primary-key' => $match->primary_column,
361
+ ),
362
+ admin_url( $this->path_to_use )
363
+ );
364
+ }
365
+ }
366
+
367
+ new Search();
includes/Extension/SQL/sql.php ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Plugin Name: String Locator: SQL search module
4
+ */
5
+
6
+ namespace StringLocator\Extension\SQL;
7
+
8
+ function validate_sql_fields( $field ) {
9
+ return preg_match( '/^[0-9a-zA-Z_]+$/s', $field );
10
+ }
11
+
12
+ require_once __DIR__ . '/class-search.php';
13
+ require_once __DIR__ . '/class-edit.php';
14
+ require_once __DIR__ . '/class-save.php';
15
+
16
+ require_once __DIR__ . '/Tests/class-serialized-data.php';
includes/Extension/SQL/views/editor/sql.php ADDED
@@ -0,0 +1,251 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+
3
+ namespace StringLocator;
4
+
5
+ if ( ! defined( 'ABSPATH' ) ) {
6
+ die();
7
+ }
8
+
9
+ global $wpdb;
10
+
11
+ $editor_content = '';
12
+
13
+ $this_url = add_query_arg(
14
+ array(
15
+ 'page' => 'string-locator',
16
+ ),
17
+ admin_url( ( is_multisite() ? 'network/admin.php' : 'tools.php' ) )
18
+ );
19
+
20
+ if ( 'int' === $_GET['sql-primary-type'] ) {
21
+ $row = $wpdb->get_row(
22
+ $wpdb->prepare(
23
+ 'SELECT * FROM ' . $_GET['sql-table'] . ' WHERE ' . $_GET['sql-primary-column'] . ' = %d LIMIT 1', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is not possible to prepare a table or column name, but these are instead validated in `/includes/Search/class-sql.php` before reaching this point.
24
+ $_GET['sql-primary-key']
25
+ )
26
+ );
27
+ } else {
28
+ $row = $wpdb->get_row(
29
+ $wpdb->prepare(
30
+ 'SELECT * FROM ' . $_GET['sql-table'] . ' WHERE ' . $_GET['sql-primary-column'] . ' = %s LIMIT 1', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is not possible to prepare a table or column name, but these are instead validated in `/includes/Search/class-sql.php` before reaching this point.
31
+ $_GET['sql-primary-key']
32
+ )
33
+ );
34
+ }
35
+
36
+ $format = 'string';
37
+
38
+ if ( is_serialized( $row->{ $_GET['sql-column'] }, true ) ) {
39
+ $format = 'serialized';
40
+ }
41
+
42
+ $editor_content = $row->{ $_GET['sql-column'] };
43
+ ?>
44
+ <form id="string-locator-edit-form" class="string-locator-editor-wrapper">
45
+ <?php wp_nonce_field( 'wp_rest' ); ?>
46
+
47
+ <h1 class="screen-reader-text">
48
+ <?php
49
+ /* translators: Title on the editor page. */
50
+ esc_html_e( 'String Locator - SQL Editor', 'string-locator' );
51
+ ?>
52
+ </h1>
53
+
54
+ <?php String_Locator::edit_form_fields( true ); ?>
55
+
56
+ <div class="string-locator-header">
57
+ <div>
58
+ <span class="title">
59
+ <?php
60
+ printf(
61
+ // translators: %s: The name of the database column being edited.
62
+ __( 'You are currently editing a database entry from <em>%s</em>', 'string-locator' ),
63
+ esc_html( $_GET['file-reference'] )
64
+ );
65
+ ?>
66
+ </span>
67
+ </div>
68
+
69
+ <div>
70
+ <a href="<?php echo esc_url( $this_url . '&restore=true' ); ?>" class="button button-default"><?php esc_html_e( 'Return to search results', 'string-locator' ); ?></a>
71
+ <button type="submit" class="button button-primary"><?php esc_html_e( 'Save changes', 'string-locator' ); ?></button>
72
+ </div>
73
+ </div>
74
+
75
+ <div class="string-locator-editor">
76
+ <div id="string-locator-notices">
77
+ <div class="row notice notice-error inline below-h2 hide-if-js">
78
+ <p>
79
+ <?php esc_html_e( 'The editor requires Javascript to be enabled before it can be used.', 'string-locator' ); ?>
80
+ </p>
81
+ </div>
82
+
83
+ <div class="row notice notice-warning inline below-h2 is-dismissible <?php echo ( 'serialized' === $format ? 'notice-error' : '' ); ?>">
84
+ <p>
85
+ <strong><?php esc_html_e( 'Warning:', 'string-locator' ); ?></strong> <?php esc_html_e( 'You are directly editing a database entry.', 'string-locator' ); ?>
86
+ </p>
87
+ <p>
88
+ <?php _e( 'You are about to make modifications directly to the database. Even if you are familiar with such changes, it is strongly recommended you keep a backup, and to perform such changes on a <a href="https://wordpress.org/support/article/running-a-development-copy-of-wordpress/">staging site</a> first.', 'string-locator' ); ?>
89
+ </p>
90
+
91
+ <?php if ( 'serialized' === $format ) : ?>
92
+ <p>
93
+ <strong>
94
+ <?php _e( 'This data is serialized, that means extra care must be taken as string lengths, and special characters must be accurately representative of the various field descriptors also found in the database entry.', 'string-locator' ); ?>
95
+ </strong>
96
+ </p>
97
+ <?php endif; ?>
98
+ </div>
99
+ </div>
100
+
101
+ <textarea
102
+ name="string-locator-editor-content"
103
+ class="string-locator-editor hide-if-no-js"
104
+ id="code-editor"
105
+ autofocus="autofocus"
106
+ ><?php echo esc_html( $editor_content ); ?></textarea>
107
+ </div>
108
+
109
+ <div class="string-locator-sidebar">
110
+
111
+ <?php do_action( 'string_locator_editor_sidebar_start' ); ?>
112
+
113
+ <div class="string-locator-panel">
114
+ <h2 class="title"><?php esc_html_e( 'Details', 'string-locator' ); ?></h2>
115
+ <div class="string-locator-panel-body">
116
+ <div class="row">
117
+ <?php
118
+ printf(
119
+ // translators: 1: Table name being edited.
120
+ esc_html__( 'Database table: %s', 'string-locator' ),
121
+ esc_html( $_GET['sql-table'] )
122
+ );
123
+ ?>
124
+ </div>
125
+
126
+ <div class="row">
127
+ <?php
128
+ printf(
129
+ // translators: 1: Column name being edited.
130
+ esc_html__( 'Database Column: %s', 'string-locator' ),
131
+ esc_html( $_GET['sql-column'] )
132
+ );
133
+ ?>
134
+ </div>
135
+
136
+ <div class="row">
137
+ <?php
138
+ printf(
139
+ // translators: 1: Primary database column name. 2: Primary database column key.
140
+ esc_html__( 'Primary column and key: %1$s:%2$s', 'string-locator' ),
141
+ esc_html( $_GET['sql-primary-column'] ),
142
+ esc_html( $_GET['sql-primary-key'] )
143
+ );
144
+ ?>
145
+ </div>
146
+ </div>
147
+ </div>
148
+
149
+ <?php do_action( 'string_locator_editor_sidebar_before_checks' ); ?>
150
+
151
+ <div class="string-locator-panel">
152
+ <h2 class="title"><?php esc_html_e( 'Save checks', 'string-locator' ); ?></h2>
153
+ <div class="string-locator-panel-body">
154
+ <?php do_action( 'string_locator_editor_checks' ); ?>
155
+ </div>
156
+ </div>
157
+
158
+ <?php do_action( 'string_locator_editor_sidebar_after_checks' ); ?>
159
+
160
+ <div class="string-locator-panel">
161
+ <h2 class="title"><?php esc_html_e( 'Database entry context', 'string-locator' ); ?></h2>
162
+ <div class="string-locator-panel-body">
163
+
164
+ <?php
165
+ foreach ( $row as $key => $value ) {
166
+ // Do not output the currently edited column as a context relationship.
167
+ if ( $_GET['sql-column'] === $key ) {
168
+ continue;
169
+ }
170
+ ?>
171
+
172
+ <div class="row">
173
+ <?php echo esc_html( $key ); ?>:
174
+ <br />
175
+ <span class="string-locator-italics">
176
+ <?php echo esc_html( $value ); ?>
177
+ </span>
178
+ </div>
179
+
180
+ <?php
181
+ }
182
+ ?>
183
+ </div>
184
+ </div>
185
+
186
+ <?php
187
+ $function_info = get_defined_functions();
188
+ $function_help = '';
189
+
190
+ foreach ( $function_info['user'] as $user_func ) {
191
+ if ( strstr( $editor_content, $user_func . '(' ) ) {
192
+ $function_object = new \ReflectionFunction( $user_func );
193
+ $attrs = $function_object->getParameters();
194
+
195
+ $attr_strings = array();
196
+
197
+ foreach ( $attrs as $attr ) {
198
+ $arg = '';
199
+
200
+ if ( $attr->isPassedByReference() ) {
201
+ $arg .= '&';
202
+ }
203
+
204
+ if ( $attr->isOptional() ) {
205
+ $arg = sprintf(
206
+ '[ %s$%s ]',
207
+ $arg,
208
+ $attr->getName()
209
+ );
210
+ } else {
211
+ $arg = sprintf(
212
+ '%s$%s',
213
+ $arg,
214
+ $attr->getName()
215
+ );
216
+ }
217
+
218
+ $attr_strings[] = $arg;
219
+ }
220
+
221
+ $function_help .= sprintf(
222
+ '<div class="row"><a href="%s" target="_blank">%s</a></div>',
223
+ esc_url( sprintf( 'https://developer.wordpress.org/reference/functions/%s/', $user_func ) ),
224
+ $user_func . '( ' . implode( ', ', $attr_strings ) . ' )'
225
+ );
226
+ }
227
+ }
228
+ ?>
229
+
230
+ <?php if ( ! empty( $function_help ) ) : ?>
231
+
232
+ <div class="string-locator-panel">
233
+ <h2 class="title"><?php esc_html_e( 'WordPress functions', 'string-locator' ); ?></h2>
234
+ <div class="string-locator-panel-body">
235
+ <?php echo $function_help; ?>
236
+ </div>
237
+ <?php endif; ?>
238
+
239
+ <?php do_action( 'string_locator_editor_sidebar_end' ); ?>
240
+
241
+ </div>
242
+
243
+ </div>
244
+
245
+ <script id="tmpl-string-locator-alert" type="text/template">
246
+ <div class="row notice notice-{{ data.type }} inline below-h2 is-dismissible">
247
+ {{{ data.message }}}
248
+
249
+ <button type="button" class="notice-dismiss" onclick="this.closest( '.notice' ).remove()"><span class="screen-reader-text"><?php esc_html_e( 'Dismiss this notice.', 'string-locator' ); ?></span></button>
250
+ </div>
251
+ </script>
includes/Extension/SQL/views/template/search.php ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ die();
4
+ }
5
+ ?>
6
+ <script id="tmpl-string-locator-search-result-sql" type="text/template">
7
+ <tr data-type="sql" data-primary-key="{{ data.primary_key }}" data-primary-column="{{ data.primary_column }}" data-primary-type="{{ data.primary_type }}" data-table-name="{{ data.table }}" data-column-name="{{ data.column }}">
8
+ <th scope="row" class="check-column">
9
+ <input type="checkbox" name="string-locator-replace-checked[]" class="check-column-box">
10
+ </th>
11
+ <td>
12
+ {{{ data.stringresult }}}
13
+
14
+ <div class="row-actions">
15
+ <# if ( data.editurl ) { #>
16
+ <span class="edit">
17
+ <a href="{{ data.editurl }}" aria-label="<?php esc_attr_e( 'Edit', 'string-locator' ); ?>">
18
+ <?php esc_html_e( 'Edit', 'string-locator' ); ?>
19
+ </a>
20
+ </span>
21
+ <# } #>
22
+ </div>
23
+ </td>
24
+ <td>
25
+ <# if ( data.editurl ) { #>
26
+ <a href="{{ data.editurl }}">
27
+ {{ data.filename }}
28
+ </a>
29
+ <# } #>
30
+ <# if ( ! data.editurl ) { #>
31
+ {{ data.filename }}
32
+ <# } #>
33
+ </td>
34
+ <td>
35
+ {{ data.primary_key }}
36
+ </td>
37
+ <td>
38
+ {{ data.linepos }}
39
+ </td>
40
+ </tr>
41
+ </script>
includes/Extension/SearchReplace/REST/class-replace.php ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * REST API endpoints for the replacement module.
4
+ */
5
+
6
+ namespace StringLocator\Extension\SearchReplace\REST;
7
+
8
+ use StringLocator\Base\REST;
9
+ use StringLocator\Extension\SearchReplace\Replace\File;
10
+ use StringLocator\Extension\SearchReplace\Replace\SQL;
11
+
12
+ /**
13
+ * Replace class.
14
+ */
15
+ class Replace extends REST {
16
+
17
+ protected $rest_base = 'replace';
18
+
19
+ /**
20
+ * Class constructor.
21
+ */
22
+ public function __construct() {
23
+ parent::__construct();
24
+ }
25
+
26
+ /**
27
+ * Register custom REST API endpoints.
28
+ *
29
+ * @return void
30
+ */
31
+ public function register_rest_route() {
32
+ register_rest_route(
33
+ $this->namespace,
34
+ $this->rest_base,
35
+ array(
36
+ 'methods' => 'POST',
37
+ 'callback' => array( $this, 'replace' ),
38
+ 'permission_callback' => array( $this, 'permission_callback' ),
39
+ )
40
+ );
41
+ }
42
+
43
+ /**
44
+ * REST API handler for the replacement endpoint.
45
+ *
46
+ * @param \WP_REST_Request $request A dynamic request object depending on the data type being replaced.
47
+ *
48
+ * @return \WP_Error|\WP_REST_Response
49
+ */
50
+ public function replace( \WP_REST_Request $request ) {
51
+ $replace_nonce = $request->get_param( 'replace_nonce' );
52
+
53
+ if ( ! $replace_nonce || ! wp_verify_nonce( $replace_nonce, 'string-locator-replace' ) ) {
54
+ return new \WP_Error( 'invalid_nonce', __( 'Invalid nonce.', 'string-locator' ), array( 'status' => 400 ) );
55
+ }
56
+
57
+ // Ensure the regex flag is a boolean value.
58
+ $is_regex = $request->get_param( 'search_regex' );
59
+ if ( ! is_bool( $is_regex ) ) {
60
+ if ( 'false' === $is_regex ) {
61
+ $is_regex = false;
62
+ } else {
63
+ $is_regex = true;
64
+ }
65
+ }
66
+
67
+ $check_loopback = $request->get_param( 'replace_loopback' );
68
+ if ( ! is_bool( $check_loopback ) ) {
69
+ if ( 'false' === $check_loopback ) {
70
+ $check_loopback = false;
71
+ } else {
72
+ $check_loopback = true;
73
+ }
74
+ }
75
+
76
+ switch ( $request->get_param( 'type' ) ) {
77
+ case 'sql':
78
+ $handler = new SQL(
79
+ $request->get_param( 'primaryColumn' ),
80
+ $request->get_param( 'primaryKey' ),
81
+ $request->get_param( 'primaryType' ),
82
+ $request->get_param( 'tableName' ),
83
+ $request->get_param( 'columnName' ),
84
+ $request->get_param( 'search_string' ),
85
+ $request->get_param( 'replace_string' ),
86
+ $is_regex
87
+ );
88
+ break;
89
+ case 'file':
90
+ default:
91
+ $handler = new File(
92
+ $request->get_param( 'filename' ),
93
+ $request->get_param( 'linenum' ),
94
+ $request->get_param( 'search_string' ),
95
+ $request->get_param( 'replace_string' ),
96
+ $is_regex
97
+ );
98
+ }
99
+
100
+ if ( ! $handler->validate() ) {
101
+ return new \WP_Error( 'invalid_request', __( 'Invalid request', 'string-locator' ), array( 'status' => 400 ) );
102
+ }
103
+
104
+ $replace = $handler->replace();
105
+
106
+ if ( is_wp_error( $replace ) ) {
107
+ return $replace;
108
+ }
109
+
110
+ // Basic check to ensure the site is not broken after the modifications.
111
+ if ( $check_loopback ) {
112
+ $urls = array(
113
+ get_site_url( null, '/' ),
114
+ get_admin_url( null, '/' ),
115
+ );
116
+
117
+ foreach ( $urls as $url ) {
118
+ $loopback = wp_remote_head( $url );
119
+
120
+ if ( is_wp_error( $loopback ) ) {
121
+ $handler->restore();
122
+
123
+ return new \WP_Error(
124
+ 'loopback_failed',
125
+ sprintf(
126
+ // translators: 1: The URL being requested.
127
+ __( 'The address `%s` could not be loaded after the edits were made, and the changes were therefore reverted.', 'string-locator' ),
128
+ esc_html( $url )
129
+ ),
130
+ array(
131
+ 'status' => 400,
132
+ )
133
+ );
134
+ }
135
+
136
+ $response_code = wp_remote_retrieve_response_code( $loopback );
137
+ if ( (int) substr( $response_code, 0, 1 ) > 3 ) {
138
+ $handler->restore();
139
+
140
+ return new \WP_Error(
141
+ 'loopback_failed',
142
+ sprintf(
143
+ // translators: 1: The URL being requested. 2: The HTTP status code returned.
144
+ __( 'The address `%1$s` returned an error code (%2$s), and the changes were therefore reverted.', 'string-locator' ),
145
+ esc_html( $url ),
146
+ esc_html( $response_code )
147
+ ),
148
+ array(
149
+ 'status' => 400,
150
+ )
151
+ );
152
+ }
153
+ }
154
+ }
155
+
156
+ /*
157
+ * A `true` response means no errors occurred, but also no replacements/updated were made.
158
+ */
159
+ if ( true !== $replace ) {
160
+ $string_preview = sprintf(
161
+ '%s<div class="row-actions"><span class="edit"><a href="%s">%s</a></span></div>',
162
+ $replace,
163
+ $handler->get_edit_url(),
164
+ esc_html__( 'Edit', 'string-locator' )
165
+ );
166
+ } else {
167
+ $string_preview = true;
168
+ }
169
+
170
+ return new \WP_REST_Response(
171
+ array(
172
+ 'success' => true,
173
+ 'data' => array(
174
+ 'replace_string' => $string_preview,
175
+ ),
176
+ ),
177
+ 200
178
+ );
179
+ }
180
+ }
181
+
182
+ new Replace();
includes/Extension/SearchReplace/Replace/class-file.php ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Class for handling replacements in files.
4
+ */
5
+
6
+ namespace StringLocator\Extension\SearchReplace\Replace;
7
+
8
+ use StringLocator\Search;
9
+ use StringLocator\String_Locator;
10
+
11
+ /**
12
+ * File class.
13
+ */
14
+ class File {
15
+
16
+ private $file;
17
+ private $line;
18
+ private $regex;
19
+ private $new_string;
20
+ private $old_string;
21
+ private $original_string;
22
+
23
+ private $search;
24
+
25
+ /**
26
+ * Class constructor.
27
+ *
28
+ * @param string $file The relative path from the WordPress root to the file being edited.
29
+ * @param int $line The line in the file containing the string to be replaced.
30
+ * @param string $old_string The string to be replaced.
31
+ * @param string $new_string The string to be added.
32
+ * @param bool $regex Is the search string a regex string.
33
+ */
34
+ public function __construct( $file, $line, $old_string, $new_string, $regex = false ) {
35
+ $this->file = trailingslashit( ABSPATH ) . $file;
36
+ $this->line = ( absint( $line ) - 1 );
37
+ $this->regex = $regex;
38
+ $this->old_string = $old_string;
39
+ $this->new_string = $new_string;
40
+
41
+ $this->search = new Search();
42
+ }
43
+
44
+ /**
45
+ * Validate that the file is in a location that does not allow directory traversals.
46
+ *
47
+ * @return bool
48
+ */
49
+ public function validate() {
50
+ return String_Locator::is_valid_location( $this->file );
51
+ }
52
+
53
+ /**
54
+ * Run the replacement function.
55
+ *
56
+ * @return bool|string|\WP_Error
57
+ */
58
+ public function replace() {
59
+ // A value of 0 or lower indicates a filename or similar matched, and these should NOT be replaced.
60
+ if ( $this->line < 0 ) {
61
+ return true;
62
+ }
63
+
64
+ $file_contents = file( $this->file );
65
+
66
+ if ( ! $file_contents ) {
67
+ return new \WP_Error( 'file_inaccessible', __( 'The file could not be read.', 'string-locator' ), array( 'status' => 400 ) );
68
+ }
69
+
70
+ $this->original_string = $file_contents[ $this->line ];
71
+
72
+ if ( $this->regex ) {
73
+ $file_contents[ $this->line ] = preg_replace( $this->old_string, $this->new_string, $file_contents[ $this->line ] );
74
+ } else {
75
+ $file_contents[ $this->line ] = str_ireplace( $this->old_string, $this->new_string, $file_contents[ $this->line ] );
76
+ }
77
+
78
+ $file = fopen( $this->file, 'w' );
79
+
80
+ if ( ! $file ) {
81
+ return new \WP_Error( 'file_inaccessible', __( 'The file could not be written.', 'string-locator' ), array( 'status' => 400 ) );
82
+ }
83
+
84
+ foreach ( $file_contents as $file_line ) {
85
+ fwrite( $file, $file_line );
86
+ }
87
+
88
+ fclose( $file );
89
+
90
+ return String_Locator::create_preview( $file_contents[ $this->line ], $this->new_string, $this->regex );
91
+ }
92
+
93
+ /**
94
+ * Restore the last ran modification.
95
+ *
96
+ * @return bool
97
+ */
98
+ public function restore() {
99
+ $file_contents = file( $this->file );
100
+
101
+ $file_contents[ $this->line ] = $this->original_string;
102
+
103
+ $file = fopen( $this->file, 'w' );
104
+
105
+ foreach ( $file_contents as $file_line ) {
106
+ fwrite( $file, $file_line );
107
+ }
108
+
109
+ fclose( $file );
110
+
111
+ return true;
112
+ }
113
+
114
+ /**
115
+ * Return the URL for the editor interface for this file.
116
+ *
117
+ * @return string
118
+ */
119
+ public function get_edit_url() {
120
+ return $this->search->create_edit_link( $this->file, $this->line, 0 );
121
+ }
122
+ }
includes/Extension/SearchReplace/Replace/class-sql.php ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Handle replacements in SQL strings.
4
+ *
5
+ * This file borrows and adapts functions and concepts from the
6
+ * interconnect.it Search-Replace script (https://github.com/interconnectit/Search-Replace-DB).
7
+ */
8
+
9
+ namespace StringLocator\Extension\SearchReplace\Replace;
10
+
11
+ use StringLocator\Extension\SQL\Search;
12
+ use StringLocator\String_Locator;
13
+ use function StringLocator\Extension\SQL\validate_sql_fields;
14
+
15
+ /**
16
+ * SQL class.
17
+ */
18
+ class SQL {
19
+
20
+ private $table_name;
21
+ private $primary_column;
22
+ private $primary_key;
23
+ private $primary_type;
24
+ private $column_name;
25
+ private $regex;
26
+ private $new_string;
27
+ private $old_string;
28
+ private $original_string;
29
+
30
+ private $search;
31
+
32
+ /**
33
+ * Class constructor.
34
+ *
35
+ * @param string $primary_column The name of the primary column of the entry being edited.
36
+ * @param string $primary_key The key identified of the primary column.
37
+ * @param string $primary_type The type of the primary column (`int` or `string`).
38
+ * @param string $table_name The name of the table to perform an edit within.
39
+ * @param string $column_name The column being edited.
40
+ * @param string $old_string The string to be replaced.
41
+ * @param string $new_string The string to be added.
42
+ * @param bool $regex Is the search string a regex string.
43
+ */
44
+ public function __construct( $primary_column, $primary_key, $primary_type, $table_name, $column_name, $old_string, $new_string, $regex = false ) {
45
+ $this->primary_column = $primary_column;
46
+ $this->primary_key = $primary_key;
47
+ $this->primary_type = $primary_type;
48
+ $this->table_name = $table_name;
49
+ $this->column_name = $column_name;
50
+ $this->regex = $regex;
51
+ $this->old_string = $old_string;
52
+ $this->new_string = $new_string;
53
+
54
+ $this->search = new Search();
55
+ }
56
+
57
+ /**
58
+ * Validate that the only non-escaped strings are alpha-numeric to avoid SQL injections.
59
+ *
60
+ * @return bool
61
+ */
62
+ public function validate() {
63
+ if ( ! validate_sql_fields( $this->primary_column ) ) {
64
+ return false;
65
+ }
66
+ if ( ! validate_sql_fields( $this->table_name ) ) {
67
+ return false;
68
+ }
69
+ if ( ! validate_sql_fields( $this->column_name ) ) {
70
+ return false;
71
+ }
72
+
73
+ return true;
74
+ }
75
+
76
+ /**
77
+ * Run the replacement function.
78
+ *
79
+ * @return bool|string|\WP_Error
80
+ */
81
+ public function replace() {
82
+ global $wpdb;
83
+
84
+ if ( 'int' === $this->primary_type ) {
85
+ $this->original_string = $wpdb->get_var(
86
+ $wpdb->prepare(
87
+ 'SELECT ' . $this->column_name . ' FROM ' . $this->table_name . ' WHERE ' . $this->primary_column . ' = %d LIMIT 1', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is not possible to prepare a table or column name, but these are instead validated in `/includes/Search/class-sql.php` before reaching this point.
88
+ $this->primary_key
89
+ )
90
+ );
91
+ } else {
92
+ $this->original_string = $wpdb->get_var(
93
+ $wpdb->prepare(
94
+ 'SELECT ' . $this->column_name . ' FROM ' . $this->table_name . ' WHERE ' . $this->primary_column . ' = %s LIMIT 1', // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared -- It is not possible to prepare a table or column name, but these are instead validated in `/includes/Search/class-sql.php` before reaching this point.
95
+ $this->primary_key
96
+ )
97
+ );
98
+ }
99
+
100
+ $replaced_line = $this->recursive_unserialize_replace( $this->old_string, $this->new_string, $this->original_string );
101
+
102
+ $updated = $wpdb->update(
103
+ $this->table_name,
104
+ array(
105
+ $this->column_name => $replaced_line,
106
+ ),
107
+ array(
108
+ $this->primary_column => $this->primary_key,
109
+ )
110
+ );
111
+
112
+ if ( ! $updated ) {
113
+ /*
114
+ * Cause an error to be thrown if updates fail due ot the query.
115
+ *
116
+ * This also checks that `$wpdb->last_error` is empty before treating
117
+ * the result as an error, this is because `$wpdb->update` will return
118
+ * a `false` value if it did not perform an update, for example when
119
+ * a string is identical. This may be the case where a class name or
120
+ * object is encountered, which can not be replaced.
121
+ */
122
+ if ( empty( $wpdb->last_error ) ) {
123
+ return true;
124
+ }
125
+
126
+ return new \WP_Error( 'search_replace_sql_error', __( 'Error updating the database.', 'search-replace' ) );
127
+ }
128
+
129
+ return String_Locator::create_preview( $replaced_line, $this->new_string, $this->regex );
130
+ }
131
+
132
+ /**
133
+ * Restore the last ran modification.
134
+ *
135
+ * @return bool
136
+ */
137
+ public function restore() {
138
+ global $wpdb;
139
+
140
+ $wpdb->update(
141
+ $this->table_name,
142
+ array(
143
+ $this->column_name => $this->original_string,
144
+ ),
145
+ array(
146
+ $this->primary_column => $this->primary_key,
147
+ )
148
+ );
149
+ return true;
150
+ }
151
+
152
+ public function get_edit_url() {
153
+ return $this->search->create_edit_link( $this->table_name, $this->column_name, $this->primary_column, $this->primary_type, (object) array( 'primary_column' => $this->primary_key ) );
154
+ }
155
+
156
+ /**
157
+ * Take a serialised array and unserialise it replacing elements as needed and
158
+ * unserialising any subordinate arrays and performing the replacement on those too.
159
+ *
160
+ * @param string $from String we're looking to replace.
161
+ * @param string $to What we want it to be replaced with
162
+ * @param array $data Used to pass any subordinate arrays back to in.
163
+ * @param bool $serialised Does the array passed via $data need serialising.
164
+ *
165
+ * @return array|string The original array with all elements replaced as needed.
166
+ */
167
+ public function recursive_unserialize_replace( $from = '', $to = '', $data = '', $serialised = false ) {
168
+ // Some unserialised data cannot be re-serialised eg. SimpleXMLElements.
169
+ try {
170
+ $unserialized = @unserialize( $data );
171
+ if ( is_string( $data ) && false !== $unserialized ) {
172
+ $data = $this->recursive_unserialize_replace( $from, $to, $unserialized, true );
173
+ } elseif ( is_array( $data ) ) {
174
+ $_tmp = array();
175
+ foreach ( $data as $key => $value ) {
176
+ $_tmp[ $key ] = $this->recursive_unserialize_replace( $from, $to, $value, false );
177
+ }
178
+
179
+ $data = $_tmp;
180
+ unset( $_tmp );
181
+ } elseif ( is_object( $data ) && ! is_a( $data, '__PHP_Incomplete_Class' ) ) {
182
+ $_tmp = $data;
183
+ $props = get_object_vars( $data );
184
+ foreach ( $props as $key => $value ) {
185
+ $_tmp->$key = $this->recursive_unserialize_replace( $from, $to, $value, false );
186
+ }
187
+
188
+ $data = $_tmp;
189
+ unset( $_tmp );
190
+ } else {
191
+ if ( is_string( $data ) ) {
192
+ $data = $this->str_replace( $from, $to, $data );
193
+ }
194
+ }
195
+
196
+ if ( $serialised ) {
197
+ return serialize( $data );
198
+ }
199
+ } catch ( \Exception $error ) {
200
+ }
201
+
202
+ return $data;
203
+ }
204
+
205
+ /**
206
+ * Wrapper for regex/non regex search & replace
207
+ *
208
+ * @param string $search
209
+ * @param string $replace
210
+ * @param string $string
211
+ * @param int $count
212
+ *
213
+ * @return string
214
+ */
215
+ public function str_replace( $search, $replace, $string, &$count = 0 ) {
216
+ if ( $this->regex ) {
217
+ return preg_replace( $search, $replace, $string, - 1, $count );
218
+ } elseif ( function_exists( 'mb_split' ) ) {
219
+ return $this->mb_str_replace( $search, $replace, $string, $count );
220
+ } else {
221
+ return str_ireplace( $search, $replace, $string, $count );
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Replace all occurrences of the search string with the replacement string.
227
+ *
228
+ * @param mixed $search
229
+ * @param mixed $replace
230
+ * @param mixed $subject
231
+ * @param int $count
232
+ *
233
+ * @return mixed
234
+ * @copyright Copyright 2012 Sean Murphy. All rights reserved.
235
+ * @license http://creativecommons.org/publicdomain/zero/1.0/
236
+ * @link http://php.net/manual/function.str-replace.php
237
+ *
238
+ * @author Sean Murphy <sean@iamseanmurphy.com>
239
+ */
240
+ public function mb_str_replace( $search, $replace, $subject, &$count = 0 ) {
241
+ if ( ! is_array( $subject ) ) {
242
+ // Normalize $search and $replace so they are both arrays of the same length
243
+ $searches = is_array( $search ) ? array_values( $search ) : array( $search );
244
+ $replacements = is_array( $replace ) ? array_values( $replace ) : array( $replace );
245
+ $replacements = array_pad( $replacements, count( $searches ), '' );
246
+
247
+ foreach ( $searches as $key => $search ) {
248
+ $parts = mb_split( preg_quote( $search ), $subject );
249
+ if ( ! is_array( $parts ) ) {
250
+ continue;
251
+ }
252
+ $count += count( $parts ) - 1;
253
+ $subject = implode( $replacements[ $key ], $parts );
254
+ }
255
+ } else {
256
+ // Call mb_str_replace for each subject in array, recursively
257
+ foreach ( $subject as $key => $value ) {
258
+ $subject[ $key ] = $this->mb_str_replace( $search, $replace, $value, $count );
259
+ }
260
+ }
261
+
262
+ return $subject;
263
+ }
264
+ }
includes/Extension/SearchReplace/class-replace.php ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Base class for the Replace feature.
4
+ */
5
+
6
+ namespace StringLocator\Extension\SearchReplace;
7
+
8
+ /**
9
+ * Replace class.
10
+ */
11
+ class Replace {
12
+
13
+ /**
14
+ * Class constructor.
15
+ */
16
+ public function __construct() {
17
+ add_action( 'string_locator_search_results_tablenav_controls', array( $this, 'add_replace_button' ) );
18
+ add_action( 'string_locator_search_results_tablenav_controls', array( $this, 'output_replace_form' ) );
19
+
20
+ add_action( 'admin_enqueue_scripts', array( $this, 'maybe_enqueue_assets' ) );
21
+
22
+ add_action( 'string_locator_search_templates', array( $this, 'add_replace_response_template' ) );
23
+ }
24
+
25
+ /**
26
+ * Add error notice template.
27
+ *
28
+ * @return void
29
+ */
30
+ public function add_replace_response_template() {
31
+ require_once STRING_LOCATOR_PLUGIN_DIR . '/includes/Extension/SearchReplace/template/error-notice.php';
32
+ }
33
+
34
+ /**
35
+ * Conditionally register assets on the appropriate pages within wp-admin.
36
+ *
37
+ * @param string $hook The hook name for the page being loaded.
38
+ *
39
+ * @return void
40
+ */
41
+ public function maybe_enqueue_assets( $hook ) {
42
+ // Break out early if we are not on a String Locator page
43
+ if ( 'tools_page_string-locator' !== $hook && 'toplevel_page_string-locator' !== $hook ) {
44
+ return;
45
+ }
46
+
47
+ $replace = STRING_LOCATOR_PLUGIN_DIR . 'build/string-locator-replace.asset.php';
48
+
49
+ $replace = file_exists( $replace ) ? require $replace : array( 'version' => false );
50
+
51
+ /**
52
+ * String Locator Styles and Scripts.
53
+ */
54
+ wp_enqueue_style( 'string-locator-replace', trailingslashit( STRING_LOCATOR_PLUGIN_URL ) . 'build/string-locator-replace.css', array(), $replace['version'] );
55
+ wp_enqueue_script( 'string-locator-replace', trailingslashit( STRING_LOCATOR_PLUGIN_URL ) . 'build/string-locator-replace.js', array(), $replace['version'], true );
56
+
57
+ wp_localize_script(
58
+ 'string-locator-replace',
59
+ 'stringLocatorReplace',
60
+ array(
61
+ 'rest_nonce' => wp_create_nonce( 'wp_rest' ),
62
+ 'replace_nonce' => wp_create_nonce( 'string-locator-replace' ),
63
+ 'url' => array(
64
+ 'replace' => get_rest_url( null, 'string-locator/v1/replace' ),
65
+ ),
66
+ 'string' => array(
67
+ 'replace_started' => __( 'Running replacemenets...', 'string-locator' ),
68
+ 'button_show' => __( 'Show replacement controls', 'string-locator' ),
69
+ 'button_hide' => __( 'Hide replacement controls', 'string-locator' ),
70
+ 'confirm_all' => __( 'Are you sure you want to replace all strings?', 'string-locator' ),
71
+ ),
72
+ )
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Output a toggle button to display the replacement form.
78
+ *
79
+ * @return void
80
+ */
81
+ public function add_replace_button() {
82
+ printf(
83
+ '<button type="button" class="button button-link" id="string-locator-toggle-replace-controls" aria-expanded="false" aria-controls="string-locator-replace-form">%s</button>',
84
+ esc_html__( 'Show replacement controls', 'string-locator' )
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Output the replacement form.
90
+ *
91
+ * @return void
92
+ */
93
+ public function output_replace_form() {
94
+ include_once __DIR__ . '/views/replace-form.php';
95
+ }
96
+
97
+ }
98
+
99
+ new Replace();
includes/Extension/SearchReplace/search-replace.php ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Plugin Name: String Locator: Replacement module
4
+ */
5
+
6
+ namespace StringLocator\Extension\SearchReplace;
7
+
8
+ // Primary extension class.
9
+ require_once __DIR__ . '/class-replace.php';
10
+
11
+ // Replacement handlers.
12
+ require_once __DIR__ . '/Replace/class-file.php';
13
+ require_once __DIR__ . '/Replace/class-sql.php';
14
+
15
+ // REST Endpoints.
16
+ require_once __DIR__ . '/REST/class-replace.php';
includes/Extension/SearchReplace/template/error-notice.php ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Template for notices returned form the REST API when performing a replacement.
4
+ */
5
+
6
+ if ( ! defined( 'ABSPATH' ) ) {
7
+ die();
8
+ }
9
+ ?>
10
+
11
+ <script id="tmpl-string-locator-replace-error" type="text/template">
12
+ <div class="notice notice-error">
13
+ <p>
14
+ {{ data.message }}
15
+ </p>
16
+ </div>
17
+ </script>
includes/Extension/SearchReplace/views/replace-form.php ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ /**
3
+ * Form output for the replace feature.
4
+ */
5
+
6
+ namespace StringLocator;
7
+
8
+ if ( ! defined( 'ABSPATH' ) ) {
9
+ die();
10
+ }
11
+
12
+ $this_url = admin_url( ( is_multisite() ? 'network/admin.php' : 'tools.php' ) . '?page=string-locator' );
13
+ ?>
14
+
15
+ <div id="string-locator-replace-form">
16
+ <h2><?php esc_html_e( 'Replace in results', 'string-locator' ); ?></h2>
17
+
18
+ <form action="<?php echo esc_url( $this_url ); ?>" method="post">
19
+ <p>
20
+ <label for="string-locator-replace-new-string"><?php esc_html_e( 'New string', 'string-locator' ); ?></label>
21
+ <input type="text" id="string-locator-replace-new-string" name="string-locator-replace-new-string">
22
+ </p>
23
+
24
+ <p>
25
+ <label>
26
+ <input type="checkbox" name="string-locator-replace-loopback-check" id="string-locator-replace-loopback-check" checked="checked">
27
+ <?php esc_html_e( 'Perform loopback check', 'string-locator' ); ?>
28
+ </label>
29
+
30
+ <br />
31
+
32
+ <em>
33
+ <?php
34
+ // translators: The link to the WordPress.org article about loopbacks.
35
+ $url = __( 'https://wordpress.org/support/article/loopbacks/', 'string-locator' );
36
+
37
+ printf(
38
+ '<a href="%s" target="_blank">%s</a>',
39
+ esc_url( $url ),
40
+ esc_html__( 'Read more about loopbacks on WordPress.org', 'string-locator' )
41
+ );
42
+ ?>
43
+ </em>
44
+ </p>
45
+
46
+ <p>
47
+ <button type="button" class="button button-primary" id="string-locator-replace-button-all">
48
+ <?php esc_html_e( 'Replace all strings', 'string-locator' ); ?>
49
+ </button>
50
+ <button type="button" class="button button-primary" id="string-locator-replace-button-selected">
51
+ <?php esc_html_e( 'Replace selected strings', 'string-locator' ); ?>
52
+ </button>
53
+ </p>
54
+ </form>
55
+ </div>
includes/REST/class-base.php DELETED
@@ -1,17 +0,0 @@
1
- <?php
2
-
3
- namespace JITS\StringLocator\REST;
4
-
5
- class Base extends \WP_REST_Controller {
6
-
7
- protected $namespace = 'string-locator/v1';
8
-
9
- public function __construct() {
10
- add_action( 'rest_api_init', array( $this, 'register_rest_route' ) );
11
- }
12
-
13
- public function permission_callback() {
14
- return current_user_can( 'edit_themes' );
15
- }
16
-
17
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
includes/REST/class-clean.php CHANGED
@@ -1,8 +1,10 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator\REST;
4
 
5
- class Clean extends Base {
 
 
6
 
7
  protected $rest_base = 'clean';
8
 
1
  <?php
2
 
3
+ namespace StringLocator\REST;
4
 
5
+ use StringLocator\Base\REST;
6
+
7
+ class Clean extends REST {
8
 
9
  protected $rest_base = 'clean';
10
 
includes/REST/class-directory-structure.php CHANGED
@@ -1,10 +1,11 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator\REST;
4
 
5
- use JITS\StringLocator\Directory_Iterator;
 
6
 
7
- class Directory_Structure extends Base {
8
 
9
  protected $rest_base = 'get-directory-structure';
10
 
@@ -25,8 +26,20 @@ class Directory_Structure extends Base {
25
  }
26
 
27
  public function get_structure( \WP_REST_Request $request ) {
 
 
 
 
 
 
 
 
 
 
 
 
28
  // Validate the search path to avoid unintended directory traversal.
29
- if ( 0 !== validate_file( $request->get_param( 'directory' ) ) ) {
30
  return new \WP_REST_Response(
31
  array(
32
  'success' => false,
@@ -37,9 +50,9 @@ class Directory_Structure extends Base {
37
  }
38
 
39
  $iterator = new Directory_Iterator(
40
- $request->get_param( 'directory' ),
41
- $request->get_param( 'search' ),
42
- $request->get_param( 'regex' )
43
  );
44
 
45
  return array(
1
  <?php
2
 
3
+ namespace StringLocator\REST;
4
 
5
+ use StringLocator\Base\REST;
6
+ use StringLocator\Directory_Iterator;
7
 
8
+ class Directory_Structure extends REST {
9
 
10
  protected $rest_base = 'get-directory-structure';
11
 
26
  }
27
 
28
  public function get_structure( \WP_REST_Request $request ) {
29
+ $short_circuit = apply_filters( 'string_locator_directory_iterator_short_circuit', array(), $request );
30
+
31
+ if ( ! empty( $short_circuit ) ) {
32
+ return $short_circuit;
33
+ }
34
+
35
+ $data = json_decode( $request->get_param( 'data' ) );
36
+
37
+ $directory = $data->directory;
38
+ $search = $data->search;
39
+ $regex = $data->regex;
40
+
41
  // Validate the search path to avoid unintended directory traversal.
42
+ if ( 0 !== validate_file( $directory ) ) {
43
  return new \WP_REST_Response(
44
  array(
45
  'success' => false,
50
  }
51
 
52
  $iterator = new Directory_Iterator(
53
+ $directory,
54
+ $search,
55
+ $regex
56
  );
57
 
58
  return array(
includes/REST/class-save.php CHANGED
@@ -1,8 +1,10 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator\REST;
4
 
5
- class Save extends Base {
 
 
6
 
7
  protected $rest_base = 'save';
8
 
@@ -23,7 +25,7 @@ class Save extends Base {
23
  }
24
 
25
  public function save( \WP_REST_Request $request ) {
26
- $handler = new \JITS\StringLocator\Save();
27
 
28
  /**
29
  * Filters the REST Request parameter values that will be used for the save call.
1
  <?php
2
 
3
+ namespace StringLocator\REST;
4
 
5
+ use StringLocator\Base\REST;
6
+
7
+ class Save extends REST {
8
 
9
  protected $rest_base = 'save';
10
 
25
  }
26
 
27
  public function save( \WP_REST_Request $request ) {
28
+ $handler = new \StringLocator\Save();
29
 
30
  /**
31
  * Filters the REST Request parameter values that will be used for the save call.
includes/REST/class-search.php CHANGED
@@ -1,8 +1,10 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator\REST;
4
 
5
- class Search extends Base {
 
 
6
 
7
  protected $rest_base = 'search';
8
 
@@ -23,14 +25,15 @@ class Search extends Base {
23
  }
24
 
25
  public function perform_search( \WP_REST_Request $request ) {
26
- $handler = new \JITS\StringLocator\Search();
27
 
28
  /**
29
  * Filter the search handler used to find strings.
30
  *
31
- * @attr object $handler The handler performing searches.
 
32
  */
33
- $handler = apply_filters( 'string_locator_search_handler', $handler );
34
 
35
  return array(
36
  'success' => true,
1
  <?php
2
 
3
+ namespace StringLocator\REST;
4
 
5
+ use StringLocator\Base\REST;
6
+
7
+ class Search extends REST {
8
 
9
  protected $rest_base = 'search';
10
 
25
  }
26
 
27
  public function perform_search( \WP_REST_Request $request ) {
28
+ $handler = new \StringLocator\Search();
29
 
30
  /**
31
  * Filter the search handler used to find strings.
32
  *
33
+ * @attr object $handler The handler performing searches.
34
+ * @attr \WP_REST_Request $request The request received by the REST API handler.
35
  */
36
+ $handler = apply_filters( 'string_locator_search_handler', $handler, $request );
37
 
38
  return array(
39
  'success' => true,
includes/Tests/class-loopback.php CHANGED
@@ -1,6 +1,6 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator\Tests;
4
 
5
  class Loopback {
6
  /**
1
  <?php
2
 
3
+ namespace StringLocator\Tests;
4
 
5
  class Loopback {
6
  /**
includes/Tests/class-smart-scan.php CHANGED
@@ -1,6 +1,6 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator\Tests;
4
 
5
  class Smart_Scan {
6
 
1
  <?php
2
 
3
+ namespace StringLocator\Tests;
4
 
5
  class Smart_Scan {
6
 
includes/class-directory-iterator.php CHANGED
@@ -1,6 +1,6 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator;
4
 
5
  class Directory_Iterator {
6
 
@@ -64,7 +64,7 @@ class Directory_Iterator {
64
 
65
  $store = (object) array(
66
  'scan_path' => $scan_path,
67
- 'search' => wp_unslash( $this->search ),
68
  'directory' => $this->directory,
69
  'chunks' => count( $file_chunks ),
70
  'regex' => $this->regex,
1
  <?php
2
 
3
+ namespace StringLocator;
4
 
5
  class Directory_Iterator {
6
 
64
 
65
  $store = (object) array(
66
  'scan_path' => $scan_path,
67
+ 'search' => $this->search,
68
  'directory' => $this->directory,
69
  'chunks' => count( $file_chunks ),
70
  'regex' => $this->regex,
includes/class-save.php CHANGED
@@ -1,9 +1,9 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator;
4
 
5
- use JITS\StringLocator\Tests\Loopback;
6
- use JITS\StringLocator\Tests\Smart_Scan;
7
 
8
  class Save {
9
 
@@ -122,7 +122,7 @@ class Save {
122
  * @return void
123
  */
124
  private function write_file( $path, $content ) {
125
- if ( ! current_user_can( 'edit_themes' ) ) {
126
  return;
127
  }
128
 
1
  <?php
2
 
3
+ namespace StringLocator;
4
 
5
+ use StringLocator\Tests\Loopback;
6
+ use StringLocator\Tests\Smart_Scan;
7
 
8
  class Save {
9
 
122
  * @return void
123
  */
124
  private function write_file( $path, $content ) {
125
+ if ( ! current_user_can( String_Locator::$default_capability ) ) {
126
  return;
127
  }
128
 
includes/class-search.php CHANGED
@@ -1,8 +1,8 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator;
4
 
5
- class Search {
6
 
7
  /**
8
  * An array of file extensions that will be ignored by the scanner.
@@ -25,129 +25,8 @@ class Search {
25
  'wmv',
26
  );
27
 
28
- /**
29
- * The path to the currently editable file.
30
- *
31
- * @var string
32
- */
33
- private $path_to_use = '';
34
-
35
- /**
36
- * The length of the excerpt from the line containing a match.
37
- *
38
- * @var int
39
- */
40
- private $excerpt_length = 25;
41
-
42
- /**
43
- * The server-configured max time a script can run.
44
- *
45
- * @var int
46
- */
47
- private $max_execution_time = null;
48
-
49
- /**
50
- * The current time when our script started executing.
51
- *
52
- * @var float
53
- */
54
- private $start_execution_timer = 0;
55
-
56
- /**
57
- * The server-configured max amount of memory a script can use.
58
- *
59
- * @var int
60
- */
61
- private $max_memory_consumption = 0;
62
-
63
  public function __construct() {
64
- /**
65
- * Define class variables requiring expressions
66
- */
67
- $this->path_to_use = ( is_multisite() ? 'network/admin.php' : 'tools.php' );
68
- $this->excerpt_length = apply_filters( 'string_locator_excerpt_length', 25 );
69
-
70
- $this->max_execution_time = absint( ini_get( 'max_execution_time' ) );
71
- $this->start_execution_timer = microtime( true );
72
-
73
- if ( $this->max_execution_time > 30 ) {
74
- $this->max_execution_time = 30;
75
- }
76
-
77
- $this->set_memory_limit();
78
- }
79
-
80
- /**
81
- * Sets up the memory limit variables.
82
- *
83
- * @return void
84
- * @since 2.0.0
85
- *
86
- */
87
- function set_memory_limit() {
88
- $memory_limit = ini_get( 'memory_limit' );
89
-
90
- $this->max_memory_consumption = absint( $memory_limit );
91
-
92
- if ( strstr( $memory_limit, 'k' ) ) {
93
- $this->max_memory_consumption = ( str_replace( 'k', '', $memory_limit ) * 1000 );
94
- }
95
- if ( strstr( $memory_limit, 'M' ) ) {
96
- $this->max_memory_consumption = ( str_replace( 'M', '', $memory_limit ) * 1000000 );
97
- }
98
- if ( strstr( $memory_limit, 'G' ) ) {
99
- $this->max_memory_consumption = ( str_replace( 'G', '', $memory_limit ) * 1000000000 );
100
- }
101
- }
102
-
103
- /**
104
- * Check if the script is about to exceed the max execution time.
105
- *
106
- * @return bool
107
- * @since 1.9.0
108
- *
109
- */
110
- function nearing_execution_limit() {
111
- // Max execution time is 0 or -1 (infinite) in server config
112
- if ( 0 === $this->max_execution_time || - 1 === $this->max_execution_time ) {
113
- return false;
114
- }
115
-
116
- $back_compat_filter = apply_filters( 'string-locator-extra-search-delay', 2 ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
117
-
118
- $built_in_delay = apply_filters( 'string_locator_extra_search_delay', $back_compat_filter );
119
- $execution_time = ( microtime( true ) - $this->start_execution_timer + $built_in_delay );
120
-
121
- if ( $execution_time >= $this->max_execution_time ) {
122
- return $execution_time;
123
- }
124
-
125
- return false;
126
- }
127
-
128
- /**
129
- * Check if the script is about to exceed the server memory limit.
130
- *
131
- * @return bool
132
- * @since 2.0.0
133
- *
134
- */
135
- function nearing_memory_limit() {
136
- // Check if the memory limit is set t o0 or -1 (infinite) in server config
137
- if ( 0 === $this->max_memory_consumption || - 1 === $this->max_memory_consumption ) {
138
- return false;
139
- }
140
-
141
- // We give our selves a 256k memory buffer, as we need to close off the script properly as well
142
- $back_compat_filter = apply_filters( 'string-locator-extra-memory-buffer', 256000 ); //phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores
143
- $built_in_buffer = apply_filters( 'string_locator_extra_memory_buffer', $back_compat_filter );
144
- $memory_use = ( memory_get_usage( true ) + $built_in_buffer );
145
-
146
- if ( $memory_use >= $this->max_memory_consumption ) {
147
- return $memory_use;
148
- }
149
-
150
- return false;
151
  }
152
 
153
  public function run( $filenum ) {
@@ -357,7 +236,7 @@ class Search {
357
  'path' => $path,
358
  'filename' => $path_string,
359
  'filename_raw' => $relativepath,
360
- 'editurl' => ( current_user_can( 'edit_themes' ) ? $editurl : false ),
361
  'stringresult' => $file,
362
  );
363
  }
@@ -365,7 +244,6 @@ class Search {
365
  $readfile = @fopen( $filename, 'r' );
366
  if ( $readfile ) {
367
  while ( ( $readline = fgets( $readfile ) ) !== false ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
368
- $string_preview_is_cut = false;
369
  $linenum ++;
370
  /**
371
  * If our string is found in this line, output the line number and other data
@@ -401,35 +279,7 @@ class Search {
401
  */
402
  $editurl = $this->create_edit_link( $path, $linenum, $str_pos );
403
 
404
- $string_preview = $readline;
405
- if ( strlen( $string_preview ) > ( strlen( $string ) + $this->excerpt_length ) ) {
406
- $string_location = strpos( $string_preview, $string );
407
-
408
- $string_location_start = $string_location - $this->excerpt_length;
409
- if ( $string_location_start < 0 ) {
410
- $string_location_start = 0;
411
- }
412
-
413
- $string_location_end = ( strlen( $string ) + ( $this->excerpt_length * 2 ) );
414
- if ( $string_location_end > strlen( $string_preview ) ) {
415
- $string_location_end = strlen( $string_preview );
416
- }
417
-
418
- $string_preview = substr( $string_preview, $string_location_start, $string_location_end );
419
- $string_preview_is_cut = true;
420
- }
421
-
422
- if ( $regex ) {
423
- $string_preview = preg_replace( preg_replace( '/\/(.+)\//', '/($1)/', $string ), '<strong>$1</strong>', esc_html( $string_preview ) );
424
- } else {
425
- $string_preview = preg_replace( '/(' . $string . ')/i', '<strong>$1</strong>', esc_html( $string_preview ) );
426
- }
427
- if ( $string_preview_is_cut ) {
428
- $string_preview = sprintf(
429
- '&hellip;%s&hellip;',
430
- $string_preview
431
- );
432
- }
433
 
434
  $path_string = sprintf(
435
  '<a href="%s">%s</a>',
@@ -444,7 +294,7 @@ class Search {
444
  'path' => $path,
445
  'filename' => $path_string,
446
  'filename_raw' => $relativepath,
447
- 'editurl' => ( current_user_can( 'edit_themes' ) ? $editurl : false ),
448
  'stringresult' => $string_preview,
449
  );
450
  }
1
  <?php
2
 
3
+ namespace StringLocator;
4
 
5
+ class Search extends Base\Search {
6
 
7
  /**
8
  * An array of file extensions that will be ignored by the scanner.
25
  'wmv',
26
  );
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  public function __construct() {
29
+ parent::__construct();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  }
31
 
32
  public function run( $filenum ) {
236
  'path' => $path,
237
  'filename' => $path_string,
238
  'filename_raw' => $relativepath,
239
+ 'editurl' => ( current_user_can( String_Locator::$default_capability ) ? $editurl : false ),
240
  'stringresult' => $file,
241
  );
242
  }
244
  $readfile = @fopen( $filename, 'r' );
245
  if ( $readfile ) {
246
  while ( ( $readline = fgets( $readfile ) ) !== false ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
 
247
  $linenum ++;
248
  /**
249
  * If our string is found in this line, output the line number and other data
279
  */
280
  $editurl = $this->create_edit_link( $path, $linenum, $str_pos );
281
 
282
+ $string_preview = String_Locator::create_preview( $readline, $string, $regex );
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
  $path_string = sprintf(
285
  '<a href="%s">%s</a>',
294
  'path' => $path,
295
  'filename' => $path_string,
296
  'filename_raw' => $relativepath,
297
+ 'editurl' => ( current_user_can( String_Locator::$default_capability ) ? $editurl : false ),
298
  'stringresult' => $string_preview,
299
  );
300
  }
includes/class-string-locator.php CHANGED
@@ -1,6 +1,6 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator;
4
 
5
  /**
6
  * Class String_Locator
@@ -13,6 +13,13 @@ class String_Locator {
13
  */
14
  public $string_locator_language = '';
15
 
 
 
 
 
 
 
 
16
  /**
17
  * An array containing all notices to display.
18
  *
@@ -48,68 +55,17 @@ class String_Locator {
48
  add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 );
49
 
50
  add_filter( 'string_locator_search_sources_markup', array( $this, 'add_search_options' ), 10, 2 );
51
- add_action( 'wp_ajax_string_locator_notice_dismiss', array( $this, 'dismiss_notice' ) );
52
 
53
- add_action( 'admin_notices', array( $this, 'show_sponsorship_notice' ) );
 
54
  }
55
 
56
- public function dismiss_notice() {
57
- // Only users who can use the plugin should be able to dismiss the notice.
58
- if ( ! current_user_can( 'update_core' ) || ! wp_verify_nonce( $_POST['_nonce'], 'string-locator-notice-dismiss' ) ) {
59
- return;
60
- }
61
-
62
- update_option( 'string-locator-sponsorship-notice-dismissed', array( 'is_dismissed' => true ) );
63
  }
64
 
65
- public function show_sponsorship_notice() {
66
- $screen = get_current_screen();
67
-
68
- // Only show the notice on the plugins search page.
69
- if ( 'tools_page_string-locator' !== $screen->id || isset( $_GET['edit-file'] ) ) {
70
- return;
71
- }
72
-
73
- // Do not show the notice to users who have dismissed it.
74
- $option = get_option( 'string-locator-sponsorship-notice-dismissed', array( 'is_dismissed' => false ) );
75
- if ( true === $option['is_dismissed'] ) {
76
- return;
77
- }
78
-
79
- ?>
80
-
81
- <div class="notice notice-info is-dismissible" id="string-locator-sponsorship-notice">
82
- <p>
83
- <?php esc_html_e( 'Thank you for trying out the String Locator plugin!', 'string-locator' ); ?>
84
- </p>
85
-
86
- <p>
87
- <?php esc_html_e( 'If you like the plugin, please consider making a donation to support the plugin development.', 'string-locator' ); ?>
88
- </p>
89
-
90
- <p>
91
- <a href="https://github.com/sponsors/Clorith/" target="_blank" class="button button-primary">
92
- <?php esc_html_e( 'Donate via GitHub', 'string-locator' ); ?>
93
- </a>
94
-
95
- <a href="https://paypal.me/clorith" target="_blank" class="button">
96
- <?php esc_html_e( 'Donate via PayPal', 'string-locator' ); ?>
97
- </a>
98
- </p>
99
-
100
- <script>
101
- jQuery( document ).ready( function( $ ) {
102
- $( '#string-locator-sponsorship-notice' ).on( 'click', '.notice-dismiss', function() {
103
- $.post( ajaxurl, {
104
- action: 'string_locator_notice_dismiss',
105
- _nonce: '<?php echo wp_create_nonce( 'string-locator-notice-dismiss' ); ?>'
106
- } );
107
- } );
108
- } );
109
- </script>
110
- </div>
111
-
112
- <?php
113
  }
114
 
115
  public function add_search_options( $searchers, $search_location ) {
@@ -148,8 +104,8 @@ class String_Locator {
148
  function plugin_row_meta( $meta, $plugin_file ) {
149
  if ( 'string-locator/string-locator.php' === $plugin_file ) {
150
  $meta[] = sprintf(
151
- '<a href="https://github.com/sponsors/Clorith/">%s</a>',
152
- esc_html__( 'Donate to this plugin', 'string-locator' )
153
  );
154
  }
155
 
@@ -319,39 +275,54 @@ class String_Locator {
319
  $item = (object) $item;
320
  }
321
 
322
- return sprintf(
323
- '<tr>
 
 
 
324
  <td>
325
- %s
326
  <div class="row-actions">
327
- %s
328
  </div>
329
  </td>
330
  <td>
331
- %s
332
  </td>
333
  <td>
334
- %d
335
  </td>
336
  <td>
337
- %d
338
  </td>
339
  </tr>',
340
  $item->stringresult,
341
- ( ! current_user_can( 'edit_themes' ) ? '' : sprintf(
342
  '<span class="edit"><a href="%1$s" aria-label="%2$s">%2$s</a></span>',
343
  esc_url( $item->editurl ),
344
  // translators: The row-action edit link label.
345
  esc_html__( 'Edit', 'string-locator' )
346
  ) ),
347
- ( ! current_user_can( 'edit_themes' ) ? $item->filename_raw : sprintf(
348
  '<a href="%s">%s</a>',
349
  esc_url( $item->editurl ),
350
  esc_html( $item->filename_raw )
351
  ) ),
 
352
  esc_html( $item->linenum ),
 
353
  esc_html( $item->linepos )
354
  );
 
 
 
 
 
 
 
 
 
 
355
  }
356
 
357
  /**
@@ -376,14 +347,18 @@ class String_Locator {
376
 
377
  $table_columns = sprintf(
378
  '<tr>
 
 
 
 
379
  <th scope="col" class="manage-column column-stringresult column-primary string">%s</th>
380
  <th scope="col" class="manage-column column-filename filename">%s</th>
381
  <th scope="col" class="manage-column column-linenum line">%s</th>
382
  <th scope="col" class="manage-column column-linepos position">%s</th>
383
  </tr>',
384
  esc_html( __( 'String', 'string-locator' ) ),
385
- esc_html( __( 'File', 'string-locator' ) ),
386
- esc_html( __( 'Line number', 'string-locator' ) ),
387
  esc_html( __( 'Line position', 'string-locator' ) )
388
  );
389
 
@@ -393,7 +368,7 @@ class String_Locator {
393
  }
394
 
395
  $table = sprintf(
396
- '<div class="tablenav top"><br class="clear"></div><table class="%s"><thead>%s</thead><tbody>%s</tbody><tfoot>%s</tfoot></table>',
397
  implode( ' ', $table_class ),
398
  $table_columns,
399
  implode( "\n", $table_rows ),
@@ -456,14 +431,14 @@ class String_Locator {
456
  */
457
  wp_enqueue_style( 'string-locator', trailingslashit( STRING_LOCATOR_PLUGIN_URL ) . 'build/string-locator.css', array(), $search['version'] );
458
 
459
- if ( ! isset( $_GET['edit-file'] ) || ! current_user_can( 'edit_themes' ) ) {
460
  /**
461
  * String Locator Scripts
462
  */
463
  wp_enqueue_script(
464
  'string-locator-search',
465
  trailingslashit( STRING_LOCATOR_PLUGIN_URL ) . 'build/string-locator-search.js',
466
- array( 'jquery', 'wp-util' ),
467
  $search['version'],
468
  true
469
  );
@@ -502,7 +477,7 @@ class String_Locator {
502
  wp_enqueue_script(
503
  'string-locator-editor',
504
  trailingslashit( STRING_LOCATOR_PLUGIN_URL ) . 'build/string-locator.js',
505
- array( 'jquery', 'code-editor', 'wp-util' ),
506
  $editor['version'],
507
  true
508
  );
@@ -512,8 +487,8 @@ class String_Locator {
512
  'string_locator',
513
  array(
514
  'CodeMirror' => $code_mirror,
515
- 'goto_line' => absint( $_GET['string-locator-line'] ),
516
- 'goto_linepos' => absint( $_GET['string-locator-linepos'] ),
517
  'url' => array(
518
  'save' => get_rest_url( null, 'string-locator/v1/save' ),
519
  ),
@@ -565,7 +540,7 @@ class String_Locator {
565
  /**
566
  * Don't load anything if the user can't edit themes any way
567
  */
568
- if ( ! current_user_can( 'update_core' ) ) {
569
  return false;
570
  }
571
 
@@ -578,8 +553,8 @@ class String_Locator {
578
  * - The edit file path query var does not contains double dots (used to traverse directories)
579
  * - The user is capable of editing files.
580
  */
581
- if ( isset( $_GET['string-locator-path'] ) && self::is_valid_location( $_GET['string-locator-path'] ) && current_user_can( 'edit_themes' ) ) {
582
- $include_path = trailingslashit( STRING_LOCATOR_PLUGIN_DIR ) . 'views/editor.php';
583
  } else {
584
  $include_path = trailingslashit( STRING_LOCATOR_PLUGIN_DIR ) . 'views/search.php';
585
  }
@@ -592,7 +567,7 @@ class String_Locator {
592
  }
593
 
594
  function admin_body_class( $class ) {
595
- if ( isset( $_GET['string-locator-path'] ) && self::is_valid_location( $_GET['string-locator-path'] ) && current_user_can( 'edit_themes' ) ) {
596
  $class .= ' file-edit-screen';
597
  }
598
 
@@ -624,25 +599,69 @@ class String_Locator {
624
  * @return bool
625
  */
626
  public static function is_valid_location( $path ) {
627
- $valid = true;
628
  $path = str_replace( array( '/' ), array( DIRECTORY_SEPARATOR ), stripslashes( $path ) );
629
  $abspath = str_replace( array( '/' ), array( DIRECTORY_SEPARATOR ), ABSPATH );
630
 
 
 
 
 
 
 
 
 
631
  // Check that it is a valid file we are trying to access as well.
632
  if ( ! file_exists( $path ) ) {
633
- $valid = false;
634
  }
635
 
636
  if ( empty( $path ) ) {
637
- $valid = false;
638
  }
639
  if ( stristr( $path, '..' ) ) {
640
- $valid = false;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
641
  }
642
- if ( ! stristr( $path, $abspath ) ) {
643
- $valid = false;
 
 
 
644
  }
645
 
646
- return $valid;
647
  }
648
  }
1
  <?php
2
 
3
+ namespace StringLocator;
4
 
5
  /**
6
  * Class String_Locator
13
  */
14
  public $string_locator_language = '';
15
 
16
+ /**
17
+ * The default capability to check for when seeing if a user can access the plugin.
18
+ *
19
+ * @var string
20
+ */
21
+ public static $default_capability = 'edit_themes';
22
+
23
  /**
24
  * An array containing all notices to display.
25
  *
55
  add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 );
56
 
57
  add_filter( 'string_locator_search_sources_markup', array( $this, 'add_search_options' ), 10, 2 );
 
58
 
59
+ add_action( 'string_locator_search_templates', array( $this, 'add_search_restults_templates' ) );
60
+ add_action( 'string_locator_editor_sidebar_before_checks', array( $this, 'add_instawp_reference' ) );
61
  }
62
 
63
+ public function add_search_restults_templates() {
64
+ require_once STRING_LOCATOR_PLUGIN_DIR . '/views/templates/search-default.php';
 
 
 
 
 
65
  }
66
 
67
+ public function add_instawp_reference() {
68
+ include_once STRING_LOCATOR_PLUGIN_DIR . '/views/templates/instawp.php';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  }
70
 
71
  public function add_search_options( $searchers, $search_location ) {
104
  function plugin_row_meta( $meta, $plugin_file ) {
105
  if ( 'string-locator/string-locator.php' === $plugin_file ) {
106
  $meta[] = sprintf(
107
+ '<a href="https://instawp.com/?utm_source=stringlocator/" target="_blank">%s</a>',
108
+ esc_html__( 'Create Disposable WordPress Sites in Seconds', 'string-locator' )
109
  );
110
  }
111
 
275
  $item = (object) $item;
276
  }
277
 
278
+ $row = sprintf(
279
+ '<tr data-type="file" data-linenum="%6$d" data-filename="%4$s">
280
+ <th scope="row" class="check-column">
281
+ <input type="checkbox" name="string-locator-replace-checked[]" class="check-column-box">
282
+ </th>
283
  <td>
284
+ %1$s
285
  <div class="row-actions">
286
+ %2$s
287
  </div>
288
  </td>
289
  <td>
290
+ %3$s
291
  </td>
292
  <td>
293
+ %5$d
294
  </td>
295
  <td>
296
+ %7$d
297
  </td>
298
  </tr>',
299
  $item->stringresult,
300
+ ( ! current_user_can( String_Locator::$default_capability ) ? '' : sprintf(
301
  '<span class="edit"><a href="%1$s" aria-label="%2$s">%2$s</a></span>',
302
  esc_url( $item->editurl ),
303
  // translators: The row-action edit link label.
304
  esc_html__( 'Edit', 'string-locator' )
305
  ) ),
306
+ ( ! current_user_can( String_Locator::$default_capability ) ? $item->filename_raw : sprintf(
307
  '<a href="%s">%s</a>',
308
  esc_url( $item->editurl ),
309
  esc_html( $item->filename_raw )
310
  ) ),
311
+ esc_attr( $item->filename_raw ),
312
  esc_html( $item->linenum ),
313
+ esc_attr( $item->linenum ),
314
  esc_html( $item->linepos )
315
  );
316
+
317
+ /**
318
+ * Enable extensions to override the table row when restoring a previous search.
319
+ *
320
+ * @attr string $row The HTML markup for the table row.
321
+ * @attr object $item The search result item data.
322
+ */
323
+ $row = apply_filters( 'string_locator_restore_search_row', $row, $item );
324
+
325
+ return $row;
326
  }
327
 
328
  /**
347
 
348
  $table_columns = sprintf(
349
  '<tr>
350
+ <th scope="col" class="manage-column column-cb check-column">
351
+ <label class="screen-reader-text" for="cb-select-all-1">Select All</label>
352
+ <input id="cb-select-all-1" type="checkbox">
353
+ </th>
354
  <th scope="col" class="manage-column column-stringresult column-primary string">%s</th>
355
  <th scope="col" class="manage-column column-filename filename">%s</th>
356
  <th scope="col" class="manage-column column-linenum line">%s</th>
357
  <th scope="col" class="manage-column column-linepos position">%s</th>
358
  </tr>',
359
  esc_html( __( 'String', 'string-locator' ) ),
360
+ esc_html( __( 'File / Table', 'string-locator' ) ),
361
+ esc_html( __( 'ID / Line number', 'string-locator' ) ),
362
  esc_html( __( 'Line position', 'string-locator' ) )
363
  );
364
 
368
  }
369
 
370
  $table = sprintf(
371
+ '<table class="%s" id="string-locator-search-results-table"><thead>%s</thead><tbody>%s</tbody><tfoot>%s</tfoot></table>',
372
  implode( ' ', $table_class ),
373
  $table_columns,
374
  implode( "\n", $table_rows ),
431
  */
432
  wp_enqueue_style( 'string-locator', trailingslashit( STRING_LOCATOR_PLUGIN_URL ) . 'build/string-locator.css', array(), $search['version'] );
433
 
434
+ if ( ! isset( $_GET['edit-file'] ) || ! current_user_can( String_Locator::$default_capability ) ) {
435
  /**
436
  * String Locator Scripts
437
  */
438
  wp_enqueue_script(
439
  'string-locator-search',
440
  trailingslashit( STRING_LOCATOR_PLUGIN_URL ) . 'build/string-locator-search.js',
441
+ array( 'wp-util' ),
442
  $search['version'],
443
  true
444
  );
477
  wp_enqueue_script(
478
  'string-locator-editor',
479
  trailingslashit( STRING_LOCATOR_PLUGIN_URL ) . 'build/string-locator.js',
480
+ array( 'code-editor', 'wp-util' ),
481
  $editor['version'],
482
  true
483
  );
487
  'string_locator',
488
  array(
489
  'CodeMirror' => $code_mirror,
490
+ 'goto_line' => ( isset( $_GET['string-locator-line'] ) ? absint( $_GET['string-locator-line'] ) : 0 ),
491
+ 'goto_linepos' => ( isset( $_GET['string-locator-linepos'] ) ? absint( $_GET['string-locator-linepos'] ) : 0 ),
492
  'url' => array(
493
  'save' => get_rest_url( null, 'string-locator/v1/save' ),
494
  ),
540
  /**
541
  * Don't load anything if the user can't edit themes any way
542
  */
543
+ if ( ! current_user_can( 'edit_users' ) ) {
544
  return false;
545
  }
546
 
553
  * - The edit file path query var does not contains double dots (used to traverse directories)
554
  * - The user is capable of editing files.
555
  */
556
+ if ( isset( $_GET['string-locator-path'] ) && self::is_valid_location( $_GET['string-locator-path'] ) && current_user_can( String_Locator::$default_capability ) ) {
557
+ $include_path = trailingslashit( STRING_LOCATOR_PLUGIN_DIR ) . 'views/editors/default.php';
558
  } else {
559
  $include_path = trailingslashit( STRING_LOCATOR_PLUGIN_DIR ) . 'views/search.php';
560
  }
567
  }
568
 
569
  function admin_body_class( $class ) {
570
+ if ( isset( $_GET['string-locator-path'] ) && self::is_valid_location( $_GET['string-locator-path'] ) && current_user_can( String_Locator::$default_capability ) ) {
571
  $class .= ' file-edit-screen';
572
  }
573
 
599
  * @return bool
600
  */
601
  public static function is_valid_location( $path ) {
 
602
  $path = str_replace( array( '/' ), array( DIRECTORY_SEPARATOR ), stripslashes( $path ) );
603
  $abspath = str_replace( array( '/' ), array( DIRECTORY_SEPARATOR ), ABSPATH );
604
 
605
+ /*
606
+ * Check that the ABSPath is the start of the path.
607
+ * This helps ensure that no protocol triggers can be used as part of the file path.
608
+ */
609
+ if ( substr( $path, 0, strlen( $abspath ) ) !== $abspath ) {
610
+ return false;
611
+ }
612
+
613
  // Check that it is a valid file we are trying to access as well.
614
  if ( ! file_exists( $path ) ) {
615
+ return false;
616
  }
617
 
618
  if ( empty( $path ) ) {
619
+ return false;
620
  }
621
  if ( stristr( $path, '..' ) ) {
622
+ return false;
623
+ }
624
+
625
+ return true;
626
+ }
627
+
628
+ public static function create_preview( $string_preview, $string, $regex = false ) {
629
+ /**
630
+ * Define class variables requiring expressions
631
+ */
632
+ $excerpt_length = apply_filters( 'string_locator_excerpt_length', 25 );
633
+
634
+ $string_preview_is_cut = false;
635
+
636
+ if ( strlen( $string_preview ) > ( strlen( $string ) + $excerpt_length ) ) {
637
+ $string_location = strpos( $string_preview, $string );
638
+
639
+ $string_location_start = $string_location - $excerpt_length;
640
+ if ( $string_location_start < 0 ) {
641
+ $string_location_start = 0;
642
+ }
643
+
644
+ $string_location_end = ( strlen( $string ) + ( $excerpt_length * 2 ) );
645
+ if ( $string_location_end > strlen( $string_preview ) ) {
646
+ $string_location_end = strlen( $string_preview );
647
+ }
648
+
649
+ $string_preview = substr( $string_preview, $string_location_start, $string_location_end );
650
+ $string_preview_is_cut = true;
651
+ }
652
+
653
+ if ( $regex ) {
654
+ $string_preview = preg_replace( preg_replace( '/\/(.+)\//', '/($1)/', $string ), '<strong>$1</strong>', esc_html( $string_preview ) );
655
+ } else {
656
+ $string_preview = preg_replace( '/(' . preg_quote( $string ) . ')/i', '<strong>$1</strong>', esc_html( $string_preview ) );
657
  }
658
+ if ( $string_preview_is_cut ) {
659
+ $string_preview = sprintf(
660
+ '&hellip;%s&hellip;',
661
+ $string_preview
662
+ );
663
  }
664
 
665
+ return $string_preview;
666
  }
667
  }
readme.txt CHANGED
@@ -1,12 +1,11 @@
1
  === String locator ===
2
- Contributors: Clorith
3
- Author URI: http://www.clorith.net
4
  Plugin URI: http://wordpress.org/plugins/string-locator/
5
- Donate link: https://www.paypal.me/clorith
6
  Tags: text, search, find, syntax, highlight
7
  Requires at least: 4.9
8
- Tested up to: 5.9
9
- Stable tag: 2.5.0
10
  License: GPLv2 or later
11
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
12
 
@@ -22,6 +21,8 @@ You can then quickly make edits directly in your browser by clicking the link fr
22
  By default a consistency check is performed when making edits to files, this will look for inconsistencies with braces, brackets and parenthesis that are often accidentally left in.
23
  This drastically reduces the risk of breaking your site when making edits, but is in no way an absolute guarantee.
24
 
 
 
25
 
26
  == Frequently asked questions ==
27
 
@@ -45,14 +46,19 @@ When writing your search string, make sure to wrap your search in forward slashe
45
 
46
  == Changelog ==
47
 
48
- = 2.5.0 (2022-02-27) =
49
- * Fixed a bug where content would have slashes stripped unexpectedly.
50
- * Improved table spacing on search results.
51
- * Improved loopback checks to also check admin access.
52
- * Hardened the search iterator so users can't accidentally perform unexpected directory traversal.
53
- * Introduced actions and filters in various places to enable extenders, and future enhancements.
54
- * Moved all ajax requests to dedicated REST endpoints.
55
- * Refactored file structure.
 
 
 
 
 
56
 
57
  = Older entries =
58
- See changelog.txt for the version history
1
  === String locator ===
2
+ Contributors: InstaWP, Clorith
3
+ Author URI: https://instawp.com/
4
  Plugin URI: http://wordpress.org/plugins/string-locator/
 
5
  Tags: text, search, find, syntax, highlight
6
  Requires at least: 4.9
7
+ Tested up to: 6.0
8
+ Stable tag: 2.6.0
9
  License: GPLv2 or later
10
  License URI: http://www.gnu.org/licenses/gpl-2.0.html
11
 
21
  By default a consistency check is performed when making edits to files, this will look for inconsistencies with braces, brackets and parenthesis that are often accidentally left in.
22
  This drastically reduces the risk of breaking your site when making edits, but is in no way an absolute guarantee.
23
 
24
+ [Create Disposable WordPress Sites in Seconds](https://instawp.com/?utm_source=stringlocator)
25
+
26
 
27
  == Frequently asked questions ==
28
 
46
 
47
  == Changelog ==
48
 
49
+ = 2.6.0 (2022-07-20) =
50
+ * Added database search feature.
51
+ * Added tools for quickly replacing data in the search results.
52
+ * Added many more filters and actions.
53
+ * Added hardening of file path checks.
54
+ * Removed one-time donation notice.
55
+ * Removed jQuery dependency in favor of vanilla JavaScript code.
56
+ * Separated search class into a base class for extenders.
57
+ * Fixed bug with code viewer sizes when resizing your window.
58
+ * Fixed bug in the list view if special characters were in the search string.
59
+ * Fixed a bug where RegEx search validation may have a false positive check for invalid patterns.
60
+ * Fixed missing translator function if Javascript is missing.
61
+ * Improved capability checks for displaying the search interface when editing is disabled.
62
 
63
  = Older entries =
64
+ See changelog.txt for the version history.
string-locator.php CHANGED
@@ -1,15 +1,16 @@
1
  <?php
2
  /**
3
  * Plugin Name: String Locator
4
- * Plugin URI: http://www.clorith.net/wordpress-string-locator/
5
  * Description: Scan through theme and plugin files looking for text strings
6
- * Version: 2.5.0
7
- * Author: Clorith
8
- * Author URI: http://www.clorith.net
9
  * Text Domain: string-locator
10
  * License: GPL2
11
  *
12
  * Copyright 2013 Marius Jensen (email : marius@clorith.net)
 
13
  *
14
  * This program is free software; you can redistribute it and/or modify
15
  * it under the terms of the GNU General Public License, version 2, as
@@ -25,7 +26,7 @@
25
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
26
  */
27
 
28
- namespace JITS\StringLocator;
29
 
30
  if ( ! defined( 'ABSPATH' ) ) {
31
  die();
@@ -33,6 +34,19 @@ if ( ! defined( 'ABSPATH' ) ) {
33
 
34
  define( 'STRING_LOCATOR_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
35
  define( 'STRING_LOCATOR_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
  /**
38
  * Plugin test runners
@@ -50,7 +64,6 @@ require_once __DIR__ . '/includes/class-directory-iterator.php';
50
  /**
51
  * Prepare REST endpoints.
52
  */
53
- require_once __DIR__ . '/includes/REST/class-base.php';
54
  require_once __DIR__ . '/includes/REST/class-save.php';
55
  require_once __DIR__ . '/includes/REST/class-clean.php';
56
  require_once __DIR__ . '/includes/REST/class-search.php';
1
  <?php
2
  /**
3
  * Plugin Name: String Locator
4
+ * Plugin URI: http://wordpress.org/plugins/string-locator/
5
  * Description: Scan through theme and plugin files looking for text strings
6
+ * Version: 2.6.0
7
+ * Author: InstaWP
8
+ * Author URI: https://instawp.com/
9
  * Text Domain: string-locator
10
  * License: GPL2
11
  *
12
  * Copyright 2013 Marius Jensen (email : marius@clorith.net)
13
+ * 2022 InstaWP (https://instawp.com)
14
  *
15
  * This program is free software; you can redistribute it and/or modify
16
  * it under the terms of the GNU General Public License, version 2, as
26
  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
27
  */
28
 
29
+ namespace StringLocator;
30
 
31
  if ( ! defined( 'ABSPATH' ) ) {
32
  die();
34
 
35
  define( 'STRING_LOCATOR_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
36
  define( 'STRING_LOCATOR_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
37
+ define( 'STRING_LOCATOR_PLUGIN_FILE', __FILE__ );
38
+
39
+ /**
40
+ * Base classes that other classes may extend.
41
+ */
42
+ require_once __DIR__ . '/includes/Base/class-search.php';
43
+ require_once __DIR__ . '/includes/Base/class-rest.php';
44
+
45
+ /**
46
+ * Search handlers
47
+ */
48
+ require_once __DIR__ . '/includes/Extension/SQL/sql.php';
49
+ require_once __DIR__ . '/includes/Extension/SearchReplace/search-replace.php';
50
 
51
  /**
52
  * Plugin test runners
64
  /**
65
  * Prepare REST endpoints.
66
  */
 
67
  require_once __DIR__ . '/includes/REST/class-save.php';
68
  require_once __DIR__ . '/includes/REST/class-clean.php';
69
  require_once __DIR__ . '/includes/REST/class-search.php';
views/assets/instawp-logo.svg ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <svg width="42" height="34" viewBox="0 0 42 34" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M34.738 12.6667H40.5506C40.6488 12.6664 40.7458 12.692 40.8314 12.7409C40.917 12.7899 40.9887 12.8605 41.0381 12.9456C41.0881 13.0307 41.114 13.1274 41.1146 13.226C41.1152 13.3246 41.0899 13.4216 41.0417 13.5073L30.5818 32.0658C30.4227 32.3485 30.1913 32.5838 29.9111 32.7477C29.631 32.9115 29.3128 32.998 28.9881 32.9983H24.5384C24.1678 32.9983 23.8057 32.8855 23.5008 32.6749C23.1953 32.4643 22.9616 32.1659 22.8302 31.8192L21.7408 28.9567L21.8174 29.0083C22.1656 29.2337 22.5561 29.3851 22.9652 29.4532C23.3749 29.5213 23.7937 29.5046 24.1962 29.4041C24.5986 29.3036 24.9758 29.1215 25.3054 28.8691C25.6344 28.6167 25.9085 28.2994 26.1098 27.9367L32.3863 16.6971H27.3787C27.2715 16.6993 27.166 16.6707 27.0744 16.6149C26.9828 16.559 26.9087 16.4781 26.8617 16.3818C26.8147 16.2854 26.7961 16.1776 26.8081 16.071C26.8202 15.9645 26.8629 15.8636 26.9304 15.7803L39.5149 0.129175C39.5787 0.060403 39.6643 0.0159868 39.7571 0.00355936C39.8505 -0.00886809 39.9445 0.0114701 40.024 0.0610772C40.1035 0.110684 40.1638 0.186462 40.1933 0.275389C40.2228 0.364315 40.221 0.460836 40.1873 0.548362L34.738 12.6667Z" fill="url(#paint0_linear_6_19)"/>
3
+ <path d="M14.3008 31.8751C14.3008 32.0223 14.2719 32.168 14.2153 32.304C14.1592 32.44 14.0767 32.5635 13.9725 32.6677C13.8688 32.7717 13.7447 32.8543 13.6091 32.9106C13.473 32.9669 13.3271 32.9959 13.1801 32.9959H8.40749C8.17612 32.9959 7.95077 32.9245 7.76158 32.7912C7.57238 32.658 7.42959 32.4695 7.35186 32.2517L6.78488 30.64L0.963238 14.1796C0.902382 14.0085 0.883702 13.8253 0.908405 13.6455C0.933712 13.4656 1.0012 13.2944 1.10604 13.1462C1.21088 12.9979 1.34946 12.877 1.51094 12.7935C1.67181 12.71 1.85076 12.6664 2.03212 12.6665H6.39263C6.67823 12.6663 6.9572 12.7543 7.19159 12.9183C7.42597 13.0824 7.60371 13.3145 7.70132 13.5833L13.9218 30.6198L14.2406 31.4895C14.2828 31.6135 14.3032 31.744 14.3008 31.8751Z" fill="#056960"/>
4
+ <path d="M19.1404 22.1422L15.5541 32.2789C15.4788 32.4905 15.3396 32.6736 15.1564 32.8032C14.9733 32.9328 14.7539 33.0026 14.5298 33.003H8.4075C8.17552 33.0025 7.94957 32.93 7.76038 32.7955C7.57118 32.661 7.42839 32.4711 7.35187 32.252L6.78247 30.6403C6.89454 30.8241 7.82906 32.2587 8.99255 30.6201L14.9733 14.2382C14.9733 14.2382 15.6909 13.8033 16.1301 14.2382L19.1404 22.1422Z" fill="#0D4943"/>
5
+ <path d="M27.228 25.9437L24.5227 30.7789C24.4118 30.9767 24.2473 31.1389 24.0479 31.2462C23.8478 31.3536 23.6219 31.4017 23.3959 31.3851C23.17 31.3683 22.9531 31.2874 22.7717 31.1519C22.5904 31.0163 22.4512 30.8317 22.3704 30.6198L19.1403 22.1419L16.2397 14.5203C16.177 14.3603 16.0601 14.2276 15.9089 14.1455C15.7583 14.0635 15.5829 14.0374 15.4148 14.072C15.2576 14.1049 15.1057 14.1623 14.9666 14.2424C15.1148 13.8276 15.3775 13.4632 15.7239 13.191C16.1469 12.852 16.6729 12.6671 17.2146 12.6665H21.2142C21.6293 12.6656 22.0342 12.7927 22.3747 13.0306C22.7145 13.2685 22.973 13.6056 23.1146 13.9958L27.2684 25.3699C27.3039 25.4627 27.3184 25.5622 27.3118 25.6612C27.3046 25.7603 27.2762 25.8567 27.228 25.9437Z" fill="#056960"/>
6
+ <defs>
7
+ <linearGradient id="paint0_linear_6_19" x1="30.5994" y1="28" x2="39.0994" y2="-3.29879e-07" gradientUnits="userSpaceOnUse">
8
+ <stop stop-color="#FE8551"/>
9
+ <stop offset="1" stop-color="#ED618E"/>
10
+ </linearGradient>
11
+ </defs>
12
+ </svg>
views/{editor.php → editors/default.php} RENAMED
@@ -1,6 +1,6 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator;
4
 
5
  if ( ! defined( 'ABSPATH' ) ) {
6
  die();
@@ -38,6 +38,17 @@ if ( 'core' === $_GET['file-type'] ) {
38
  'description' => $themedata->get( 'Description' ),
39
  'parent' => $themedata->get( 'parent' ),
40
  );
 
 
 
 
 
 
 
 
 
 
 
41
  } else {
42
  $plugins = get_plugins();
43
 
@@ -58,10 +69,12 @@ if ( 'core' === $_GET['file-type'] ) {
58
  }
59
  }
60
 
61
- $readfile = fopen( $file, 'r' );
62
- if ( $readfile ) {
63
- while ( ( $readline = fgets( $readfile ) ) !== false ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
64
- $editor_content .= $readline;
 
 
65
  }
66
  }
67
  ?>
@@ -100,7 +113,7 @@ if ( $readfile ) {
100
  <div id="string-locator-notices">
101
  <div class="row notice notice-error inline below-h2 hide-if-js">
102
  <p>
103
- Something about JS being required!
104
  </p>
105
  </div>
106
 
@@ -162,6 +175,8 @@ if ( $readfile ) {
162
  </div>
163
  </div>
164
 
 
 
165
  <div class="string-locator-panel">
166
  <h2 class="title"><?php esc_html_e( 'Save checks', 'string-locator' ); ?></h2>
167
  <div class="string-locator-panel-body">
@@ -169,6 +184,8 @@ if ( $readfile ) {
169
  </div>
170
  </div>
171
 
 
 
172
  <div class="string-locator-panel">
173
  <h2 class="title"><?php esc_html_e( 'File information', 'string-locator' ); ?></h2>
174
  <div class="string-locator-panel-body">
@@ -262,6 +279,6 @@ if ( $readfile ) {
262
  <div class="row notice notice-{{ data.type }} inline below-h2 is-dismissible">
263
  {{{ data.message }}}
264
 
265
- <button type="button" class="notice-dismiss"><span class="screen-reader-text"><?php esc_html_e( 'Dismiss this notice.', 'string-locator' ); ?></span></button>
266
  </div>
267
  </script>
1
  <?php
2
 
3
+ namespace StringLocator;
4
 
5
  if ( ! defined( 'ABSPATH' ) ) {
6
  die();
38
  'description' => $themedata->get( 'Description' ),
39
  'parent' => $themedata->get( 'parent' ),
40
  );
41
+ } elseif ( 'sql' === $_GET['file-type'] ) {
42
+ $details = array(
43
+ 'name' => 'Name',
44
+ 'version' => 'Version',
45
+ 'author' => array(
46
+ 'uri' => 'author URI',
47
+ 'name' => 'author name',
48
+ ),
49
+ 'description' => 'description',
50
+ 'parent' => 'parent',
51
+ );
52
  } else {
53
  $plugins = get_plugins();
54
 
69
  }
70
  }
71
 
72
+ if ( 'sql' !== $_GET['file-type'] ) {
73
+ $readfile = fopen( $file, 'r' );
74
+ if ( $readfile ) {
75
+ while ( ( $readline = fgets( $readfile ) ) !== false ) { // phpcs:ignore WordPress.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
76
+ $editor_content .= $readline;
77
+ }
78
  }
79
  }
80
  ?>
113
  <div id="string-locator-notices">
114
  <div class="row notice notice-error inline below-h2 hide-if-js">
115
  <p>
116
+ <?php esc_html_e( 'The editor requires Javascript to be enabled before it can be used.', 'string-locator' ); ?>
117
  </p>
118
  </div>
119
 
175
  </div>
176
  </div>
177
 
178
+ <?php do_action( 'string_locator_editor_sidebar_before_checks' ); ?>
179
+
180
  <div class="string-locator-panel">
181
  <h2 class="title"><?php esc_html_e( 'Save checks', 'string-locator' ); ?></h2>
182
  <div class="string-locator-panel-body">
184
  </div>
185
  </div>
186
 
187
+ <?php do_action( 'string_locator_editor_sidebar_after_checks' ); ?>
188
+
189
  <div class="string-locator-panel">
190
  <h2 class="title"><?php esc_html_e( 'File information', 'string-locator' ); ?></h2>
191
  <div class="string-locator-panel-body">
279
  <div class="row notice notice-{{ data.type }} inline below-h2 is-dismissible">
280
  {{{ data.message }}}
281
 
282
+ <button type="button" class="notice-dismiss" onclick="this.closest( '.notice' ).remove()"><span class="screen-reader-text"><?php esc_html_e( 'Dismiss this notice.', 'string-locator' ); ?></span></button>
283
  </div>
284
  </script>
views/search.php CHANGED
@@ -1,6 +1,6 @@
1
  <?php
2
 
3
- namespace JITS\StringLocator;
4
 
5
  if ( ! defined( 'ABSPATH' ) ) {
6
  die();
@@ -48,7 +48,7 @@ if ( isset( $_GET['restore'] ) ) {
48
  </strong>
49
  </p>
50
  <p>
51
- <?php esc_html_e( 'Because this site is configured to not allow direct file editing, the String Locator plugin has limited functionality and will only allow you to search for files with your string in them.', 'string-locator' ); ?>
52
  </p>
53
  </div>
54
  <?php endif; ?>
@@ -71,17 +71,52 @@ if ( isset( $_GET['restore'] ) ) {
71
  <p>
72
  <input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Search', 'string-locator' ); ?>">
73
  <a href="<?php echo esc_url( $this_url . '&restore=true' ); ?>" class="button button-primary"><?php esc_html_e( 'Restore last search', 'string-locator' ); ?></a>
 
 
 
 
 
 
 
 
 
74
  </p>
75
  </form>
76
 
77
- <div class="notices"></div>
78
 
79
- <div class="string-locator-feedback hide">
80
  <progress id="string-locator-search-progress" max="100"></progress>
81
  <span id="string-locator-feedback-text"><?php esc_html_e( 'Preparing search&hellip;', 'string-locator' ); ?></span>
82
  </div>
83
 
84
- <div class="table-wrapper">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  <?php
86
  if ( isset( $_GET['restore'] ) ) {
87
  $items = get_option( 'string-locator-search-history', array() );
@@ -93,38 +128,14 @@ if ( isset( $_GET['restore'] ) ) {
93
  }
94
  ?>
95
  </div>
 
 
 
 
 
 
 
 
96
  </div>
97
 
98
- <script id="tmpl-string-locator-search-result" type="text/template">
99
- <tr>
100
- <td>
101
- {{{ data.stringresult }}}
102
-
103
- <div class="row-actions">
104
- <# if ( data.editurl ) { #>
105
- <span class="edit">
106
- <a href="{{ data.editurl }}" aria-label="<?php esc_attr_e( 'Edit', 'string-locator' ); ?>">
107
- <?php esc_html_e( 'Edit', 'string-locator' ); ?>
108
- </a>
109
- </span>
110
- <# } #>
111
- </div>
112
- </td>
113
- <td>
114
- <# if ( data.editurl ) { #>
115
- <a href="{{ data.editurl }}">
116
- {{ data.filename_raw }}
117
- </a>
118
- <# } #>
119
- <# if ( ! data.editurl ) { #>
120
- {{ data.filename_raw }}
121
- <# } #>
122
- </td>
123
- <td>
124
- {{ data.linenum }}
125
- </td>
126
- <td>
127
- {{ data.linepos }}
128
- </td>
129
- </tr>
130
- </script>
1
  <?php
2
 
3
+ namespace StringLocator;
4
 
5
  if ( ! defined( 'ABSPATH' ) ) {
6
  die();
48
  </strong>
49
  </p>
50
  <p>
51
+ <?php esc_html_e( 'Because this site is configured to not allow direct file editing, the String Locator plugin has limited functionality and may noy allow you to directly edit files with your string in them.', 'string-locator' ); ?>
52
  </p>
53
  </div>
54
  <?php endif; ?>
71
  <p>
72
  <input type="submit" name="submit" id="submit" class="button button-primary" value="<?php esc_html_e( 'Search', 'string-locator' ); ?>">
73
  <a href="<?php echo esc_url( $this_url . '&restore=true' ); ?>" class="button button-primary"><?php esc_html_e( 'Restore last search', 'string-locator' ); ?></a>
74
+
75
+ <?php
76
+ /**
77
+ * Provides an area for outputting additional markup or controls
78
+ * immediately following the button controllers for the String Locator
79
+ * search form, but within the same paragraph for easier styling where needed.
80
+ */
81
+ do_action( 'string_locator_after_search_buttons' );
82
+ ?>
83
  </p>
84
  </form>
85
 
86
+ <div class="notices" id="string-locator-search-notices"></div>
87
 
88
+ <div class="string-locator-feedback hide" id="string-locator-progress-wrapper">
89
  <progress id="string-locator-search-progress" max="100"></progress>
90
  <span id="string-locator-feedback-text"><?php esc_html_e( 'Preparing search&hellip;', 'string-locator' ); ?></span>
91
  </div>
92
 
93
+ <?php
94
+ /**
95
+ * Provides an action for outputting additional markup or controls
96
+ * immediately preceding the table displaying search results.
97
+ */
98
+ do_action( 'string_locator_before_search_results_table' );
99
+
100
+ $wrapper_classes = array(
101
+ 'table-wrapper',
102
+ );
103
+
104
+ if ( isset( $_GET['restore'] ) ) {
105
+ $wrapper_classes[] = 'restore';
106
+ }
107
+ ?>
108
+
109
+ <div id="string-locator-search-results-table-wrapper" class="<?php echo esc_attr( implode( ' ', $wrapper_classes ) ); ?>">
110
+ <div class="tablenav top">
111
+ <?php
112
+ /**
113
+ * An action to output controls in the tablenav region, which only
114
+ * become visible when there are search results available.
115
+ */
116
+ do_action( 'string_locator_search_results_tablenav_controls' );
117
+ ?>
118
+ <br class="clear" />
119
+ </div>
120
  <?php
121
  if ( isset( $_GET['restore'] ) ) {
122
  $items = get_option( 'string-locator-search-history', array() );
128
  }
129
  ?>
130
  </div>
131
+
132
+ <?php
133
+ /**
134
+ * Provides an action for outputting additional markup or controls
135
+ * immediately following the table displaying search results.
136
+ */
137
+ do_action( 'string_locator_after_search_results_table' );
138
+ ?>
139
  </div>
140
 
141
+ <?php do_action( 'string_locator_search_templates' ); ?>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
views/templates/instawp.php ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ die();
4
+ }
5
+ ?>
6
+
7
+ <div class="string-locator-panel">
8
+ <h2 class="title">
9
+ <img src="<?php echo plugins_url( 'views/assets/instawp-logo.svg', STRING_LOCATOR_PLUGIN_FILE ); ?>" alt="<?php esc_attr_e( 'InstaWP logo', 'string-locator' ); ?>">
10
+ <span>
11
+ <?php esc_html_e( 'Create a disposable site', 'string-locator' ); ?>
12
+ </span>
13
+ </h2>
14
+ <div class="string-locator-panel-body">
15
+ <div class="row">
16
+ <a href="https://instawp.com/?utm_source=stringlocator" target="_blank">
17
+ <?php esc_html_e( 'Create Disposable WordPress Sites in Seconds.', 'string-locator' ); ?>
18
+ </a>
19
+ </div>
20
+ </div>
21
+ </div>
views/templates/search-default.php ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <?php
2
+ if ( ! defined( 'ABSPATH' ) ) {
3
+ die();
4
+ }
5
+ ?>
6
+ <script id="tmpl-string-locator-search-result" type="text/template">
7
+ <tr data-type="file" data-linenum="{{ data.linenum }}" data-filename="{{ data.filename_raw }}">
8
+ <th scope="row" class="check-column">
9
+ <input type="checkbox" name="string-locator-replace-checked[]" class="check-column-box">
10
+ </th>
11
+ <td>
12
+ {{{ data.stringresult }}}
13
+
14
+ <div class="row-actions">
15
+ <# if ( data.editurl ) { #>
16
+ <span class="edit">
17
+ <a href="{{ data.editurl }}" aria-label="<?php esc_attr_e( 'Edit', 'string-locator' ); ?>">
18
+ <?php esc_html_e( 'Edit', 'string-locator' ); ?>
19
+ </a>
20
+ </span>
21
+ <# } #>
22
+ </div>
23
+ </td>
24
+ <td>
25
+ <# if ( data.editurl ) { #>
26
+ <a href="{{ data.editurl }}">
27
+ {{ data.filename_raw }}
28
+ </a>
29
+ <# } #>
30
+ <# if ( ! data.editurl ) { #>
31
+ {{ data.filename_raw }}
32
+ <# } #>
33
+ </td>
34
+ <td>
35
+ {{ data.linenum }}
36
+ </td>
37
+ <td>
38
+ {{ data.linepos }}
39
+ </td>
40
+ </tr>
41
+ </script>