This commit is contained in:
2026-07-02 15:54:39 -06:00
commit 9883323161
17470 changed files with 4470592 additions and 0 deletions
@@ -0,0 +1 @@
<?php return array('dependencies' => array('wp-api-fetch', 'wp-polyfill'), 'version' => '28dcf1c6b99dfedf9bb0');
@@ -0,0 +1 @@
(()=>{"use strict";var e={1455(e){e.exports=window.wp.apiFetch}},t={};function n(o){var a=t[o];if(void 0!==a)return a.exports;var r=t[o]={exports:{}};return e[o](r,r.exports,n),r.exports}n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var o in t)n.o(t,o)&&!n.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);var o=n(1455);n.n(o)()({path:"my-jetpack/v1/red-bubble-notifications",method:"POST"}).then(e=>{const t=Object.values(e||{}).filter(e=>!e.is_silent).length,n=document.querySelector("#toplevel_page_jetpack .wp-menu-name");if(!n)return;const o=n.querySelector(".awaiting-mod");if(t>0)if(o)o.className="awaiting-mod",o.textContent=String(t);else{const e=document.createElement("span");e.className="awaiting-mod",e.textContent=String(t),n.appendChild(document.createTextNode(" ")),n.appendChild(e)}else o&&o.remove()}).catch(e=>{console.log("[Jetpack] Red bubble notification fetch failed:",e)})})();
@@ -0,0 +1,25 @@
<svg width="253" height="31" viewBox="0 0 253 31" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15.5006 31C24.0612 31 31.0012 24.0601 31.0012 15.4994C31.0012 6.93876 24.0612 0 15.5006 0C6.9399 0 0 6.9399 0 15.5006C0 24.0612 6.9399 31 15.5006 31Z" fill="#069E08"/>
<path d="M16.2695 12.8975V27.924L24.0198 12.8975H16.2695Z" fill="white"/>
<path d="M14.7009 18.074V3.07617L6.98047 18.074H14.7009Z" fill="white"/>
<path d="M49.6778 5.00294L56.3817 23.5053C56.9599 25.0884 57.8482 25.6623 59.367 25.7964C59.4225 25.7991 59.4769 25.8136 59.5268 25.8392C59.5768 25.8648 59.6212 25.9009 59.6573 25.9452C59.6934 25.9895 59.7205 26.0411 59.7369 26.0968C59.7532 26.1525 59.7586 26.2112 59.7525 26.2691C59.7547 26.329 59.7456 26.3887 59.7257 26.4448C59.7058 26.501 59.6755 26.5524 59.6366 26.5962C59.5976 26.6399 59.5507 26.6751 59.4987 26.6998C59.4467 26.7244 59.3905 26.738 59.3335 26.7397H51.9906C51.9313 26.7412 51.8722 26.73 51.8171 26.7069C51.7619 26.6838 51.7118 26.6493 51.6697 26.6053C51.6276 26.5614 51.5945 26.5089 51.5722 26.4511C51.5499 26.3933 51.5391 26.3314 51.5402 26.2691C51.5359 26.2087 51.5435 26.1479 51.5626 26.0907C51.5817 26.0335 51.6119 25.981 51.6512 25.9366C51.6906 25.8922 51.7383 25.8569 51.7913 25.8327C51.8443 25.8086 51.9014 25.7962 51.9592 25.7964C52.6966 25.7964 53.5325 25.5941 53.5325 24.6509C53.526 24.2575 53.4493 23.869 53.3063 23.5053L51.6973 19.0221C51.6324 18.8198 51.601 18.7516 51.4082 18.7516H45.2804C44.3168 18.7516 43.1289 18.7516 42.5821 20.0335L41.1157 23.5053C40.9665 23.8203 40.8789 24.1635 40.858 24.5145C40.858 25.7282 42.3684 25.7964 43.1624 25.7964C43.2202 25.7962 43.2774 25.8086 43.3304 25.8327C43.3834 25.8569 43.431 25.8922 43.4704 25.9366C43.5097 25.981 43.5399 26.0335 43.559 26.0907C43.5781 26.1479 43.5858 26.2087 43.5814 26.2691C43.5826 26.3315 43.5717 26.3935 43.5492 26.4513C43.5268 26.5092 43.4934 26.5616 43.4511 26.6054C43.4088 26.6492 43.3584 26.6835 43.303 26.7062C43.2476 26.7289 43.1883 26.7396 43.1289 26.7375H36.6136C36.5543 26.739 36.4954 26.7278 36.4403 26.7047C36.3853 26.6816 36.3353 26.647 36.2934 26.603C36.2514 26.559 36.2185 26.5065 36.1964 26.4488C36.1744 26.391 36.1638 26.3291 36.1652 26.2669C36.1609 26.2065 36.1685 26.1457 36.1876 26.0885C36.2068 26.0313 36.2369 25.9788 36.2763 25.9344C36.3156 25.89 36.3633 25.8547 36.4163 25.8305C36.4693 25.8064 36.5265 25.794 36.5842 25.7942C38.1576 25.6931 38.6792 24.9147 39.2805 23.5031L47.0193 5.70873C47.3733 4.93258 48.1443 4.25977 48.8503 4.25977C49.3573 4.26196 49.5186 4.56539 49.6778 5.00294ZM43.8119 17.0674C43.7731 17.1274 43.7506 17.1973 43.7469 17.2697C43.7469 17.3379 43.8119 17.3708 43.9397 17.3708H50.2979C50.7462 17.3708 50.939 17.2697 50.939 16.9663C50.9384 16.814 50.9054 16.6638 50.8426 16.5265L49.0765 11.5376C48.6575 10.392 48.05 8.87711 47.8573 7.62823C47.8573 7.56227 47.8237 7.52709 47.7923 7.52709C47.7609 7.52709 47.7295 7.56227 47.7295 7.62823C47.4883 8.48691 47.1764 9.32192 46.7972 10.1238L43.8119 17.0674Z" fill="black"/>
<path d="M49.6778 5.00294L56.3817 23.5053C56.9599 25.0884 57.8482 25.6623 59.367 25.7964C59.4225 25.7991 59.4769 25.8136 59.5268 25.8392C59.5768 25.8648 59.6212 25.9009 59.6573 25.9452C59.6934 25.9895 59.7205 26.0411 59.7369 26.0968C59.7532 26.1525 59.7586 26.2112 59.7525 26.2691C59.7547 26.329 59.7456 26.3887 59.7257 26.4448C59.7058 26.501 59.6755 26.5524 59.6366 26.5962C59.5976 26.6399 59.5507 26.6751 59.4987 26.6998C59.4467 26.7244 59.3905 26.738 59.3335 26.7397H51.9906C51.9313 26.7412 51.8722 26.73 51.8171 26.7069C51.7619 26.6838 51.7118 26.6493 51.6697 26.6053C51.6276 26.5614 51.5945 26.5089 51.5722 26.4511C51.5499 26.3933 51.5391 26.3314 51.5402 26.2691C51.5359 26.2087 51.5435 26.1479 51.5626 26.0907C51.5817 26.0335 51.6119 25.981 51.6512 25.9366C51.6906 25.8922 51.7383 25.8569 51.7913 25.8327C51.8443 25.8086 51.9014 25.7962 51.9592 25.7964C52.6966 25.7964 53.5325 25.5941 53.5325 24.6509C53.526 24.2575 53.4493 23.869 53.3063 23.5053L51.6973 19.0221C51.6324 18.8198 51.601 18.7516 51.4082 18.7516H45.2804C44.3168 18.7516 43.1289 18.7516 42.5821 20.0335L41.1157 23.5053C40.9665 23.8203 40.8789 24.1635 40.858 24.5145C40.858 25.7282 42.3684 25.7964 43.1624 25.7964C43.2202 25.7962 43.2774 25.8086 43.3304 25.8327C43.3834 25.8569 43.431 25.8922 43.4704 25.9366C43.5097 25.981 43.5399 26.0335 43.559 26.0907C43.5781 26.1479 43.5858 26.2087 43.5814 26.2691C43.5826 26.3315 43.5717 26.3935 43.5492 26.4513C43.5268 26.5092 43.4934 26.5616 43.4511 26.6054C43.4088 26.6492 43.3584 26.6835 43.303 26.7062C43.2476 26.7289 43.1883 26.7396 43.1289 26.7375H36.6136C36.5543 26.739 36.4954 26.7278 36.4403 26.7047C36.3853 26.6816 36.3353 26.647 36.2934 26.603C36.2514 26.559 36.2185 26.5065 36.1964 26.4488C36.1744 26.391 36.1638 26.3291 36.1652 26.2669C36.1609 26.2065 36.1685 26.1457 36.1876 26.0885C36.2068 26.0313 36.2369 25.9788 36.2763 25.9344C36.3156 25.89 36.3633 25.8547 36.4163 25.8305C36.4693 25.8064 36.5265 25.794 36.5842 25.7942C38.1576 25.6931 38.6792 24.9147 39.2805 23.5031L47.0193 5.70873C47.3733 4.93258 48.1443 4.25977 48.8503 4.25977C49.3573 4.26196 49.5186 4.56539 49.6778 5.00294ZM43.8119 17.0674C43.7731 17.1274 43.7506 17.1973 43.7469 17.2697C43.7469 17.3379 43.8119 17.3708 43.9397 17.3708H50.2979C50.7462 17.3708 50.939 17.2697 50.939 16.9663C50.9384 16.814 50.9054 16.6638 50.8426 16.5265L49.0765 11.5376C48.6575 10.392 48.05 8.87711 47.8573 7.62823C47.8573 7.56227 47.8237 7.52709 47.7923 7.52709C47.7609 7.52709 47.7295 7.56227 47.7295 7.62823C47.4883 8.48691 47.1764 9.32192 46.7972 10.1238L43.8119 17.0674Z" fill="black"/>
<path d="M61.7565 16.2732C61.7565 17.1395 61.2894 17.6298 60.4451 17.6298C59.6008 17.6298 59.2656 17.1791 59.2656 16.4909C59.2656 15.6246 59.7538 15.1343 60.5771 15.1343C61.4004 15.1343 61.7565 15.6048 61.7565 16.2732Z" fill="black"/>
<path d="M36.5967 16.3023C36.5967 17.1664 36.1274 17.6589 35.2852 17.6589C34.443 17.6589 34.1016 17.206 34.1016 16.5178C34.1016 15.6536 34.5897 15.1611 35.413 15.1611C36.2363 15.1611 36.5967 15.6339 36.5967 16.3023Z" fill="black"/>
<path d="M79.7775 19.668H73.9986L72.4965 23.578H69.5039L75.5924 8.23636H78.1608L84.2493 23.578H81.2796L79.7775 19.668ZM76.8651 11.9284C76.33 13.5337 75.8523 14.8752 75.4319 15.953L74.9044 17.3175H78.8717L78.3442 15.953C77.9238 14.8676 77.4461 13.526 76.911 11.9284H76.8651Z" fill="black"/>
<path d="M85.7288 6.9751H88.4463V16.9735H88.5036L92.7346 12.4673H96.0942L91.2211 17.6844L96.1286 23.578H92.8378L88.4922 18.4068H88.4463V23.578H85.7288V6.9751Z" fill="black"/>
<path d="M97.3444 12.4673H100.062V23.578H97.3444V12.4673ZM97.2756 6.9751H100.131V9.92188H97.2756V6.9751Z" fill="black"/>
<path d="M107.573 20.356C107.573 20.119 107.504 19.9241 107.366 19.7712C107.236 19.6183 107.06 19.4922 106.839 19.3928C106.617 19.2858 106.361 19.1941 106.071 19.1177C105.788 19.0412 105.49 18.9648 105.176 18.8883C104.779 18.7813 104.397 18.659 104.03 18.5214C103.663 18.3762 103.338 18.1889 103.055 17.9596C102.78 17.7303 102.558 17.4436 102.39 17.0996C102.229 16.748 102.149 16.3199 102.149 15.8154C102.149 15.1963 102.26 14.6612 102.482 14.2102C102.703 13.7515 103.001 13.3732 103.376 13.075C103.758 12.7769 104.198 12.5552 104.695 12.41C105.199 12.2648 105.73 12.1922 106.288 12.1922C106.969 12.1922 107.607 12.2495 108.203 12.3641C108.799 12.4712 109.338 12.6126 109.82 12.7884V15.0013C109.568 14.9173 109.3 14.8408 109.017 14.772C108.742 14.6956 108.459 14.6306 108.169 14.5771C107.886 14.5236 107.603 14.4815 107.32 14.451C107.038 14.4128 106.77 14.3936 106.518 14.3936C106.197 14.3936 105.925 14.4242 105.704 14.4854C105.482 14.5465 105.302 14.6306 105.165 14.7376C105.027 14.837 104.928 14.9555 104.867 15.0931C104.805 15.223 104.775 15.3606 104.775 15.5058C104.775 15.7581 104.84 15.9645 104.97 16.125C105.107 16.2855 105.295 16.4155 105.532 16.5149C105.769 16.6142 106.017 16.6983 106.277 16.7671C106.537 16.8283 106.789 16.8894 107.034 16.9506C107.416 17.0423 107.798 17.1531 108.18 17.2831C108.563 17.413 108.907 17.5965 109.212 17.8335C109.518 18.0628 109.766 18.3647 109.958 18.7393C110.156 19.1062 110.256 19.5763 110.256 20.1496C110.256 20.7764 110.137 21.323 109.9 21.7893C109.663 22.2555 109.331 22.6454 108.903 22.9588C108.482 23.2646 107.974 23.4901 107.378 23.6353C106.789 23.7882 106.139 23.8646 105.428 23.8646C104.725 23.8646 104.091 23.8073 103.525 23.6926C102.967 23.5856 102.505 23.448 102.138 23.2798V21.0898C102.734 21.3115 103.284 21.4644 103.789 21.5485C104.293 21.6249 104.763 21.6631 105.199 21.6631C105.535 21.6631 105.849 21.6402 106.139 21.5943C106.43 21.5408 106.678 21.4606 106.885 21.3535C107.099 21.2465 107.267 21.1128 107.389 20.9522C107.511 20.7841 107.573 20.5853 107.573 20.356Z" fill="black"/>
<path d="M126.446 16.813C126.446 16.576 126.439 16.3543 126.423 16.1479C126.408 15.9416 126.381 15.7619 126.343 15.609C126.259 15.2727 126.095 15.0013 125.85 14.795C125.605 14.5886 125.261 14.4815 124.818 14.4739C124.581 14.4739 124.336 14.5083 124.084 14.5771C123.832 14.6459 123.58 14.7453 123.327 14.8752C123.083 14.9975 122.846 15.1504 122.617 15.3339C122.395 15.5097 122.192 15.7084 122.009 15.9301V23.578H119.291V16.813C119.291 16.576 119.284 16.3543 119.268 16.1479C119.253 15.9416 119.226 15.7619 119.188 15.609C119.104 15.2727 118.94 15.0013 118.695 14.795C118.458 14.5886 118.114 14.4815 117.663 14.4739C117.426 14.4739 117.182 14.5083 116.929 14.5771C116.677 14.6459 116.425 14.7453 116.173 14.8752C115.928 14.9975 115.691 15.1504 115.462 15.3339C115.24 15.5097 115.037 15.7084 114.854 15.9301V23.578H112.137V12.4673H114.384L114.636 14.0153H114.682C114.843 13.7859 115.045 13.5604 115.29 13.3388C115.534 13.1171 115.81 12.9222 116.115 12.754C116.429 12.5858 116.769 12.4521 117.136 12.3527C117.51 12.2457 117.904 12.1922 118.317 12.1922C119.051 12.1922 119.708 12.3565 120.289 12.6852C120.878 13.0062 121.325 13.5108 121.63 14.1987H121.676C121.89 13.9006 122.127 13.6292 122.387 13.3846C122.655 13.14 122.949 12.9298 123.27 12.754C123.591 12.5782 123.935 12.4406 124.302 12.3412C124.677 12.2418 125.074 12.1922 125.495 12.1922C126.29 12.1998 126.955 12.368 127.49 12.6967C128.032 13.0177 128.449 13.4611 128.739 14.0267C128.816 14.172 128.881 14.3325 128.934 14.5083C128.988 14.6765 129.03 14.8676 129.06 15.0816C129.099 15.2956 129.125 15.5326 129.141 15.7925C129.156 16.0524 129.164 16.3505 129.164 16.6869V23.578H126.446V16.813Z" fill="black"/>
<path d="M137.408 21.6746C137.936 21.6746 138.486 21.6211 139.059 21.5141C139.633 21.4071 140.214 21.2542 140.802 21.0554V23.2225C140.451 23.3754 139.912 23.5206 139.185 23.6582C138.459 23.7958 137.706 23.8646 136.927 23.8646C136.139 23.8646 135.39 23.7652 134.679 23.5665C133.976 23.3601 133.361 23.0314 132.833 22.5804C132.306 22.1294 131.885 21.5446 131.572 20.8261C131.266 20.1076 131.113 19.2361 131.113 18.2118C131.113 17.2028 131.259 16.3199 131.549 15.5632C131.84 14.8064 132.229 14.1796 132.719 13.6827C133.208 13.1859 133.766 12.8151 134.393 12.5705C135.027 12.3183 135.681 12.1922 136.353 12.1922C137.057 12.1922 137.703 12.2992 138.291 12.5132C138.887 12.7272 139.4 13.0636 139.828 13.5222C140.263 13.9809 140.6 14.5733 140.837 15.2995C141.081 16.018 141.203 16.8703 141.203 17.8564C141.196 18.2386 141.184 18.5635 141.169 18.831H133.888C133.926 19.3432 134.041 19.7827 134.232 20.1496C134.423 20.5089 134.672 20.8032 134.977 21.0325C135.291 21.2542 135.654 21.4185 136.067 21.5255C136.479 21.6249 136.927 21.6746 137.408 21.6746ZM136.319 14.3134C135.975 14.3134 135.669 14.3784 135.402 14.5083C135.134 14.6306 134.901 14.8026 134.702 15.0243C134.511 15.2383 134.358 15.4906 134.244 15.781C134.129 16.0715 134.052 16.3811 134.014 16.7098H138.383C138.383 16.3811 138.337 16.0715 138.245 15.781C138.161 15.4906 138.031 15.2383 137.855 15.0243C137.687 14.8026 137.473 14.6306 137.213 14.5083C136.961 14.3784 136.663 14.3134 136.319 14.3134Z" fill="black"/>
<path d="M150.044 23.5665C149.739 23.6582 149.372 23.7308 148.944 23.7843C148.523 23.8379 148.129 23.8646 147.763 23.8646C146.83 23.8646 146.066 23.7156 145.469 23.4174C144.881 23.1117 144.464 22.676 144.22 22.1103C144.044 21.7052 143.956 21.1548 143.956 20.4592V14.7032H141.88V12.4673H143.956V9.36004H146.673V12.4673H149.884V14.7032H146.673V20.1037C146.673 20.5318 146.738 20.849 146.868 21.0554C147.098 21.4071 147.552 21.5829 148.233 21.5829C148.546 21.5829 148.856 21.5599 149.161 21.5141C149.475 21.4606 149.769 21.3956 150.044 21.3191V23.5665Z" fill="black"/>
<path d="M156.242 23.5774L162.491 8.23579H164.108L170.357 23.5774H168.327L166.493 18.9566H160.083L158.249 23.5774H156.242ZM163.282 10.6207C162.602 12.5317 161.948 14.2669 161.322 15.8263L160.748 17.271H165.828L165.255 15.8263C164.628 14.2364 163.986 12.5012 163.328 10.6207H163.282Z" fill="black"/>
<path d="M173.74 14.1981H173.774C173.973 13.9459 174.191 13.7127 174.428 13.4987C174.672 13.277 174.944 13.0859 175.242 12.9254C175.54 12.7572 175.865 12.6273 176.217 12.5356C176.576 12.4438 176.966 12.398 177.386 12.398C177.883 12.398 178.349 12.4668 178.785 12.6044C179.221 12.742 179.599 12.9636 179.92 13.2694C180.241 13.5752 180.493 13.9765 180.677 14.4733C180.868 14.9625 180.963 15.5664 180.963 16.285V23.5774H179.186V16.5716C179.186 15.6314 179.003 14.9549 178.636 14.5421C178.277 14.1293 177.749 13.923 177.054 13.923C176.442 13.923 175.85 14.0949 175.276 14.4389C174.703 14.7753 174.21 15.2416 173.797 15.8378V23.5774H172.02V12.6846H173.396L173.74 14.1981Z" fill="black"/>
<path d="M189.678 23.6003C189.525 23.6615 189.292 23.7188 188.979 23.7723C188.665 23.8258 188.291 23.8564 187.855 23.864C187.282 23.864 186.785 23.7914 186.364 23.6462C185.944 23.5086 185.592 23.3098 185.31 23.0499C185.034 22.7824 184.828 22.4614 184.69 22.0868C184.553 21.7122 184.484 21.2956 184.484 20.837V14.2096H182.386V12.6846H184.484V9.70344H186.261V12.6846H189.518V14.2096H186.261V20.5618C186.261 20.8141 186.296 21.0472 186.364 21.2612C186.441 21.4753 186.556 21.6626 186.708 21.8231C186.869 21.976 187.072 22.0983 187.316 22.19C187.561 22.2741 187.855 22.3161 188.199 22.3161C188.436 22.3161 188.684 22.297 188.944 22.2588C189.212 22.2206 189.456 22.1709 189.678 22.1097V23.6003Z" fill="black"/>
<path d="M191.743 23.5774V12.6846H193.52V23.5774H191.743ZM191.697 10.1506V8.16699H193.566V10.1506H191.697Z" fill="black"/>
<path d="M196.272 17.0303H201.329V18.6928H196.272V17.0303Z" fill="black"/>
<path d="M205.113 15.5855C205.113 15.8837 205.178 16.1283 205.308 16.3194C205.438 16.5105 205.61 16.6672 205.824 16.7895C206.045 16.9041 206.298 16.9997 206.581 17.0761C206.871 17.1526 207.169 17.2214 207.475 17.2825C207.895 17.3742 208.293 17.4774 208.667 17.5921C209.042 17.6991 209.386 17.8635 209.699 18.0851C210.02 18.3068 210.276 18.6088 210.468 18.991C210.666 19.3655 210.766 19.8547 210.766 20.4586C210.766 21.009 210.659 21.4982 210.445 21.9263C210.231 22.3467 209.929 22.7021 209.539 22.9926C209.149 23.2754 208.683 23.4895 208.14 23.6347C207.597 23.7876 207.001 23.864 206.351 23.864C205.954 23.864 205.591 23.8411 205.262 23.7952C204.941 23.757 204.654 23.7035 204.402 23.6347C204.157 23.5736 203.947 23.5086 203.771 23.4398C203.596 23.371 203.45 23.306 203.336 23.2449V21.7543C203.794 21.9377 204.249 22.083 204.7 22.19C205.151 22.297 205.648 22.3505 206.191 22.3505C206.619 22.3505 207.005 22.3123 207.349 22.2359C207.693 22.1594 207.987 22.0486 208.232 21.9033C208.484 21.7505 208.675 21.5632 208.805 21.3415C208.943 21.1122 209.011 20.8485 209.011 20.5503C209.011 20.2446 208.95 19.9923 208.828 19.7936C208.706 19.5948 208.541 19.4305 208.335 19.3005C208.128 19.1706 207.888 19.0636 207.612 18.9795C207.345 18.8954 207.062 18.819 206.764 18.7502C206.374 18.6661 205.977 18.5667 205.571 18.4521C205.174 18.3374 204.811 18.1769 204.482 17.9705C204.154 17.7641 203.886 17.4851 203.68 17.1335C203.481 16.7742 203.381 16.3155 203.381 15.7575C203.381 15.1842 203.485 14.6874 203.691 14.2669C203.897 13.8465 204.176 13.4987 204.528 13.2235C204.88 12.9483 205.289 12.742 205.755 12.6044C206.221 12.4668 206.714 12.398 207.234 12.398C207.838 12.398 208.396 12.4553 208.908 12.57C209.428 12.6846 209.898 12.8299 210.318 13.0057V14.5192C209.86 14.3434 209.394 14.202 208.92 14.0949C208.453 13.9803 207.972 13.9191 207.475 13.9115C207.054 13.9115 206.695 13.9535 206.397 14.0376C206.099 14.1217 205.854 14.2402 205.663 14.3931C205.472 14.5383 205.331 14.7141 205.239 14.9205C205.155 15.1192 205.113 15.3409 205.113 15.5855Z" fill="black"/>
<path d="M220.787 18.0278C220.787 17.3934 220.719 16.8239 220.581 16.3194C220.451 15.8149 220.252 15.3868 219.985 15.0352C219.717 14.6759 219.389 14.4007 218.999 14.2096C218.609 14.0185 218.158 13.923 217.646 13.923C217.279 13.923 216.942 13.9726 216.637 14.072C216.339 14.1714 216.067 14.2975 215.823 14.4504C215.578 14.6033 215.368 14.7676 215.192 14.9434C215.016 15.1192 214.875 15.276 214.768 15.4135V21.1122C215.142 21.4944 215.567 21.7963 216.04 22.018C216.522 22.2397 217.038 22.3505 217.588 22.3505C217.925 22.3505 218.28 22.2894 218.655 22.1671C219.029 22.0371 219.373 21.8116 219.687 21.4906C220.008 21.1619 220.271 20.7185 220.478 20.1605C220.684 19.6025 220.787 18.8916 220.787 18.0278ZM222.645 18.0049C222.645 18.7616 222.568 19.4343 222.416 20.0229C222.27 20.6039 222.075 21.1122 221.831 21.5479C221.586 21.9836 221.3 22.3505 220.971 22.6486C220.642 22.9391 220.302 23.1761 219.95 23.3595C219.599 23.5353 219.24 23.6615 218.873 23.7379C218.513 23.822 218.173 23.864 217.852 23.864C217.187 23.864 216.61 23.757 216.121 23.543C215.632 23.3213 215.192 22.9964 214.802 22.5684H214.768V28.9091H212.99V12.6846H214.332L214.71 13.923H214.745C214.844 13.7701 214.97 13.621 215.123 13.4758C215.284 13.3305 215.494 13.1777 215.754 13.0171C216.021 12.849 216.339 12.7037 216.706 12.5814C217.08 12.4591 217.485 12.398 217.921 12.398C218.532 12.398 219.121 12.5012 219.687 12.7076C220.26 12.9139 220.764 13.2426 221.2 13.6936C221.636 14.1446 221.984 14.7256 222.244 15.4365C222.511 16.1397 222.645 16.9959 222.645 18.0049Z" fill="black"/>
<path d="M231.371 22.3734H231.325C231.18 22.5569 230.997 22.7404 230.775 22.9238C230.553 23.0996 230.297 23.2602 230.007 23.4054C229.716 23.543 229.399 23.6538 229.055 23.7379C228.711 23.822 228.344 23.864 227.954 23.864C227.427 23.864 226.941 23.7838 226.498 23.6232C226.062 23.4704 225.684 23.2449 225.363 22.9468C225.049 22.641 224.805 22.2703 224.629 21.8345C224.453 21.3988 224.365 20.9058 224.365 20.3554C224.365 19.8127 224.461 19.3273 224.652 18.8992C224.851 18.4712 225.126 18.1081 225.478 17.81C225.829 17.5118 226.25 17.2825 226.739 17.122C227.236 16.9615 227.782 16.8812 228.378 16.8812C228.944 16.8888 229.468 16.9385 229.949 17.0303C230.431 17.122 230.859 17.2366 231.233 17.3742H231.279V16.6175C231.279 16.3194 231.26 16.0556 231.222 15.8263C231.184 15.597 231.119 15.3944 231.027 15.2186C230.828 14.8517 230.511 14.5498 230.075 14.3128C229.64 14.0682 229.051 13.9459 228.31 13.9459C227.759 13.9459 227.239 13.9994 226.75 14.1064C226.269 14.2058 225.787 14.3472 225.306 14.5307V13.0401C225.489 12.9483 225.703 12.8643 225.948 12.7878C226.2 12.7037 226.464 12.6349 226.739 12.5814C227.014 12.5203 227.304 12.4744 227.61 12.4438C227.916 12.4133 228.222 12.398 228.527 12.398C229.628 12.398 230.526 12.5929 231.222 12.9827C231.925 13.3726 232.43 13.8886 232.736 14.5307C232.85 14.7753 232.93 15.0466 232.976 15.3447C233.03 15.6352 233.057 15.9563 233.057 16.3079V23.5774H231.566L231.371 22.3734ZM231.279 18.796C230.951 18.7043 230.561 18.6164 230.11 18.5323C229.659 18.4482 229.162 18.4024 228.619 18.3947C227.862 18.3947 227.27 18.5552 226.842 18.8763C226.414 19.1973 226.2 19.6904 226.2 20.3554C226.2 20.6918 226.253 20.9861 226.36 21.2383C226.467 21.4906 226.613 21.7008 226.796 21.8689C226.987 22.0295 227.209 22.1518 227.461 22.2359C227.721 22.3123 227.996 22.3505 228.287 22.3505C228.669 22.3505 229.024 22.297 229.353 22.19C229.682 22.0753 229.972 21.9416 230.224 21.7887C230.484 21.6358 230.702 21.4829 230.878 21.33C231.061 21.1772 231.195 21.0587 231.279 20.9746V18.796Z" fill="black"/>
<path d="M244.936 23.5774H243.159V16.5716C243.159 16.0977 243.105 15.6964 242.998 15.3677C242.899 15.0313 242.75 14.7562 242.551 14.5421C242.36 14.3281 242.127 14.1714 241.852 14.072C241.584 13.9726 241.282 13.923 240.946 13.923C240.311 13.923 239.734 14.0949 239.214 14.4389C238.695 14.7753 238.228 15.2416 237.815 15.8378V23.5774H236.038V12.6846H237.414L237.758 14.1981H237.793C237.991 13.9459 238.205 13.7127 238.435 13.4987C238.672 13.277 238.931 13.0859 239.214 12.9254C239.505 12.7572 239.822 12.6273 240.166 12.5356C240.51 12.4438 240.885 12.398 241.29 12.398C242.1 12.398 242.773 12.5929 243.308 12.9827C243.843 13.3726 244.256 13.9038 244.546 14.5765H244.592C245.073 13.8962 245.624 13.3649 246.243 12.9827C246.862 12.5929 247.585 12.398 248.41 12.398C248.792 12.398 249.197 12.4515 249.625 12.5585C250.054 12.6655 250.447 12.8643 250.807 13.1547C251.166 13.4376 251.464 13.8312 251.701 14.3357C251.938 14.8402 252.056 15.49 252.056 16.285V23.5774H250.291V16.5716C250.291 16.0977 250.237 15.6964 250.13 15.3677C250.031 15.0313 249.882 14.7562 249.683 14.5421C249.492 14.3281 249.259 14.1714 248.983 14.072C248.716 13.9726 248.414 13.923 248.078 13.923C247.443 13.923 246.862 14.0949 246.335 14.4389C245.815 14.7753 245.349 15.2416 244.936 15.8378V23.5774Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,3 @@
<svg width="14" height="17" viewBox="0 0 14 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9.3584 2.68018C9.85059 2.08545 10.1992 1.26514 10.1992 0.444824C10.1992 0.332031 10.189 0.219238 10.1685 0.126953C9.36865 0.157715 8.39453 0.660156 7.82031 1.33691C7.35889 1.85986 6.93848 2.68018 6.93848 3.51074C6.93848 3.63379 6.95898 3.75684 6.96924 3.79785C7.02051 3.80811 7.10254 3.81836 7.18457 3.81836C7.9126 3.81836 8.8252 3.33643 9.3584 2.68018ZM9.92236 3.99268C8.7124 3.99268 7.72803 4.73096 7.10254 4.73096C6.43604 4.73096 5.5542 4.03369 4.49805 4.03369C2.49854 4.03369 0.478516 5.69482 0.478516 8.80176C0.478516 10.75 1.22705 12.7905 2.1499 14.1133C2.94971 15.231 3.64697 16.1538 4.6416 16.1538C5.63623 16.1538 6.06689 15.4873 7.29736 15.4873C8.54834 15.4873 8.83545 16.1333 9.92236 16.1333C11.0093 16.1333 11.7373 15.1387 12.4141 14.1543C13.1831 13.0264 13.501 11.9395 13.5112 11.8779C13.4497 11.8574 11.3682 11.0166 11.3682 8.6377C11.3682 6.57666 13.0088 5.66406 13.1011 5.59229C12.0244 4.03369 10.3838 3.99268 9.92236 3.99268Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 42 28.9" style="enable-background:new 0 0 42 28.9;" xml:space="preserve">
<style type="text/css">
.st0{fill-rule:evenodd;clip-rule:evenodd;}
</style>
<path class="st0" d="M10.2,8.7c-0.1,0-0.3,0-0.4,0C4.4,8.7,0,13.2,0,18.8s4.4,10.1,9.8,10.1l0.1,0h23.7l0,0c4.6,0,8.4-3.9,8.4-8.7
c0-4.3-3-7.9-7-8.5l0-0.1C35,5.2,29.4,0,22.4,0C16.5,0,11.6,3.7,10.2,8.7L10.2,8.7z M31,15l0-3.4l0-0.1c0-3.9-3.5-7.6-8.7-7.6
c-4.3,0-7.5,2.7-8.4,5.8l-0.8,3L10,12.6c-0.1,0-0.2,0-0.2,0c-3.1,0-5.9,2.6-5.9,6.2c0,3.5,2.7,6.2,5.9,6.2l0.1,0h23.7l0,0
c2.4,0,4.5-2,4.5-4.7c0-2.4-1.7-4.3-3.7-4.7L31,15z"/>
</svg>

After

Width:  |  Height:  |  Size: 856 B

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 36 36" style="enable-background:new 0 0 36 36;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FFFFFF;stroke:#FFFFFF;stroke-width:2;}
.st1{clip-path:url(#SVGID_00000131361309518468547900000009062464345318814348_);}
.st2{fill:#F6F7F7;filter:url(#Adobe_OpacityMaskFilter);}
.st3{mask:url(#mask0_2900_10877_00000159448472586931246160000013733448166778715831_);}
.st4{fill:#F0F2EB;}
.st5{fill-rule:evenodd;clip-rule:evenodd;fill:#646970;}
</style>
<circle class="st0" cx="18" cy="18" r="17"/>
<g>
<defs>
<rect id="SVGID_1_" width="36" height="36"/>
</defs>
<clipPath id="SVGID_00000143603845024951162640000013916798552048052664_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000143603845024951162640000013916798552048052664_);">
<defs>
<filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="0" y="0" width="36" height="36">
<feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
</filter>
</defs>
<mask maskUnits="userSpaceOnUse" x="0" y="0" width="36" height="36" id="mask0_2900_10877_00000159448472586931246160000013733448166778715831_">
<circle class="st2" cx="18" cy="18" r="18"/>
</mask>
<g class="st3">
<circle class="st4" cx="18" cy="18" r="18"/>
<path class="st5" d="M12.7,23c-1.2-1.3-2-3.1-2-5c0-4,3.3-7.3,7.3-7.3s7.3,3.3,7.3,7.3c0,1.9-0.7,3.7-2,5v-0.5
c0-1.7-1.4-3.1-3.1-3.1h-4.5c-1.7,0-3.1,1.4-3.1,3.1V23z M14.3,24.3c1.1,0.6,2.3,1,3.7,1s2.6-0.4,3.7-1v-1.8
c0-0.8-0.6-1.4-1.4-1.4h-4.5c-0.8,0-1.4,0.6-1.4,1.4V24.3z M9,18c0-5,4-9,9-9s9,4,9,9s-4,9-9,9S9,23,9,18z M20.2,15.8
c0,1.2-1,2.2-2.2,2.2s-2.2-1-2.2-2.2s1-2.2,2.2-2.2S20.2,14.5,20.2,15.8z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.0083 0C4.47395 0 0 4.58332 0 10.2535C0 14.786 2.86662 18.6226 6.84337 19.9805C7.34057 20.0826 7.52269 19.7599 7.52269 19.4885C7.52269 19.2508 7.5063 18.436 7.5063 17.5871C4.72224 18.1983 4.14249 16.3648 4.14249 16.3648C3.69507 15.1764 3.03214 14.871 3.03214 14.871C2.12092 14.2428 3.09852 14.2428 3.09852 14.2428C4.1093 14.3107 4.63969 15.2954 4.63969 15.2954C5.53431 16.857 6.97592 16.4158 7.55588 16.1442C7.63864 15.482 7.90393 15.0237 8.18562 14.7691C5.96514 14.5315 3.62891 13.6487 3.62891 9.71017C3.62891 8.58976 4.02633 7.67309 4.65608 6.96018C4.55672 6.7056 4.20866 5.65289 4.75563 4.24394C4.75563 4.24394 5.60069 3.97227 7.50609 5.29644C8.32187 5.072 9.16316 4.95781 10.0083 4.95686C10.8533 4.95686 11.7148 5.07581 12.5102 5.29644C14.4159 3.97227 15.2609 4.24394 15.2609 4.24394C15.8079 5.65289 15.4596 6.7056 15.3602 6.96018C16.0066 7.67309 16.3876 8.58976 16.3876 9.71017C16.3876 13.6487 14.0514 14.5144 11.8143 14.7691C12.1789 15.0916 12.4936 15.7026 12.4936 16.6704C12.4936 18.0454 12.4772 19.1489 12.4772 19.4883C12.4772 19.7599 12.6596 20.0826 13.1566 19.9808C17.1334 18.6224 20 14.786 20 10.2535C20.0163 4.58332 15.526 0 10.0083 0Z" fill="#24292F"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg class="social-icons social-icons__google social-icons--enabled" width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><path d="M19.6 10.227c0-.709-.064-1.39-.182-2.045H10v3.868h5.382a4.6 4.6 0 0 1-1.996 3.018v2.51h3.232c1.891-1.742 2.982-4.305 2.982-7.35z" fill="#4285F4"></path><path d="M10 20c2.7 0 4.964-.895 6.618-2.423l-3.232-2.509c-.895.6-2.04.955-3.386.955-2.605 0-4.81-1.76-5.595-4.123H1.064v2.59A9.996 9.996 0 0 0 10 20z" fill="#34A853"></path><path d="M4.405 11.9c-.2-.6-.314-1.24-.314-1.9 0-.66.114-1.3.314-1.9V5.51H1.064A9.996 9.996 0 0 0 0 10c0 1.614.386 3.14 1.064 4.49l3.34-2.59z" fill="#FBBC05"></path><path d="M10 3.977c1.468 0 2.786.505 3.823 1.496l2.868-2.868C14.959.99 12.695 0 10 0 6.09 0 2.71 2.24 1.064 5.51l3.34 2.59C5.192 5.736 7.396 3.977 10 3.977z" fill="#EA4335"></path></g></svg>

After

Width:  |  Height:  |  Size: 873 B

@@ -0,0 +1,17 @@
<svg width="380" height="183" viewBox="0 0 380 183" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.75" filter="url(#filter0_f_5953_11924)">
<path d="M360 -60C360 7.93102 304.931 63 237 63C169.069 63 114 7.93102 114 -60C114 -127.931 169.069 -183 237 -183C304.931 -183 360 -127.931 360 -60Z" fill="url(#paint0_linear_5953_11924)"/>
</g>
<defs>
<filter id="filter0_f_5953_11924" x="-6" y="-303" width="486" height="486" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="60" result="effect1_foregroundBlur_5953_11924"/>
</filter>
<linearGradient id="paint0_linear_5953_11924" x1="360" y1="-60" x2="114" y2="-60" gradientUnits="userSpaceOnUse">
<stop stop-color="#48FF50"/>
<stop offset="0.471846" stop-color="#108642"/>
<stop offset="1" stop-color="#2E38FA"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 990 B

@@ -0,0 +1,10 @@
<svg width="105" height="105" viewBox="0 0 105 105" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0.190476" y="0.19043" width="103.91" height="104" rx="12.1905" fill="#2C3338"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M32.1456 33.3356C32.1456 32.6707 32.6857 32.1306 33.3506 32.1306H60.9406C61.6055 32.1306 62.1456 32.6707 62.1456 33.3356V36.1306H64.1456V33.3356C64.1456 31.5661 62.7101 30.1306 60.9406 30.1306H33.3506C31.5812 30.1306 30.1456 31.5661 30.1456 33.3356V60.9256C30.1456 62.6951 31.5812 64.1306 33.3506 64.1306H36.1456V62.1306H33.3506C32.6857 62.1306 32.1456 61.5905 32.1456 60.9256V33.3356Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M37.1456 38.3356C37.1456 37.6701 37.6851 37.1306 38.3506 37.1306H65.9406C66.6061 37.1306 67.1456 37.6701 67.1456 38.3356V41.1306H69.1456V38.3356C69.1456 36.5655 67.7107 35.1306 65.9406 35.1306H38.3506C36.5806 35.1306 35.1456 36.5655 35.1456 38.3356V65.9256C35.1456 67.6957 36.5806 69.1306 38.3506 69.1306H41.1456V67.1306H38.3506C37.6851 67.1306 37.1456 66.5911 37.1456 65.9256V38.3356Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M43.3506 42.1306C42.6851 42.1306 42.1456 42.6701 42.1456 43.3356V70.9256C42.1456 71.5911 42.6851 72.1306 43.3506 72.1306H70.9406C71.6061 72.1306 72.1456 71.5911 72.1456 70.9256V43.3356C72.1456 42.6701 71.6061 42.1306 70.9406 42.1306H43.3506ZM40.1456 43.3356C40.1456 41.5655 41.5805 40.1306 43.3506 40.1306H70.9406C72.7107 40.1306 74.1456 41.5655 74.1456 43.3356V70.9256C74.1456 72.6957 72.7107 74.1306 70.9406 74.1306H43.3506C41.5805 74.1306 40.1456 72.6957 40.1456 70.9256V43.3356Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M66.7775 63.5483H47.1456V61.7073H66.7775V63.5483Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M47.1456 51.2786H66.7775V53.1195H47.1456V51.2786Z" fill="white"/>
<path d="M60.029 65.6962C61.7236 65.6962 63.0973 64.3224 63.0973 62.6279C63.0973 60.9333 61.7236 59.5596 60.029 59.5596C58.3344 59.5596 56.9607 60.9333 56.9607 62.6279C56.9607 64.3224 58.3344 65.6962 60.029 65.6962Z" fill="white"/>
<path d="M53.8941 55.2672C55.5887 55.2672 56.9624 53.8935 56.9624 52.1989C56.9624 50.5043 55.5887 49.1306 53.8941 49.1306C52.1996 49.1306 50.8258 50.5043 50.8258 52.1989C50.8258 53.8935 52.1996 55.2672 53.8941 55.2672Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_00000115494289508776443440000006419494924222942340_);}
.st1{stroke:#FFFFFF;stroke-width:2;}
.st2{fill:#FFFFFF;}
</style>
<g>
<defs>
<rect id="SVGID_1_" x="0" y="0" width="32" height="32"/>
</defs>
<clipPath id="SVGID_00000114031482566589492640000009417605807093364883_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000114031482566589492640000009417605807093364883_);">
<path class="st1" d="M16,33c9.4,0,17-7.6,17-17c0-9.4-7.6-17-17-17C6.6-1-1,6.6-1,16C-1,25.4,6.6,33,16,33z"/>
<path class="st2" d="M16.8,13.3v15.5l8-15.5H16.8z"/>
<path class="st2" d="M15.2,18.7V3.2l-8,15.5H15.2z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;fill:#C4C4C4" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_00000115494289508776443440000006419494924222942340_);}
.st1{stroke:#FFFFFF;stroke-width:2;}
.st2{fill:#FFFFFF;}
</style>
<g>
<defs>
<rect id="SVGID_1_" x="0" y="0" width="32" height="32"/>
</defs>
<clipPath id="SVGID_00000114031482566589492640000009417605807093364883_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000114031482566589492640000009417605807093364883_);">
<path class="st1" d="M16,33c9.4,0,17-7.6,17-17c0-9.4-7.6-17-17-17C6.6-1-1,6.6-1,16C-1,25.4,6.6,33,16,33z"/>
<path class="st2" d="M16.8,13.3v15.5l8-15.5H16.8z"/>
<path class="st2" d="M15.2,18.7V3.2l-8,15.5H15.2z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@@ -0,0 +1,3 @@
<svg width="22" height="26" viewBox="0 0 22 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.1538 1L21.3077 5.45957V12.1076C21.3077 17.7747 17.5408 23.1405 12.2289 24.8332C11.531 25.0556 10.7767 25.0556 10.0787 24.8332C4.76694 23.1405 1 17.7747 1 12.1076V5.45957L11.1538 1Z" stroke="#A7AAAD"/>
</svg>

After

Width:  |  Height:  |  Size: 357 B

@@ -0,0 +1,5 @@
<svg width="21" height="24" viewBox="0 0 21 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1538 0L20.3077 4.45957V11.1076C20.3077 16.7747 16.5408 22.1405 11.2289 23.8332C10.531 24.0556 9.77674 24.0556 9.07875 23.8332C3.76694 22.1405 0 16.7747 0 11.1076V4.45957L10.1538 0Z" fill="#A7AAAD"/>
<rect x="8.63086" y="15.5" width="3.04615" height="3" rx="1.5" fill="white"/>
<path d="M8.69834 7.06333C8.66178 6.48729 9.11913 6 9.69633 6H10.6115C11.1887 6 11.6461 6.48729 11.6095 7.06333L11.2288 13.0633C11.1953 13.59 10.7585 14 10.2308 14H10.0771C9.54941 14 9.11253 13.59 9.07911 13.0633L8.69834 7.06333Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 679 B

@@ -0,0 +1,4 @@
<svg width="21" height="23" viewBox="0 0 21 23" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.3077 0.541687L19.6154 4.62962V10.7237C19.6154 15.9185 16.1624 20.8371 11.2932 22.3888C10.6534 22.5927 9.96201 22.5927 9.32218 22.3888C4.45303 20.8371 1 15.9185 1 10.7237V4.62962L10.3077 0.541687Z" fill="white" stroke="#00A32A" stroke-width="0.916667" stroke-dasharray="3.67 0.92"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.1708 8.28525L9.23134 16.1521L5.46863 13.3968L6.30169 12.2933L8.94402 14.2283L14.0504 7.46481L15.1708 8.28525Z" fill="#069E08"/>
</svg>

After

Width:  |  Height:  |  Size: 619 B

@@ -0,0 +1,4 @@
<svg width="21" height="25" viewBox="0 0 21 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M10.1538 0.5L20.3077 4.95957V11.6076C20.3077 17.2747 16.5408 22.6405 11.2289 24.3332C10.531 24.5556 9.77674 24.5556 9.07875 24.3332C3.76694 22.6405 0 17.2747 0 11.6076V4.95957L10.1538 0.5Z" fill="#069E08"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M15.4591 8.94752L8.97971 17.5296L4.87493 14.5237L5.78373 13.32L8.66627 15.4308L14.2369 8.05249L15.4591 8.94752Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

@@ -0,0 +1 @@
<svg width="110" height="18" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)" fill="#000"><path d="M79.42 9.335c0-.484.523-.672 1.085-.672.908 0 1.769.282 1.769.282V7.628c-.59-.174-1.193-.241-1.89-.241-1.528 0-2.6.725-2.6 1.908 0 2.299 3.162 1.708 3.162 2.823 0 .551-.495.7-1.218.7-.51 0-1.327-.2-1.916-.406v1.304c.49.195 1.18.378 1.862.378 1.487 0 2.907-.444 2.907-2.043 0-2.218-3.162-1.64-3.162-2.715l.001-.001Zm8.337 1.345c0-1.491.817-2.003 1.783-2.003.643 0 1.32.19 1.774.364V7.696c-.536-.174-1.01-.31-1.802-.31-2.104 0-3.404 1.291-3.404 3.375 0 1.975.938 3.333 3.31 3.333.75 0 1.333-.148 1.976-.364v-1.317c-.723.282-1.279.39-1.775.39-1.044 0-1.862-.484-1.862-2.124v.001ZM42.273 8.596h-.054V7.548h-1.474v6.384h1.581v-3.804c.496-.94 1.099-1.25 2.105-1.25h.16V7.494s-.227-.027-.428-.027c-.858 0-1.448.336-1.89 1.13Zm19.837 0h-.053V7.548h-1.474v6.384h1.58v-3.804c.497-.94 1.1-1.25 2.105-1.25h.16V7.494s-.227-.027-.428-.027c-.857 0-1.447.336-1.889 1.13Zm45.76-1.21c-.682 0-1.46.377-2.171.848l-.187.12c-.335-.712-1.005-.967-1.742-.967-.683 0-1.461.35-2.171.82v-.66h-1.501v6.385h1.594V9.35c.59-.39 1.219-.632 1.676-.632.523 0 .844.282.844 1.183v4.032h1.581v-4.57c.59-.39 1.219-.645 1.676-.645.523 0 .845.282.845 1.183v4.032h1.581V9.524c0-1.237-.738-2.137-2.024-2.137h-.001Zm-12.262 5.431c-1.032 0-1.58-.671-1.58-2.07 0-1.398.548-2.07 1.58-2.07s1.568.673 1.568 2.07c0 1.398-.536 2.07-1.568 2.07Zm0-5.43c-2.184 0-3.216 1.411-3.216 3.36 0 1.95 1.032 3.347 3.216 3.347s3.216-1.399 3.216-3.347-1.045-3.36-3.216-3.36ZM73.664 9.336c0-.484.523-.672 1.086-.672.907 0 1.768.282 1.768.282V7.63c-.59-.174-1.193-.241-1.889-.241-1.527 0-2.6.725-2.6 1.908 0 2.299 3.163 1.708 3.163 2.823 0 .551-.496.7-1.22.7-.508 0-1.325-.2-1.915-.406v1.304c.491.195 1.178.378 1.862.378 1.487 0 2.908-.444 2.908-2.043 0-2.218-3.163-1.64-3.163-2.715v-.001Zm-6.86.7c.094-.887.576-1.452 1.366-1.452.845 0 1.099.673 1.099 1.452h-2.466Zm1.38-2.648c-2.037 0-3.096 1.586-3.096 3.32 0 2.38 1.245 3.387 3.267 3.387.817 0 1.523-.109 2.347-.363v-1.32c-.664.244-1.263.391-1.905.391-1.178 0-1.967-.323-2.008-1.72h4.074c.027-.256.054-.497.054-.94 0-1.345-.71-2.756-2.734-2.756ZM56.3 9.483h-1.254V6.257H56.3c1.019 0 1.527.564 1.527 1.573s-.469 1.653-1.527 1.653Zm-.013-4.556h-2.928v9.004h1.688V10.8h1.24c1.916 0 3.31-1.035 3.31-2.97 0-1.935-1.394-2.903-3.31-2.903ZM49.82 12.09c-.51.47-.992.725-1.5.725-.871 0-1.435-.578-1.435-1.935 0-1.465.697-2.231 1.903-2.231.375 0 .764.094 1.032.202v3.239Zm0-4.557c-.47-.094-.805-.149-1.193-.149-2.184 0-3.377 1.466-3.377 3.603 0 2.137 1.193 3.105 2.6 3.105.697 0 1.54-.323 2.05-.753v.591h1.5V4.926h-1.58v2.608Zm-13.565 5.282c-1.031 0-1.58-.671-1.58-2.07 0-1.398.549-2.07 1.58-2.07 1.032 0 1.569.673 1.569 2.07 0 1.397-.537 2.07-1.569 2.07Zm0-5.43c-2.184 0-3.216 1.41-3.216 3.36s1.032 3.347 3.216 3.347c2.185 0 3.216-1.399 3.216-3.347s-1.044-3.36-3.215-3.36Zm47.372 6.545h1.587v-1.64h-1.587v1.64ZM31.608 4.926 29.82 11.82 28.03 4.926h-1.805l.42 1.477-1.575 5.65-1.712-7.128H21.62l2.441 9.005h1.892l1.462-4.815 1.37 4.815h1.892l2.563-9.005h-1.634.001ZM8.973 0C4.025 0 0 4.037 0 9s4.025 9 8.973 9 8.973-4.038 8.973-9-4.026-9-8.973-9ZM.906 9c0-1.173.25-2.287.698-3.293l3.849 10.575A8.095 8.095 0 0 1 .906 9Zm8.067 8.092a8.052 8.052 0 0 1-2.28-.329l2.42-7.055 2.48 6.814c.017.04.037.076.059.112a8.02 8.02 0 0 1-2.68.458Zm1.111-11.885a16 16 0 0 0 .924-.077c.435-.052.383-.693-.051-.667 0 0-1.307.103-2.15.103-.793 0-2.126-.103-2.126-.103-.435-.026-.485.64-.051.667 0 0 .411.052.846.077L8.733 8.66l-1.766 5.311L4.03 5.206c.486-.026.924-.077.924-.077.434-.052.383-.693-.052-.667 0 0-1.306.103-2.15.103-.151 0-.33-.004-.519-.01A8.049 8.049 0 0 1 14.42 3.033c-.035-.003-.069-.007-.105-.007-.792 0-1.355.693-1.355 1.436 0 .667.384 1.231.792 1.897.307.54.666 1.232.666 2.232 0 .692-.266 1.496-.614 2.615l-.806 2.697-2.915-8.698.002.002Zm5.967-.088a8.077 8.077 0 0 1 .989 3.882 8.09 8.09 0 0 1-4.012 6.994l2.464-7.146c.461-1.155.614-2.078.614-2.9 0-.297-.02-.573-.055-.831v.001Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h109.895v18H0z"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

@@ -0,0 +1 @@
<?php return array('dependencies' => array('jetpack-connection', 'jetpack-script-data', 'jetpack-shared-stores', 'react', 'react-dom', 'react-jsx-runtime', 'wp-a11y', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-data', 'wp-date', 'wp-element', 'wp-i18n', 'wp-keycodes', 'wp-notices', 'wp-polyfill', 'wp-primitives', 'wp-private-apis', 'wp-url', 'wp-warning'), 'version' => '3a5cf7ed13493836367e');
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
/*!
Copyright (c) 2018 Jed Watson.
Licensed under the MIT License (MIT), see
http://jedwatson.github.io/classnames
*/
/**
* @license React
* use-sync-external-store-shim.production.js
*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/**
* react-router v7.15.1
*
* Copyright (c) Remix Software Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE.md file in the root directory of this source tree.
*
* @license MIT
*/
File diff suppressed because one or more lines are too long
@@ -0,0 +1,112 @@
<?php
/**
* Sets up the Historically Active Modules rest api endpoint and helper functions
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use WP_Error;
/**
* Registers REST route for updating historically active modules
* and includes all helper functions for triggering an update elsewhere
*/
class Historically_Active_Modules {
public const UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY = 'update-historically-active-jetpack-modules';
/**
* Register the REST API routes.
*
* @return void
*/
public static function register_rest_endpoints() {
register_rest_route(
'my-jetpack/v1',
'site/update-historically-active-modules',
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::rest_trigger_historically_active_modules_update',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Check user capabilities to access historically active modules.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
return current_user_can( 'edit_posts' );
}
/**
* Update historically active Jetpack plugins
* Historically active is defined as the Jetpack plugins that are installed and active with the required connections
* This array will consist of any plugins that were active at one point in time and are still enabled on the site
*
* @return void
*/
public static function update_historically_active_jetpack_modules() {
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
$products = Products::get_products();
$product_classes = Products::get_products_classes();
foreach ( $products as $product ) {
$product_slug = $product['slug'];
$status = $product_classes[ $product_slug ]::get_status();
// We want to leave modules in the array if they've been active in the past
// and were not manually disabled by the user.
if ( in_array( $status, Products::$broken_module_statuses, true ) ) {
continue;
}
// If the module is active and not already in the array, add it
if (
in_array( $status, Products::$active_module_statuses, true ) &&
! in_array( $product_slug, $historically_active_modules, true )
) {
$historically_active_modules[] = $product_slug;
}
// If the module has been disabled due to a manual user action,
// or because of a missing plan error, remove it from the array
if ( in_array( $status, Products::$disabled_module_statuses, true ) ) {
$historically_active_modules = array_values( array_diff( $historically_active_modules, array( $product_slug ) ) );
}
}
\Jetpack_Options::update_option( 'historically_active_modules', array_unique( $historically_active_modules ) );
}
/**
* REST API endpoint to trigger an update to the historically active Jetpack modules
*
* @return WP_Error|\WP_REST_Response
*/
public static function rest_trigger_historically_active_modules_update() {
self::update_historically_active_jetpack_modules();
$historically_active_modules = \Jetpack_Options::get_option( 'historically_active_modules', array() );
return rest_ensure_response( $historically_active_modules );
}
/**
* Set transient to queue an update to the historically active Jetpack modules on the next wp-admin load
*
* @param string $plugin The plugin that triggered the update. This will be present if the function was queued by a plugin activation.
*
* @return void
*/
public static function queue_historically_active_jetpack_modules_update( $plugin = null ) {
$plugin_filenames = Products::get_all_plugin_filenames();
if ( ! $plugin || in_array( $plugin, $plugin_filenames, true ) ) {
set_transient( self::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY, true );
}
}
}
@@ -0,0 +1,828 @@
<?php
/**
* WP Admin page with information and configuration shared among all Jetpack stand-alone plugins
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Agents_Manager\WP_REST_Jetpack_AI_JWT;
use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score;
use Automattic\Jetpack\Boost_Speed_Score\Speed_Score_History;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Connection\Rest_Authentication as Connection_Rest_Authentication;
use Automattic\Jetpack\Constants as Jetpack_Constants;
use Automattic\Jetpack\ExPlat;
use Automattic\Jetpack\JITMS\JITM;
use Automattic\Jetpack\Licensing;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Plugins_Installer;
use Automattic\Jetpack\Status;
use Automattic\Jetpack\Status\Host as Status_Host;
use Automattic\Jetpack\Sync\Functions as Sync_Functions;
use Automattic\Jetpack\Terms_Of_Service;
use Automattic\Jetpack\Tracking;
use Jetpack;
use WP_Error;
/**
* The main Initializer class that registers the admin menu and eneuque the assets.
*/
class Initializer {
/**
* My Jetpack package version
*
* @var string
*/
const PACKAGE_VERSION = '5.40.4';
/**
* HTML container ID for the IDC screen on My Jetpack page.
*/
private const IDC_CONTAINER_ID = 'my-jetpack-identity-crisis-container';
public const JETPACK_PLUGIN_SLUGS = array(
'jetpack-backup',
'jetpack-boost',
'zerobscrm',
'jetpack',
'jetpack-protect',
'jetpack-social',
'jetpack-videopress',
'jetpack-search',
);
private const MY_JETPACK_SITE_INFO_TRANSIENT_KEY = 'my-jetpack-site-info';
/**
* Holds info/data about the site (from the /sites/%d endpoint)
*
* @var object
*/
public static $site_info;
/**
* Initialize My Jetpack
*
* @return void
*/
public static function init() {
if ( ! self::should_initialize() || did_action( 'my_jetpack_init' ) ) {
return;
}
// Extend jetpack plugins action links.
Products::extend_plugins_action_links();
// Set up the REST authentication hooks.
Connection_Rest_Authentication::init();
if ( self::is_licensing_ui_enabled() ) {
Licensing::instance()->initialize();
}
// Initialize Boost Speed Score
new Speed_Score( array(), 'jetpack-my-jetpack' );
// Add custom WP REST API endoints.
add_action( 'rest_api_init', array( __CLASS__, 'register_rest_endpoints' ) );
add_action( 'admin_menu', array( __CLASS__, 'add_my_jetpack_menu_item' ) );
add_action( 'admin_init', array( __CLASS__, 'setup_historically_active_jetpack_modules_sync' ) );
// This is later than the admin-ui package, which runs on 1000
add_action( 'admin_init', array( __CLASS__, 'maybe_show_red_bubble' ), 1001 );
// Set up the ExPlat package endpoints
ExPlat::init();
// Sets up JITMS.
JITM::configure();
// Add "Jetpack Manage" menu item.
Jetpack_Manage::init();
/**
* Fires after the My Jetpack package is initialized
*
* @since 0.1.0
*/
do_action( 'my_jetpack_init' );
}
/**
* Acts as a feature flag, returning a boolean for whether we should show the licensing UI.
*
* @since 1.2.0
*
* @return boolean
*/
public static function is_licensing_ui_enabled() {
// Default changed to true in 1.5.0.
$is_enabled = true;
/*
* Bail if My Jetpack is not enabled,
* and thus the licensing UI shouldn't be enabled either.
*/
if ( ! self::should_initialize() ) {
$is_enabled = false;
}
/**
* Acts as a feature flag, returning a boolean for whether we should show the licensing UI.
*
* @param bool $is_enabled Defaults to true.
*
* @since 1.2.0
* @since 1.5.0 Update default value to true.
*/
return apply_filters(
'jetpack_my_jetpack_should_enable_add_license_screen',
$is_enabled
);
}
/**
* Add My Jetpack menu item to the admin menu.
*
* @return void
*/
public static function add_my_jetpack_menu_item() {
$page_suffix = Admin_Menu::add_menu(
__( 'My Jetpack', 'jetpack-my-jetpack' ),
__( 'My Jetpack', 'jetpack-my-jetpack' ),
'edit_posts',
'my-jetpack',
array( __CLASS__, 'admin_page' ),
-1
);
add_action( 'load-' . $page_suffix, array( __CLASS__, 'admin_init' ) );
}
/**
* Callback for the load my jetpack page hook.
*
* @return void
*/
public static function admin_init() {
$connection = new Connection_Manager();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- No nonce needed for redirect flow control
$step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : '';
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checking for partner coupon redemption flow
$show_coupon_redemption = isset( $_GET['showCouponRedemption'] );
// Redirect to Jetpack dashboard for partner coupon redemption
if ( $show_coupon_redemption ) {
wp_safe_redirect( admin_url( 'admin.php?page=jetpack&showCouponRedemption=1#/dashboard' ) );
exit( 0 );
}
// Handle onboarding redirects based on connection status
$should_redirect = false;
$redirect_args = array( 'page' => 'my-jetpack' );
if ( ! $connection->is_connected() && $step !== 'onboarding' ) {
// Redirect to onboarding if not connected
$redirect_args['step'] = 'onboarding';
$should_redirect = true;
} elseif ( $connection->is_connected() && $step === 'onboarding' ) {
// Redirect away from onboarding if already connected
$should_redirect = true;
}
if ( $should_redirect ) {
$admin_page = add_query_arg( $redirect_args, admin_url( 'admin.php' ) );
$location = wp_sanitize_redirect( $admin_page );
// Remove wp_get_referer filter applied in `fix_redirect` method of `Jetpack_Admin` class
remove_filter( 'wp_redirect', 'wp_get_referer' );
wp_safe_redirect( $location );
exit( 0 );
}
// If the user reaches the onboarding page, add a class to the body
if ( $step === 'onboarding' ) {
add_filter( 'admin_body_class', array( __CLASS__, 'add_onboarding_admin_body_class' ) );
}
self::$site_info = self::get_site_info();
add_filter( 'identity_crisis_container_id', array( static::class, 'get_idc_container_id' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_scripts' ) );
}
/**
* Add a body class to the My Jetpack onboarding page.
* This class hides the WP Admin toolbar and the sidebar menu.
*
* @param string $classes The body classes.
* @return string The modified body classes.
*/
public static function add_onboarding_admin_body_class( $classes ) {
$classes .= 'jetpack-admin-full-screen';
return $classes;
}
/**
* Returns whether we are in condition to track to use
* Analytics functionality like Tracks, MC, or GA.
*/
public static function can_use_analytics() {
$status = new Status();
$connection = new Connection_Manager();
$tracking = new Tracking( 'jetpack', $connection );
return $tracking->should_enable_tracking( new Terms_Of_Service(), $status );
}
/**
* Enqueue admin page assets.
*
* @return void
*/
public static function enqueue_scripts() {
/**
* Fires after the My Jetpack page is initialized.
* Allows for enqueuing additional scripts only on the My Jetpack page.
*
* @since 4.35.7
*/
do_action( 'myjetpack_enqueue_scripts' );
Assets::register_script(
'my_jetpack_main_app',
'../build/index.js',
__FILE__,
array(
'enqueue' => true,
'in_footer' => true,
'textdomain' => 'jetpack-my-jetpack',
)
);
$modules = new Modules();
$connection = new Connection_Manager();
$speed_score_history = new Speed_Score_History( get_site_url() );
$latest_score = $speed_score_history->latest();
$previous_score = array();
if ( $speed_score_history->count() > 1 ) {
$previous_score = $speed_score_history->latest( 1 );
}
$latest_score['previousScores'] = $previous_score['scores'] ?? array();
$sandboxed_domain = '';
$is_dev_version = false;
if ( class_exists( 'Jetpack' ) ) {
$is_dev_version = Jetpack::is_development_version();
$sandboxed_domain = defined( 'JETPACK__SANDBOX_DOMAIN' ) ? JETPACK__SANDBOX_DOMAIN : '';
}
wp_localize_script(
'my_jetpack_main_app',
'myJetpackInitialState',
array(
'products' => array(
'items' => Products::get_products(),
),
'plugins' => Plugins_Installer::get_plugins(),
'themes' => Sync_Functions::get_themes(),
'myJetpackUrl' => admin_url( 'admin.php?page=my-jetpack' ),
'myJetpackCheckoutUri' => admin_url( 'admin.php?page=my-jetpack' ),
'topJetpackMenuItemUrl' => Admin_Menu::get_top_level_menu_item_url(),
'siteSuffix' => ( new Status() )->get_site_suffix(),
'siteUrl' => esc_url( get_site_url() ),
'blogID' => Connection_Manager::get_site_id( true ),
'myJetpackVersion' => self::PACKAGE_VERSION,
'myJetpackFlags' => self::get_my_jetpack_flags(),
'fileSystemWriteAccess' => self::has_file_system_write_access(),
'loadAddLicenseScreen' => self::is_licensing_ui_enabled(),
'adminUrl' => esc_url( admin_url() ),
'IDCContainerID' => static::get_idc_container_id(),
'userIsAdmin' => current_user_can( 'manage_options' ),
'lifecycleStats' => array(
'jetpackPlugins' => self::get_installed_jetpack_plugins(),
'historicallyActiveModules' => \Jetpack_Options::get_option( 'historically_active_modules', array() ),
'brokenModules' => Red_Bubble_Notifications::check_for_broken_modules(),
'isSiteConnected' => $connection->is_connected(),
'isUserConnected' => $connection->is_user_connected(),
'modules' => self::get_active_modules(),
),
'recommendedModules' => array(
'modules' => self::get_recommended_modules(),
'isFirstRun' => \Jetpack_Options::get_option( 'recommendations_first_run', true ),
'dismissed' => \Jetpack_Options::get_option( 'dismissed_recommendations', false ),
),
'isStatsModuleActive' => $modules->is_active( 'stats' ),
'canUserViewStats' => current_user_can( 'manage_options' ) || current_user_can( 'view_stats' ),
'sandboxedDomain' => $sandboxed_domain,
'isDevVersion' => $is_dev_version,
'isAtomic' => ( new Status_Host() )->is_woa_site(),
'isJetpackPluginActive' => class_exists( 'Jetpack' ),
'latestBoostSpeedScores' => $latest_score,
'seoOptIn' => self::get_seo_opt_in_state(),
)
);
wp_localize_script(
'my_jetpack_main_app',
'myJetpackRest',
array(
'apiRoot' => esc_url_raw( rest_url() ),
'apiNonce' => wp_create_nonce( 'wp_rest' ),
)
);
// Connection Initial State.
Connection_Initial_State::render_script( 'my_jetpack_main_app' );
// Required for Analytics.
if ( self::can_use_analytics() ) {
Tracking::register_tracks_functions_scripts( true );
}
}
/**
* Get installed Jetpack plugins
*
* @return array
*/
public static function get_installed_jetpack_plugins() {
$plugin_slugs = array_keys( Plugins_Installer::get_plugins() );
$plugin_slugs = array_map(
static function ( $slug ) {
$parts = explode( '/', $slug );
// Return the last segment of the filepath without the PHP extension
return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
},
$plugin_slugs
);
return array_values( array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs ) );
}
/**
* Get active modules (except ones enabled by default)
*
* @return array
*/
public static function get_active_modules() {
$modules = new Modules();
$active_modules = $modules->get_active();
// if the Jetpack plugin is active, filter out the modules that are active by default
if ( class_exists( 'Jetpack' ) && ! empty( $active_modules ) ) {
$active_modules = array_diff( $active_modules, Jetpack::get_default_modules() );
}
return array_values( $active_modules );
}
/**
* Determine if the current user is "new" to Jetpack
* This is used to vary some messaging in My Jetpack
*
* On the front-end, purchases are also taken into account
*
* @return bool
*/
public static function is_jetpack_user_new() {
// is the user connected?
$connection = new Connection_Manager();
if ( $connection->is_user_connected() ) {
return false;
}
// TODO: add a data point for the last known connection/ disconnection time
// are any modules active?
$active_modules = self::get_active_modules();
if ( ! empty( $active_modules ) ) {
return false;
}
// check for other Jetpack plugins that are installed on the site (active or not)
// If there's more than one Jetpack plugin active, this user is not "new"
$plugin_slugs = array_keys( Plugins_Installer::get_plugins() );
$plugin_slugs = array_map(
static function ( $slug ) {
$parts = explode( '/', $slug );
// Return the last segment of the filepath without the PHP extension
return str_replace( '.php', '', $parts[ count( $parts ) - 1 ] );
},
$plugin_slugs
);
$installed_jetpack_plugins = array_intersect( self::JETPACK_PLUGIN_SLUGS, $plugin_slugs );
if ( is_countable( $installed_jetpack_plugins ) && count( $installed_jetpack_plugins ) >= 2 ) {
return false;
}
// Does the site have any purchases?
$purchases = Wpcom_Products::get_site_current_purchases();
if ( ! empty( $purchases ) && ! is_wp_error( $purchases ) ) {
return false;
}
return true;
}
/**
* Build flags for My Jetpack UI
*
* @return array
*/
public static function get_my_jetpack_flags() {
$flags = array(
'videoPressStats' => Jetpack_Constants::is_true( 'JETPACK_MY_JETPACK_VIDEOPRESS_STATS_ENABLED' ),
'showFullJetpackStatsCard' => class_exists( 'Jetpack' ),
);
return $flags;
}
/**
* Build the state the My Jetpack "try the new SEO experience" opt-in card hydrates from.
*
* The card invites an existing self-hosted install to switch over to the new Jetpack SEO
* dashboard (JETPACK-1700). Gating lives server-side, where the signals actually are. The
* card shows only when all of:
*
* - the new SEO product is available the `rsm_jetpack_seo` feature filter is on (the SEO
* package autoloads regardless, so `class_exists()` alone isn't enough; the filter is the
* real availability switch and the same one the SEO package gates its own surface behind);
* - the site is self-hosted WordPress.com (Simple + Atomic) decides its own SEO surface, so
* the opt-in card is for self-hosted installs only;
* - the install hasn't opted in yet `jetpack_seo_surface_visible` is still false. On wpcom
* the SEO package's `is_seo_surface_visible()` short-circuits to `true`, so the
* "not visible yet" check also doubles as the self-hosted guard, but we check the platform
* explicitly for clarity.
*
* Referenced through `class_exists()` rather than a hard composer dependency: both packages
* ship inside the Jetpack plugin and the SEO surface is feature-flagged, so a guarded read of
* its public API keeps this from adding plumbing to consumers that don't load SEO.
*
* The on-success destination is computed by the opt-in endpoint itself; we only seed the card
* with the same admin URL so the button has a sensible fallback before the request resolves.
*
* @return array{showCard: bool, redirect: string}
*/
public static function get_seo_opt_in_state() {
// Guard with method_exists rather than class_exists: an older bundled SEO package can ship
// the Initializer class without this method, and class_exists alone would still fatal.
$seo_initializer = 'Automattic\Jetpack\SEO\Initializer';
// @phan-suppress-next-line PhanUndeclaredClassReference -- optional SEO package, guarded by method_exists.
$show_card = method_exists( $seo_initializer, 'is_optin_available' ) && $seo_initializer::is_optin_available();
return array(
'showCard' => $show_card,
'redirect' => admin_url( 'admin.php?page=jetpack-seo' ),
);
}
/**
* Echoes the admin page content.
*
* @return void
*/
public static function admin_page() {
$step = isset( $_GET['step'] ) ? sanitize_text_field( wp_unslash( $_GET['step'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$is_onboarding = $step === 'onboarding';
// Add data attribute for onboarding, otherwise render normal container
echo '<div id="my-jetpack-container" ' . ( $is_onboarding ? 'data-route="onboarding"' : '' ) . '></div>';
}
/**
* Register the REST API routes.
*
* @return void
*/
public static function register_rest_endpoints() {
new REST_Products();
new REST_Purchases();
new REST_Zendesk_Chat();
( new WP_REST_Jetpack_AI_JWT() )->register_rest_route();
new REST_Recommendations_Evaluation();
Products::register_product_endpoints();
Historically_Active_Modules::register_rest_endpoints();
Jetpack_Manage::register_rest_endpoints();
Red_Bubble_Notifications::register_rest_endpoints();
register_rest_route(
'my-jetpack/v1',
'site',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
register_rest_route(
'my-jetpack/v1',
'site/dismiss-welcome-banner',
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::dismiss_welcome_banner',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
return current_user_can( 'manage_options' );
}
/**
* Return true if we should initialize the My Jetpack admin page.
*/
public static function should_initialize() {
$should = true;
// All options presented in My Jetpack require a connection to WordPress.com.
if ( ( new Status() )->is_offline_mode() ) {
$should = false;
}
/**
* Allows filtering whether My Jetpack should be initialized.
*
* @since 0.5.0-alpha
*
* @param bool $shoud_initialize Should we initialize My Jetpack?
*/
return apply_filters( 'jetpack_my_jetpack_should_initialize', $should );
}
/**
* Hook into several connection-based actions to update the historically active Jetpack modules
* If the transient that indicates the list needs to be synced, update it and delete the transient
*
* @return void
*/
public static function setup_historically_active_jetpack_modules_sync() {
// yummmm. ham.
$ham = new Historically_Active_Modules();
if ( get_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY ) && ! wp_doing_ajax() ) {
$ham::update_historically_active_jetpack_modules();
delete_transient( $ham::UPDATE_HISTORICALLY_ACTIVE_JETPACK_MODULES_KEY );
}
$actions = array(
'jetpack_site_registered',
'jetpack_user_authorized',
'activated_plugin',
);
foreach ( $actions as $action ) {
add_action( $action, array( $ham, 'queue_historically_active_jetpack_modules_update' ), 5 );
}
// Modules are often updated async, so we need to update them right away as there will sometimes be no page reload.
add_action( 'jetpack_activate_module', array( $ham, 'update_historically_active_jetpack_modules' ), 5 );
}
/**
* Site full-data endpoint.
*
* @return object Site data.
*/
public static function get_site() {
$site_id = \Jetpack_Options::get_option( 'id' );
$wpcom_endpoint = sprintf( '/sites/%d?force=wpcom', $site_id );
$wpcom_api_version = '1.1';
$response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, $wpcom_api_version );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ) );
}
return rest_ensure_response( $body );
}
/**
* Populates the self::$site_info var with site data from the /sites/%d endpoint
*
* @return object|WP_Error
*/
public static function get_site_info() {
static $site_info = null;
if ( $site_info !== null ) {
return $site_info;
}
// Check for a cached value before doing lookup
$stored_site_info = get_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY );
if ( $stored_site_info !== false ) {
return $stored_site_info;
}
$response = self::get_site();
if ( is_wp_error( $response ) ) {
return $response;
}
$site_info = $response->data;
set_transient( self::MY_JETPACK_SITE_INFO_TRANSIENT_KEY, $site_info, DAY_IN_SECONDS );
return $site_info;
}
/**
* Returns whether a site has been determined "commercial" or not.
*
* @return bool|null
*/
public static function is_commercial_site() {
if ( is_wp_error( self::$site_info ) ) {
return null;
}
return empty( self::$site_info->options->is_commercial ) ? false : self::$site_info->options->is_commercial;
}
/**
* Check if site is registered (has been connected before).
*
* @return bool
*/
public static function is_registered() {
return (bool) \Jetpack_Options::get_option( 'id' );
}
/**
* Dismiss the welcome banner.
*
* @return \WP_REST_Response
*/
public static function dismiss_welcome_banner() {
\Jetpack_Options::update_option( 'dismissed_welcome_banner', true );
return rest_ensure_response( array( 'success' => true ) );
}
/**
* Returns "yes" if the site has file write access to the plugins folder, "no" otherwise.
*
* @return string
**/
public static function has_file_system_write_access() {
$cache = get_transient( 'my_jetpack_write_access' );
if ( false !== $cache ) {
return $cache;
}
if ( ! function_exists( 'get_filesystem_method' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
require_once ABSPATH . 'wp-admin/includes/template.php';
$write_access = 'no';
$filesystem_method = get_filesystem_method( array(), WP_PLUGIN_DIR );
if ( 'direct' === $filesystem_method ) {
$write_access = 'yes';
}
if ( 'no' === $write_access ) {
ob_start();
$filesystem_credentials_are_stored = request_filesystem_credentials( self_admin_url() );
ob_end_clean();
if ( $filesystem_credentials_are_stored ) {
$write_access = 'yes';
}
}
set_transient( 'my_jetpack_write_access', $write_access, 30 * MINUTE_IN_SECONDS );
return $write_access;
}
/**
* Get container IDC for the IDC screen.
*
* @return string
*/
public static function get_idc_container_id() {
return static::IDC_CONTAINER_ID;
}
/**
* Conditionally append the red bubble notification to the "Jetpack" menu item if there are alerts to show.
*
* On My Jetpack page: Uses blocking behavior to fetch fresh data.
* On other admin pages: Uses cached data only to avoid blocking, with async JS fetch if cache is empty.
*
* @return void
*/
public static function maybe_show_red_bubble() {
global $menu, $pagenow;
// Don't show red bubble alerts for non-admin users
// These alerts are generally only actionable for admins
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Don't show any red bubbles when Jetpack is disconnected
// Users can't act on most alerts without a connection
$connection = new Connection_Manager();
if ( ! $connection->is_connected() ) {
return;
}
// Check if we're on the My Jetpack page.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
$is_my_jetpack_page = $pagenow === 'admin.php' && $page === 'my-jetpack';
if ( $is_my_jetpack_page ) {
// On My Jetpack page: use blocking behavior for fresh data.
add_filter( 'my_jetpack_red_bubble_notification_slugs', array( Red_Bubble_Notifications::class, 'add_red_bubble_alerts' ) );
$red_bubble_alerts = Red_Bubble_Notifications::get_red_bubble_alerts();
} else {
// On other pages: use cached data only to avoid blocking.
$cached_alerts = Red_Bubble_Notifications::get_cached_alerts();
if ( false === $cached_alerts ) {
// No cache - fetch asynchronously via JS.
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_red_bubble_script' ) );
return;
}
$red_bubble_alerts = $cached_alerts;
}
// Filter out silent alerts.
$red_bubble_alerts = array_filter(
$red_bubble_alerts,
function ( $alert ) {
return empty( $alert['is_silent'] );
}
);
// The Jetpack menu item should be on index 3
if (
! empty( $red_bubble_alerts ) &&
isset( $menu[3] ) &&
$menu[3][0] === 'Jetpack'
) {
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$menu[3][0] .= sprintf( ' <span class="awaiting-mod">%d</span>', count( $red_bubble_alerts ) );
}
}
/**
* Enqueue the notification bubble script.
* Fetches fresh alert data via REST API without blocking page load.
*
* @return void
*/
public static function enqueue_red_bubble_script() {
Assets::register_script(
'my-jetpack-notification-bubble',
'../build/async-notification-bubble.js',
__FILE__,
array(
'enqueue' => true,
'in_footer' => true,
)
);
}
/**
* Get list of module names sorted by their recommendation score
*
* @return array|null
*/
public static function get_recommended_modules() {
$recommendations_evaluation = \Jetpack_Options::get_option( 'recommendations_evaluation', null );
if ( ! $recommendations_evaluation ) {
return null;
}
arsort( $recommendations_evaluation ); // Sort by scores in descending order
return array_keys( $recommendations_evaluation ); // Get only module names
}
}
@@ -0,0 +1,172 @@
<?php
/**
* Tools to manage things related to "Jetpack Manage"
* - Add Jetpack Manage menu item.
* - Check if user is an agency (used by the Jetpack Manage banner)
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Admin_UI\Admin_Menu;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Redirect;
use WP_Error;
use WP_Rest_Response;
/**
* Jetpack Manage features in My Jetpack.
*/
class Jetpack_Manage {
/**
* Initialize the class and hooks needed.
*/
public static function init() {
add_action( 'admin_menu', array( self::class, 'add_submenu_jetpack' ) );
}
/**
* Register the REST API routes.
*
* @return void
*/
public static function register_rest_endpoints() {
register_rest_route(
'my-jetpack/v1',
'jetpack-manage/data',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_jetpack_manage_data',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Check user capabilities to access historically active modules.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
return current_user_can( 'manage_options' );
}
/**
* The page to be added to submenu
*
* @return void|null|string The resulting page's hook_suffix
*/
public static function add_submenu_jetpack() {
// Do not display the menu if the user has < 2 sites.
if ( ! self::could_use_jp_manage( 2 ) ) {
return;
}
$args = array();
$blog_id = Connection_Manager::get_site_id( true );
if ( $blog_id ) {
$args = array( 'site' => $blog_id );
}
return Admin_Menu::add_menu(
__( 'Jetpack Manage', 'jetpack-my-jetpack' ),
_x( 'Jetpack Manage', 'product name shown in menu', 'jetpack-my-jetpack' ) . ' <span aria-hidden="true">↗</span>',
'manage_options',
esc_url( Redirect::get_url( 'cloud-manage-dashboard-wp-menu', $args ) ),
null,
16
);
}
/**
* Check if the user has enough sites to be able to use Jetpack Manage.
*
* @param int $min_sites Minimum number of sites to be able to use Jetpack Manage.
*
* @return bool Return true if the user has enough sites to be able to use Jetpack Manage.
*/
public static function could_use_jp_manage( $min_sites = 2 ) {
// Only proceed if the user is connected to WordPress.com.
if ( ! ( new Connection_Manager() )->is_user_connected() ) {
return false;
}
// Do not display the menu if Jetpack plugin is not installed.
if ( ! class_exists( 'Jetpack' ) ) {
return false;
}
// Do not display the menu on Multisite.
if ( is_multisite() ) {
return false;
}
// Check if the user has the minimum number of sites.
$user_data = ( new Connection_Manager() )->get_connected_user_data( get_current_user_id() );
if ( ! isset( $user_data['site_count'] ) || $user_data['site_count'] < $min_sites ) {
return false;
}
return true;
}
/**
* Check if the user is a partner/agency.
*
* @return bool Return true if the user is a partner/agency, otherwise false.
*/
public static function is_agency_account() {
// Only proceed if the user is connected to WordPress.com.
if ( ! ( new Connection_Manager() )->is_user_connected() ) {
return false;
}
// Get the cached partner data.
$partner = get_transient( 'jetpack_partner_data' );
if ( $partner === false ) {
$wpcom_response = Client::wpcom_json_api_request_as_user( '/jetpack-partners' );
if ( 200 !== wp_remote_retrieve_response_code( $wpcom_response ) || is_wp_error( $wpcom_response ) ) {
return false;
}
$partner_data = json_decode( wp_remote_retrieve_body( $wpcom_response ) );
// The jetpack-partners endpoint will return only one partner data into an array, it uses Jetpack_Partner::find_by_owner.
if ( ! is_array( $partner_data ) || count( $partner_data ) !== 1 || ! is_object( $partner_data[0] ) ) {
return false;
}
$partner = $partner_data[0];
// Cache the partner data for 1 hour.
set_transient( 'jetpack_partner_data', $partner, HOUR_IN_SECONDS );
}
return $partner->partner_type === 'agency';
}
/**
* Get Jetpack Manage data for REST API.
*
* @return WP_Error|WP_REST_Response
*/
public static function get_jetpack_manage_data() {
$is_enabled = self::could_use_jp_manage();
$is_agency_account = self::is_agency_account();
return rest_ensure_response(
array(
'isEnabled' => $is_enabled,
'isAgencyAccount' => $is_agency_account,
)
);
}
}
@@ -0,0 +1,492 @@
<?php
/**
* Class for manipulating products
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
/**
* A class for everything related to product handling in My Jetpack
*/
class Products {
/**
* Constants for the status of a product on a site
*
* @var string
*/
public const STATUS_SITE_CONNECTION_ERROR = 'site_connection_error';
public const STATUS_USER_CONNECTION_ERROR = 'user_connection_error';
public const STATUS_ACTIVE = 'active';
public const STATUS_CAN_UPGRADE = 'can_upgrade';
public const STATUS_EXPIRING_SOON = 'expiring';
public const STATUS_EXPIRED = 'expired';
public const STATUS_INACTIVE = 'inactive';
public const STATUS_MODULE_DISABLED = 'module_disabled';
public const STATUS_PLUGIN_ABSENT = 'plugin_absent';
public const STATUS_PLUGIN_ABSENT_WITH_PLAN = 'plugin_absent_with_plan';
public const STATUS_NEEDS_PLAN = 'needs_plan';
public const STATUS_NEEDS_ACTIVATION = 'needs_activation';
public const STATUS_NEEDS_FIRST_SITE_CONNECTION = 'needs_first_site_connection';
public const STATUS_NEEDS_ATTENTION__WARNING = 'needs_attention_warning';
public const STATUS_NEEDS_ATTENTION__ERROR = 'needs_attention_error';
public const INTERSTITIALS_OPTION_NAME = 'my_jetpack_products_interstitials_state';
/**
* List of statuses that display the module as disabled
* This is defined as the statuses in which the user willingly has the module disabled whether it be by
* default, uninstalling the plugin, disabling the module, or not renewing their plan.
*
* @var array
*/
public static $disabled_module_statuses = array(
self::STATUS_INACTIVE,
self::STATUS_MODULE_DISABLED,
self::STATUS_PLUGIN_ABSENT,
self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
self::STATUS_NEEDS_ACTIVATION,
self::STATUS_NEEDS_FIRST_SITE_CONNECTION,
);
/**
* List of statuses that display the module as broken
*
* @var array
*/
public static $broken_module_statuses = array(
self::STATUS_SITE_CONNECTION_ERROR,
self::STATUS_USER_CONNECTION_ERROR,
);
/**
* List of statuses that display the module as needing attention with a warning
*
* @var array
*/
public static $warning_module_statuses = array(
self::STATUS_SITE_CONNECTION_ERROR,
self::STATUS_USER_CONNECTION_ERROR,
self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
self::STATUS_NEEDS_PLAN,
self::STATUS_NEEDS_ATTENTION__ERROR,
self::STATUS_NEEDS_ATTENTION__WARNING,
);
/**
* List of statuses that display the module as active
*
* @var array
*/
public static $active_module_statuses = array(
self::STATUS_ACTIVE,
self::STATUS_CAN_UPGRADE,
);
/**
* List of statuses that display the module as active
*
* @var array
*/
public static $expiring_or_expired_module_statuses = array(
self::STATUS_EXPIRING_SOON,
self::STATUS_EXPIRED,
);
/**
* List of all statuses that a product can have
*
* @var array
*/
public static $all_statuses = array(
self::STATUS_SITE_CONNECTION_ERROR,
self::STATUS_USER_CONNECTION_ERROR,
self::STATUS_ACTIVE,
self::STATUS_CAN_UPGRADE,
self::STATUS_EXPIRING_SOON,
self::STATUS_EXPIRED,
self::STATUS_INACTIVE,
self::STATUS_MODULE_DISABLED,
self::STATUS_PLUGIN_ABSENT,
self::STATUS_PLUGIN_ABSENT_WITH_PLAN,
self::STATUS_NEEDS_PLAN,
self::STATUS_NEEDS_ACTIVATION,
self::STATUS_NEEDS_FIRST_SITE_CONNECTION,
self::STATUS_NEEDS_ATTENTION__WARNING,
self::STATUS_NEEDS_ATTENTION__ERROR,
);
/**
* Get the list of Products classes
*
* Here's where all the existing Products are registered
*
* @throws \Exception If the result of a filter has invalid classes.
* @return array List of class names
*/
public static function get_products_classes() {
$classes = array(
'anti-spam' => Products\Anti_Spam::class,
'backup' => Products\Backup::class,
'boost' => Products\Boost::class,
'crm' => Products\Crm::class,
'creator' => Products\Creator::class,
'extras' => Products\Extras::class,
'jetpack-ai' => Products\Jetpack_Ai::class,
// TODO: Remove this duplicate class ('ai')? See: https://github.com/Automattic/jetpack/pull/35910#pullrequestreview-2456462227.
'ai' => Products\Jetpack_Ai::class,
'scan' => Products\Scan::class,
'search' => Products\Search::class,
'social' => Products\Social::class,
'security' => Products\Security::class,
'protect' => Products\Protect::class,
'videopress' => Products\Videopress::class,
'stats' => Products\Stats::class,
'growth' => Products\Growth::class,
'complete' => Products\Complete::class,
// Features.
'newsletter' => Products\Newsletter::class,
'site-accelerator' => Products\Site_Accelerator::class,
'related-posts' => Products\Related_Posts::class,
'jetpack-forms' => Products\Jetpack_Forms::class,
);
/**
* This filter allows plugin to override the Product class of a given product. The new class must be a child class of the default one declared in My Jetpack
*
* For example, a stand-alone plugin could overwrite its product class to control specific behavior of the product in the My Jetpack page after it is active without having to commit changes to the My Jetpack package:
*
* add_filter( 'my_jetpack_products_classes', function( $classes ) {
* $classes['my_plugin'] = 'My_Plugin'; // a class that extends the original one declared in the My Jetpack package.
* return $classes
* } );
*
* @param array $classes An array where the keys are the product slugs and the values are the class names.
*/
$final_classes = apply_filters( 'my_jetpack_products_classes', $classes );
// Check that the classes are still child of the same original classes.
foreach ( (array) $final_classes as $slug => $final_class ) {
if ( $final_class === $classes[ $slug ] ) {
continue;
}
if ( ! class_exists( $final_class ) || ! is_subclass_of( $final_class, $classes[ $slug ] ) ) {
throw new \Exception( 'You can only overwrite a Product class with a child of the original class.' );
}
}
return $final_classes;
}
/**
* Register endpoints related to product classes
*
* @return void
*/
public static function register_product_endpoints() {
$classes = self::get_products_classes();
foreach ( $classes as $class ) {
$class::register_endpoints();
}
}
/**
* List of product slugs that are displayed on the main My Jetpack page
*
* @var array
*/
public static $shown_products = array(
'anti-spam',
'backup',
'boost',
'crm',
'jetpack-ai',
'search',
'social',
'protect',
'videopress',
'stats',
);
/**
* Gets the list of product slugs that are Not displayed on the main My Jetpack page
*
* @return array
*/
public static function get_not_shown_products() {
return array_diff( array_keys( static::get_products_classes() ), self::$shown_products );
}
/**
* Product data
*
* @param array $product_slugs (optional) An array of specified product slugs.
* @return array Jetpack products on the site and their availability.
*/
public static function get_products( $product_slugs = array() ) {
$all_classes = self::get_products_classes();
$products = array();
// If an array of $product_slugs are passed, return only the products specified in $product_slugs array.
if ( $product_slugs ) {
foreach ( $product_slugs as $product_slug ) {
if ( isset( $all_classes[ $product_slug ] ) ) {
$class = $all_classes[ $product_slug ];
$products[ $product_slug ] = $class::get_info();
}
}
return $products;
}
// Otherwise return All products.
foreach ( $all_classes as $slug => $class ) {
$products[ $slug ] = $class::get_info();
}
return $products;
}
/**
* Get products data related to the wpcom api
*
* @param array $product_slugs - (optional) An array of specified product slugs.
* @return array
*/
public static function get_products_api_data( $product_slugs = array() ) {
$all_classes = self::get_products_classes();
$products = array();
// If an array of $product_slugs are passed, return only the products specified in $product_slugs array.
if ( $product_slugs ) {
foreach ( $product_slugs as $product_slug ) {
if ( isset( $all_classes[ $product_slug ] ) ) {
$class = $all_classes[ $product_slug ];
$products[ $product_slug ] = $class::get_wpcom_info();
}
}
return $products;
}
// Otherwise return All products.
foreach ( $all_classes as $slug => $class ) {
$products[ $slug ] = $class::get_wpcom_info();
}
return $products;
}
/**
* Get a list of products sorted by whether or not the user owns them
* An owned product is defined as a product that is any of the following
* - Active
* - Has historically been active
* - The user has a plan that includes the product
* - The user has the standalone plugin for the product installed
*
* @param string $type The type of ownership to return ('owned' or 'unowned').
*
* @return array
*/
public static function get_products_by_ownership( $type ) {
$owned_active_products = array();
$owned_warning_products = array();
$owned_inactive_products = array();
$unowned_products = array();
foreach ( self::get_products_classes() as $class ) {
$product_slug = $class::$slug;
$status = $class::get_status();
if ( $class::is_owned() ) {
// This sorts the the products in the order of active -> warning -> inactive.
// This enables the frontend to display them in that order.
// This is not needed for unowned products as those will always have a status of 'inactive'.
if ( in_array( $status, self::$active_module_statuses, true ) ) {
array_push( $owned_active_products, $product_slug );
} elseif ( in_array( $status, self::$warning_module_statuses, true ) ) {
array_push( $owned_warning_products, $product_slug );
} else {
array_push( $owned_inactive_products, $product_slug );
}
continue;
}
array_push( $unowned_products, $product_slug );
}
$data = array(
'owned' => array_values(
array_unique(
array_merge(
$owned_active_products,
$owned_warning_products,
$owned_inactive_products
)
)
),
'unowned' => array_values(
array_unique( $unowned_products )
),
);
return $data[ $type ];
}
/**
* Get all plugin filenames associated with the products.
*
* @return array
*/
public static function get_all_plugin_filenames() {
$filenames = array();
foreach ( self::get_products_classes() as $class ) {
if ( ! isset( $class::$plugin_filename ) ) {
continue;
}
if ( is_array( $class::$plugin_filename ) ) {
$filenames = array_merge( $filenames, $class::$plugin_filename );
} else {
$filenames[] = $class::$plugin_filename;
}
}
return $filenames;
}
/**
* Get one product data by its slug
*
* @param string $product_slug The product slug.
*
* @return ?array
*/
public static function get_product( $product_slug ) {
$classes = self::get_products_classes();
if ( isset( $classes[ $product_slug ] ) ) {
return $classes[ $product_slug ]::get_info();
}
}
/**
* Get one product Class name
*
* @param string $product_slug The product slug.
*
* @return ?string
*/
public static function get_product_class( $product_slug ) {
$classes = self::get_products_classes();
if ( isset( $classes[ $product_slug ] ) ) {
return $classes[ $product_slug ];
}
}
/**
* Return product slugs list.
*
* @return array Product slugs array.
*/
public static function get_products_slugs() {
return array_keys( self::get_products_classes() );
}
/**
* Gets the json schema for the product data
*
* @return array
*/
public static function get_product_data_schema() {
return array(
'title' => 'The requested product data',
'type' => 'object',
'properties' => array(
'product' => array(
'description' => __( 'Product slug', 'jetpack-my-jetpack' ),
'type' => 'string',
'enum' => __CLASS__ . '::get_product_slugs',
'required' => false,
'validate_callback' => __CLASS__ . '::check_product_argument',
),
'action' => array(
'description' => __( 'Production action to execute', 'jetpack-my-jetpack' ),
'type' => 'string',
'enum' => array( 'activate', 'deactivate' ),
'required' => false,
'validate_callback' => __CLASS__ . '::check_product_argument',
),
'slug' => array(
'title' => 'The product slug',
'type' => 'string',
),
'name' => array(
'title' => 'The product name',
'type' => 'string',
),
'description' => array(
'title' => 'The product description',
'type' => 'string',
),
'status' => array(
'title' => 'The product status',
'type' => 'string',
'enum' => self::$all_statuses,
),
'class' => array(
'title' => 'The product class handler',
'type' => 'string',
),
),
);
}
/**
* Extend actions links for plugins
* tied to the Products.
*/
public static function extend_plugins_action_links() {
$products = array(
'backup',
'boost',
'crm',
'videopress',
'social',
'protect',
'crm',
'search',
'jetpack-ai',
);
// Add plugin action links for the core Jetpack plugin.
Product::extend_core_plugin_action_links();
// Add plugin action links to standalone products.
foreach ( $products as $product ) {
$class_name = self::get_product_class( $product );
$class_name::extend_plugin_action_links();
}
}
/**
* Get interstitials state for the products
*
* @return array A key-value array of product slugs and their interstitial states. True means the interstitial was seen by the user for that product.
*/
public static function get_interstitials_state() {
return get_option( self::INTERSTITIALS_OPTION_NAME, array() );
}
/**
* Update interstitials state for the products
*
* @param array $new_state A key-value array of product slugs and their interstitial states.
*
* @return bool True if the option was updated successfully, false otherwise.
*/
public static function update_interstitials_state( $new_state ) {
// Merge the existing interstitials state with the new state.
$interstitials_state = array_merge( self::get_interstitials_state(), $new_state );
return update_option( self::INTERSTITIALS_OPTION_NAME, $interstitials_state );
}
}
@@ -0,0 +1,443 @@
<?php
/**
* Sets up the Red Bubble Notifications rest api endpoint and helper functions
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Jetpack_Options;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
/**
* Registers REST route for getting red bubble notification data
* and includes all helper functions related to red bubble notifications
*/
class Red_Bubble_Notifications {
private const MISSING_CONNECTION_NOTIFICATION_KEY = 'missing-connection';
private const MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY = 'my-jetpack-red-bubble-transient';
/**
* Summary of register_rest_routes
*
* @return void
*/
public static function register_rest_endpoints() {
register_rest_route(
'my-jetpack/v1',
'red-bubble-notifications',
array(
'methods' => \WP_REST_Server::CREATABLE,
'callback' => __CLASS__ . '::rest_api_get_red_bubble_alerts',
'permission_callback' => __CLASS__ . '::permissions_callback',
'args' => array(
'dismissal_cookies' => array(
'type' => 'array',
'description' => 'Array of dismissal cookies to set for the red bubble notifications.',
'required' => false,
'items' => array(
'type' => 'string',
),
'sanitize_callback' => function ( $param ) {
if ( ! is_array( $param ) ) {
return array();
}
return array_map( 'sanitize_text_field', $param );
},
),
),
)
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
return current_user_can( 'edit_posts' );
}
/**
* Gets the plugins that need installed or activated for each paid plan.
*
* @return array
*/
public static function get_paid_plans_plugins_requirements() {
$plugin_requirements = array();
foreach ( Products::get_products_classes() as $slug => $product_class ) {
// Skip these- we don't show them in My Jetpack.
if ( in_array( $slug, Products::get_not_shown_products(), true ) ) {
continue;
}
// Skip CRM from installation requirements - e.g. don't enforce installation for Complete plan users
if ( $slug === 'crm' ) {
continue;
}
if ( ! $product_class::has_paid_plan_for_product() ) {
continue;
}
$purchase = $product_class::get_paid_plan_purchase_for_product();
if ( ! $purchase ) {
continue;
}
// Check if required plugin needs installed or activated.
if ( ! $product_class::is_plugin_installed() ) {
// Plugin needs installed (and activated)
$plugin_requirements[ $purchase->product_slug ]['needs_installed'][] = $product_class::$slug;
} elseif ( ! $product_class::is_plugin_active() ) {
// Plugin is installed, but not activated.
$plugin_requirements[ $purchase->product_slug ]['needs_activated_only'][] = $product_class::$slug;
}
}
return $plugin_requirements;
}
/**
* Check for features broken by a disconnected user or site
*
* @return array
*/
public static function check_for_broken_modules() {
$connection = new Connection_Manager();
$is_user_connected = $connection->is_user_connected() || $connection->has_connected_owner();
$is_site_connected = $connection->is_connected();
$broken_modules = array(
'needs_site_connection' => array(),
'needs_user_connection' => array(),
);
if ( $is_user_connected && $is_site_connected ) {
return $broken_modules;
}
$products = Products::get_products_classes();
$historically_active_modules = Jetpack_Options::get_option( 'historically_active_modules', array() );
foreach ( $products as $product ) {
if ( ! in_array( $product::$slug, $historically_active_modules, true ) ) {
continue;
}
if ( $product::$requires_user_connection && ! $is_user_connected ) {
if ( ! in_array( $product::$slug, $broken_modules['needs_user_connection'], true ) ) {
$broken_modules['needs_user_connection'][] = $product::$slug;
}
} elseif ( ! $is_site_connected ) {
if ( ! in_array( $product::$slug, $broken_modules['needs_site_connection'], true ) ) {
$broken_modules['needs_site_connection'][] = $product::$slug;
}
}
}
return $broken_modules;
}
/**
* Add an alert slug if the site is missing a site connection
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_missing_connection( array $red_bubble_slugs ) {
$broken_modules = self::check_for_broken_modules();
$connection = new Connection_Manager();
// Checking for site connection issues first.
if ( ! empty( $broken_modules['needs_site_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => true,
);
return $red_bubble_slugs;
}
if ( ! empty( $broken_modules['needs_user_connection'] ) ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'user',
'is_error' => true,
);
return $red_bubble_slugs;
}
if ( ! $connection->is_connected() ) {
$red_bubble_slugs[ self::MISSING_CONNECTION_NOTIFICATION_KEY ] = array(
'type' => 'site',
'is_error' => false,
);
return $red_bubble_slugs;
}
return $red_bubble_slugs;
}
/**
* Add an alert slug if Backups are failing or having an issue.
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_last_backup_failed( array $red_bubble_slugs ) {
// Backup is not supported on multisite installations.
if ( is_multisite() ) {
return $red_bubble_slugs;
}
// Make sure the Notice wasn't previously dismissed.
if ( ! empty( $_COOKIE['backup_failure_dismissed'] ) ) {
return $red_bubble_slugs;
}
// Make sure there's a Backup paid plan
if ( ! Products\Backup::is_plugin_active() || ! Products\Backup::has_paid_plan_for_product() ) {
return $red_bubble_slugs;
}
// Make sure the plan isn't just recently purchased in last 30min.
// Give some time to queue & run the first backup.
$purchase = Products\Backup::get_paid_plan_purchase_for_product();
if ( $purchase ) {
$thirty_minutes_after_plan_purchase = strtotime( $purchase->subscribed_date . ' +30 minutes' );
if ( strtotime( 'now' ) < $thirty_minutes_after_plan_purchase ) {
return $red_bubble_slugs;
}
}
$backup_failed_status = Products\Backup::does_module_need_attention();
if ( $backup_failed_status ) {
$red_bubble_slugs['backup_failure'] = $backup_failed_status;
}
return $red_bubble_slugs;
}
/**
* Add an alert slug if Protect has scan threats/vulnerabilities.
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_protect_has_threats( array $red_bubble_slugs ) {
// Scan is not supported on multisite installations.
if ( is_multisite() ) {
return $red_bubble_slugs;
}
// Make sure the Notice hasn't been dismissed.
if ( ! empty( $_COOKIE['protect_threats_detected_dismissed'] ) ) {
return $red_bubble_slugs;
}
// Make sure we're dealing with the Protect product only
if ( ! Products\Protect::has_paid_plan_for_product() ) {
return $red_bubble_slugs;
}
$protect_threats_status = Products\Protect::does_module_need_attention();
if ( $protect_threats_status ) {
$red_bubble_slugs['protect_has_threats'] = $protect_threats_status;
}
return $red_bubble_slugs;
}
/**
* Add an alert slug if any paid plan/products are expiring or expired.
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_paid_plan_expiring( array $red_bubble_slugs ) {
$connection = new Connection_Manager();
if ( ! $connection->is_connected() ) {
return $red_bubble_slugs;
}
$product_classes = Products::get_products_classes();
$products_included_in_expiring_plan = array();
foreach ( $product_classes as $key => $product ) {
// Skip these- we don't show them in My Jetpack.
if ( in_array( $key, Products::get_not_shown_products(), true ) ) {
continue;
}
if ( $product::has_paid_plan_for_product() ) {
$purchase = $product::get_paid_plan_purchase_for_product();
if ( $purchase ) {
// Check if this product is covered by an active bundle plan
$is_covered_by_active_bundle = false;
if ( ! $product::is_bundle_product() ) {
foreach ( $product_classes as $bundle_product ) {
if ( $bundle_product::is_bundle_product() &&
$bundle_product::has_paid_plan_for_product() &&
! $bundle_product::is_paid_plan_expired() &&
! $bundle_product::is_paid_plan_expiring() &&
method_exists( $bundle_product, 'get_supported_products' ) &&
in_array( $key, $bundle_product::get_supported_products(), true ) ) {
$is_covered_by_active_bundle = true;
break;
}
}
}
// Only show expiration alerts if not covered by an active bundle
if ( ! $is_covered_by_active_bundle ) {
$redbubble_notice_data = array(
'product_slug' => $purchase->product_slug,
'product_name' => $purchase->product_name,
'expiry_date' => $purchase->expiry_date,
'expiry_message' => $purchase->expiry_message,
'manage_url' => $product::get_manage_paid_plan_purchase_url(),
);
if ( $product::is_paid_plan_expired() && empty( $_COOKIE[ "$purchase->product_slug--plan_expired_dismissed" ] ) ) {
$red_bubble_slugs[ "$purchase->product_slug--plan_expired" ] = $redbubble_notice_data;
if ( ! $product::is_bundle_product() ) {
$products_included_in_expiring_plan[ "$purchase->product_slug--plan_expired" ][] = $product::get_name();
}
}
if ( $product::is_paid_plan_expiring() && empty( $_COOKIE[ "$purchase->product_slug--plan_expiring_soon_dismissed" ] ) ) {
$red_bubble_slugs[ "$purchase->product_slug--plan_expiring_soon" ] = $redbubble_notice_data;
$red_bubble_slugs[ "$purchase->product_slug--plan_expiring_soon" ]['manage_url'] = $product::get_renew_paid_plan_purchase_url();
if ( ! $product::is_bundle_product() ) {
$products_included_in_expiring_plan[ "$purchase->product_slug--plan_expiring_soon" ][] = $product::get_name();
}
}
}
}
}
}
foreach ( $products_included_in_expiring_plan as $expiring_plan => $products ) {
$red_bubble_slugs[ $expiring_plan ]['products_effected'] = $products;
}
return $red_bubble_slugs;
}
/**
* Add an alert slug if a site's paid plan requires a plugin install and/or activation.
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function alert_if_paid_plan_requires_plugin_install_or_activation( array $red_bubble_slugs ) {
$connection = new Connection_Manager();
// Don't trigger red bubble (and show notice) when the site is not connected or if the
// user doesn't have plugin installation/activation permissions.
if ( ! $connection->is_connected() || ! current_user_can( 'activate_plugins' ) ) {
return $red_bubble_slugs;
}
$plugins_needing_installed_activated = self::get_paid_plans_plugins_requirements();
if ( empty( $plugins_needing_installed_activated ) ) {
return $red_bubble_slugs;
}
foreach ( $plugins_needing_installed_activated as $plan_slug => $plugins_requirements ) {
if ( empty( $_COOKIE[ "$plan_slug--plugins_needing_installed_dismissed" ] ) ) {
$red_bubble_slugs[ "$plan_slug--plugins_needing_installed_activated" ] = $plugins_requirements;
}
}
return $red_bubble_slugs;
}
/**
* Add relevant red bubble notifications
*
* @param array $red_bubble_slugs - slugs that describe the reasons the red bubble is showing.
* @return array
*/
public static function add_red_bubble_alerts( array $red_bubble_slugs ) {
if ( wp_doing_ajax() ) {
return array();
}
$connection = new Connection_Manager();
$welcome_banner_dismissed = Jetpack_Options::get_option( 'dismissed_welcome_banner', false );
if ( Initializer::is_jetpack_user_new() && ! $welcome_banner_dismissed ) {
$red_bubble_slugs['welcome-banner-active'] = array(
'is_silent' => $connection->is_connected(), // we don't display the red bubble if the user is connected
);
return $red_bubble_slugs;
} else {
return array_merge(
self::alert_if_missing_connection( $red_bubble_slugs ),
self::alert_if_last_backup_failed( $red_bubble_slugs ),
self::alert_if_paid_plan_expiring( $red_bubble_slugs ),
self::alert_if_protect_has_threats( $red_bubble_slugs ),
self::alert_if_paid_plan_requires_plugin_install_or_activation( $red_bubble_slugs )
);
}
}
/**
* Get cached red bubble alerts without triggering expensive computation.
* Returns the cached transient value or false if not cached.
*
* @return array|false Cached alerts or false if cache is empty.
*/
public static function get_cached_alerts() {
return get_transient( self::MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY );
}
/**
* Collect all possible alerts that we might use a red bubble notification for
*
* @param bool $bypass_cache - whether to bypass the red bubble cache.
* @return array
*/
public static function get_red_bubble_alerts( bool $bypass_cache = false ) {
static $red_bubble_alerts = array();
// check for stored alerts
$stored_alerts = get_transient( self::MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY );
// Cache bypass for red bubbles should only happen on the My Jetpack page
if ( $stored_alerts !== false && ! ( $bypass_cache ) ) {
return $stored_alerts;
}
// go find the alerts
$red_bubble_alerts = apply_filters( 'my_jetpack_red_bubble_notification_slugs', $red_bubble_alerts );
// cache the alerts for one hour
set_transient( self::MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY, $red_bubble_alerts, 3600 );
return $red_bubble_alerts;
}
/**
* Get the red bubble alerts, bypassing cache when called via the REST API
*
* @param WP_REST_Request $request The REST API request object.
*
* @return WP_Error|WP_REST_Response
*/
public static function rest_api_get_red_bubble_alerts( $request ) {
add_filter( 'my_jetpack_red_bubble_notification_slugs', array( __CLASS__, 'add_red_bubble_alerts' ) );
$cookies = $request->get_param( 'dismissal_cookies' );
// Update $_COOKIE superglobal with the provided cookies
if ( ! empty( $cookies ) && is_array( $cookies ) ) {
foreach ( $cookies as $cookie_string ) {
// Parse cookie string in format "name=value"
$parts = explode( '=', $cookie_string, 2 );
if ( count( $parts ) === 2 ) {
$name = trim( $parts[0] );
$value = trim( $parts[1] );
$_COOKIE[ $name ] = $value;
}
}
}
$red_bubble_alerts = self::get_red_bubble_alerts( true );
return rest_ensure_response( $red_bubble_alerts );
}
}
@@ -0,0 +1,465 @@
<?php
/**
* Sets up the Products REST API endpoints.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use WP_REST_Server;
/**
* Registers the REST routes for Products.
*
* @phan-constructor-used-for-side-effects
*/
class REST_Products {
/**
* Constructor.
*/
public function __construct() {
register_rest_route(
'my-jetpack/v1',
'site/products',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_products_api_data',
'permission_callback' => __CLASS__ . '::view_products_permissions_callback',
'args' => array(
'products' => array(
'description' => __( 'Comma-separated list of product slugs that should be retrieved.', 'jetpack-my-jetpack' ),
'type' => 'string',
'required' => false,
'validate_callback' => __CLASS__ . '::check_products_string',
),
),
),
'schema' => array( $this, 'get_products_schema' ),
)
);
$products_arg = array(
'description' => __( 'Array of Product slugs', 'jetpack-my-jetpack' ),
'type' => 'array',
'items' => array(
'enum' => Products::get_products_slugs(),
'type' => 'string',
),
'required' => true,
'validate_callback' => __CLASS__ . '::check_products_argument',
);
register_rest_route(
'my-jetpack/v1',
'site/products/install',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::install_plugins',
'permission_callback' => __CLASS__ . '::edit_permissions_callback',
'args' => array(
'products' => $products_arg,
),
),
)
);
register_rest_route(
'my-jetpack/v1',
'site/products/activate',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::activate_products',
'permission_callback' => __CLASS__ . '::edit_permissions_callback',
'args' => array(
'products' => $products_arg,
),
),
)
);
register_rest_route(
'my-jetpack/v1',
'site/products/interstitials',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'get_interstitials_state' ),
'permission_callback' => array( self::class, 'edit_permissions_callback' ),
),
array(
'methods' => WP_REST_Server::EDITABLE,
'callback' => array( self::class, 'update_interstitials_state' ),
'permission_callback' => array( self::class, 'edit_permissions_callback' ),
'args' => array(
'products' => array(
'description' => __( 'Key-value pairs of product slugs and their interstitial states.', 'jetpack-my-jetpack' ),
'type' => 'object',
'required' => true,
'properties' => array_fill_keys(
Products::get_products_slugs(),
array(
'type' => 'boolean',
)
),
'additionalProperties' => false,
'minProperties' => 1,
),
),
),
'schema' => array( self::class, 'get_interstitials_schema' ),
)
);
register_rest_route(
'my-jetpack/v1',
'site/products/deactivate',
array(
array(
'methods' => \WP_REST_Server::DELETABLE,
'callback' => __CLASS__ . '::deactivate_products',
'permission_callback' => __CLASS__ . '::edit_permissions_callback',
'args' => array(
'products' => $products_arg,
),
),
)
);
register_rest_route(
'my-jetpack/v1',
'site/products-ownership',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_products_by_ownership',
'permission_callback' => __CLASS__ . '::view_products_permissions_callback',
),
)
);
}
/**
* Get the schema for the products endpoint
*
* @return array
*/
public function get_products_schema() {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'products',
'type' => 'object',
'properties' => Products::get_product_data_schema(),
);
}
/**
* Get the schema for the interstitials endpoint
*
* @return array
*/
public static function get_interstitials_schema() {
return array(
'$schema' => 'http://json-schema.org/draft-04/schema#',
'title' => 'Products interstitials',
'type' => 'object',
'properties' => array(
'products' => array(
'type' => 'object',
'description' => __( 'Key-value pairs of product slugs and their interstitial states.', 'jetpack-my-jetpack' ),
'properties' => array_fill_keys(
Products::get_products_slugs(),
array(
'description' => __( 'Interstitial state for the product. True means that the user has seen the interstitial for the product.', 'jetpack-my-jetpack' ),
'type' => 'boolean',
)
),
),
),
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
return current_user_can( 'manage_options' );
}
/**
* Check if the user is permitted to view the product and product info
*
* @return bool
*/
public static function view_products_permissions_callback() {
return current_user_can( 'edit_posts' );
}
/**
* Check Products string (comma-separated string).
*
* @access public
* @static
*
* @param mixed $value - Value of the 'product' argument.
* @return true|WP_Error True if the value is valid, WP_Error otherwise.
*/
public static function check_products_string( $value ) {
if ( ! is_string( $value ) ) {
return new WP_Error(
'rest_invalid_param',
esc_html__( 'The product argument must be a string.', 'jetpack-my-jetpack' ),
array( 'status' => 400 )
);
}
$products_array = explode( ',', $value );
$all_products = Products::get_products_slugs();
foreach ( $products_array as $product_slug ) {
if ( ! in_array( $product_slug, $all_products, true ) ) {
return new WP_Error(
'rest_invalid_param',
esc_html(
sprintf(
/* translators: %s is the product_slug, it should Not be translated. */
__( 'The specified product argument %s is an invalid product.', 'jetpack-my-jetpack' ),
$product_slug
)
),
array( 'status' => 400 )
);
}
}
return true;
}
/**
* Check Products argument.
*
* @access public
* @static
*
* @param mixed $value - Value of the 'product' argument.
* @return true|WP_Error True if the value is valid, WP_Error otherwise.
*/
public static function check_products_argument( $value ) {
if ( ! is_array( $value ) ) {
return new WP_Error(
'rest_invalid_param',
esc_html__( 'The product argument must be an array.', 'jetpack-my-jetpack' ),
array( 'status' => 400 )
);
}
return true;
}
/**
* Site products endpoint.
*
* @param \WP_REST_Request $request The request object.
* @return WP_Error|\WP_REST_Response
*/
public static function get_products( $request ) {
$slugs = $request->get_param( 'products' );
$product_slugs = ! empty( $slugs ) ? array_map( 'trim', explode( ',', $slugs ) ) : array();
$response = Products::get_products( $product_slugs );
return rest_ensure_response( $response );
}
/**
* Site API product data endpoint
*
* @param \WP_REST_Request $request The request object.
*
* @return WP_Error|\WP_REST_Response
*/
public static function get_products_api_data( $request ) {
$slugs = $request->get_param( 'products' );
$product_slugs = ! empty( $slugs ) ? array_map( 'trim', explode( ',', $slugs ) ) : array();
$response = Products::get_products_api_data( $product_slugs );
return rest_ensure_response( $response );
}
/**
* Site products endpoint.
*
* @return \WP_REST_Response of site products list.
*/
public static function get_products_by_ownership() {
$response = array(
'unownedProducts' => Products::get_products_by_ownership( 'unowned' ),
'ownedProducts' => Products::get_products_by_ownership( 'owned' ),
);
return rest_ensure_response( $response );
}
/**
* Check permission to edit product
*
* @return bool
*/
public static function edit_permissions_callback() {
if ( ! current_user_can( 'activate_plugins' ) ) {
return false;
}
if ( is_multisite() && ! current_user_can( 'manage_network' ) ) {
return false;
}
return true;
}
/**
* Callback for activating products
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response|\WP_Error
*/
public static function activate_products( $request ) {
$products_array = $request->get_param( 'products' );
foreach ( $products_array as $product_slug ) {
$product = Products::get_product( $product_slug );
if ( ! isset( $product['class'] ) ) {
return new \WP_Error(
'product_class_handler_not_found',
sprintf(
/* translators: %s is the product_slug */
__( 'The product slug %s does not have an associated class handler.', 'jetpack-my-jetpack' ),
$product_slug
),
array( 'status' => 501 )
);
}
$activate_product_result = call_user_func( array( $product['class'], 'activate' ) );
if ( is_wp_error( $activate_product_result ) ) {
$activate_product_result->add_data( array( 'status' => 400 ) );
return $activate_product_result;
}
}
set_transient( 'my_jetpack_product_activated', implode( ',', $products_array ), 10 );
return rest_ensure_response( Products::get_products( $products_array ) );
}
/**
* Callback for deactivating products
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response|\WP_Error
*/
public static function deactivate_products( $request ) {
$products_array = $request->get_param( 'products' );
foreach ( $products_array as $product_slug ) {
$product = Products::get_product( $product_slug );
if ( ! isset( $product['class'] ) ) {
return new \WP_Error(
'product_class_handler_not_found',
sprintf(
/* translators: %s is the product_slug */
__( 'The product slug %s does not have an associated class handler.', 'jetpack-my-jetpack' ),
$product_slug
),
array( 'status' => 501 )
);
}
$deactivate_product_result = call_user_func( array( $product['class'], 'deactivate' ) );
if ( is_wp_error( $deactivate_product_result ) ) {
$deactivate_product_result->add_data( array( 'status' => 400 ) );
return $deactivate_product_result;
}
}
return rest_ensure_response( Products::get_products( $products_array ) );
}
/**
* Callback for installing (and activating) multiple product plugins.
*
* @param \WP_REST_Request $request The request object.
* @return \WP_REST_Response|\WP_Error
*/
public static function install_plugins( $request ) {
$products_array = $request->get_param( 'products' );
foreach ( $products_array as $product_slug ) {
$product = Products::get_product( $product_slug );
if ( ! isset( $product['class'] ) ) {
return new \WP_Error(
'product_class_handler_not_found',
sprintf(
/* translators: %s is the product_slug */
__( 'The product slug %s does not have an associated class handler.', 'jetpack-my-jetpack' ),
$product_slug
),
array( 'status' => 501 )
);
}
$install_product_result = call_user_func( array( $product['class'], 'install_and_activate_standalone' ) );
if ( is_wp_error( $install_product_result ) ) {
$install_product_result->add_data( array( 'status' => 400 ) );
return $install_product_result;
}
}
return rest_ensure_response( Products::get_products( $products_array ) );
}
/**
* Get interstitials state for the products
*
* @return WP_REST_Response
*/
public static function get_interstitials_state() {
return rest_ensure_response(
array(
'products' => Products::get_interstitials_state(),
)
);
}
/**
* Update interstitials state for the products
*
* @param WP_REST_Request $request The request object.
* @return WP_REST_Response|WP_Error
*/
public static function update_interstitials_state( WP_REST_Request $request ) {
$success = Products::update_interstitials_state( $request->get_param( 'products' ) );
if ( ! $success ) {
return new WP_Error(
'my_jetpack_interstitials_update_error',
__( 'Failed to update interstitials state.', 'jetpack-my-jetpack' ),
array( 'status' => 500 )
);
}
return rest_ensure_response(
array(
'products' => Products::get_interstitials_state(),
)
);
}
}
@@ -0,0 +1,79 @@
<?php
/**
* Sets up the Purchases REST API endpoints.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use WP_Error;
/**
* Registers the REST routes for Purchases.
*
* @phan-constructor-used-for-side-effects
*/
class REST_Purchases {
/**
* Constructor.
*/
public function __construct() {
register_rest_route(
'my-jetpack/v1',
'/site/purchases',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_current_purchases',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
$connection = new Connection_Manager();
$is_site_connected = $connection->is_connected();
if ( ! $is_site_connected ) {
return new WP_Error(
'not_connected',
__( 'Your site is not connected to Jetpack.', 'jetpack-my-jetpack' ),
array(
'status' => 400,
)
);
}
return current_user_can( 'edit_posts' );
}
/**
* Site purchases endpoint.
*
* @return array|WP_Error of site purchases.
*/
public static function get_site_current_purchases() {
$site_id = \Jetpack_Options::get_option( 'id' );
$wpcom_endpoint = sprintf( '/upgrades?site=%1$d&locale=%2$s', $site_id, get_user_locale() );
$wpcom_api_version = '1.2';
$response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, $wpcom_api_version );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $response['body'] ) || 200 !== $response_code ) {
return new WP_Error( 'site_data_fetch_failed', 'Site data fetch failed', array( 'status' => $response_code ? $response_code : 400 ) );
}
return rest_ensure_response( $body );
}
}
@@ -0,0 +1,152 @@
<?php
/**
* Sets up the Evaluation Recommendations REST API endpoints.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use WP_Error;
/**
* Registers the REST routes for Evaluation Recommendations.
*
* @phan-constructor-used-for-side-effects
*/
class REST_Recommendations_Evaluation {
/**
* Constructor.
*/
public function __construct() {
register_rest_route(
'my-jetpack/v1',
'/site/recommendations/evaluation/',
array(
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::evaluate_site_recommendations',
'permission_callback' => __CLASS__ . '::permissions_callback',
),
)
);
register_rest_route(
'my-jetpack/v1',
'/site/recommendations/evaluation/result/',
array(
array(
'methods' => \WP_REST_Server::EDITABLE,
'callback' => __CLASS__ . '::save_evaluation_recommendations',
'permission_callback' => __CLASS__ . '::permissions_callback',
),
)
);
register_rest_route(
'my-jetpack/v1',
'/site/recommendations/evaluation/result/',
array(
array(
'methods' => \WP_REST_Server::DELETABLE,
'callback' => __CLASS__ . '::dismiss_evaluation_recommendations',
'permission_callback' => __CLASS__ . '::permissions_callback',
),
)
);
}
/**
* Check user capability to access the endpoint.
*
* @access public
* @static
*
* @return true|WP_Error
*/
public static function permissions_callback() {
$connection = new Connection_Manager();
$is_site_connected = $connection->is_connected();
if ( ! $is_site_connected ) {
return new WP_Error(
'not_connected',
__( 'Your site is not connected to Jetpack.', 'jetpack-my-jetpack' ),
array(
'status' => 400,
)
);
}
return true; // We require site to be connected.
}
/**
* Recommendations Evaluation endpoint.
*
* @param \WP_REST_Request $request Query request.
*
* @return \WP_REST_Response|WP_Error of 3 product slugs (recommendations).
*/
public static function evaluate_site_recommendations( $request ) {
$goals = $request->get_param( 'goals' );
if ( ! isset( $goals ) ) {
return new WP_Error( 'missing_goals', 'Goals are required', array( 'status' => 400 ) );
}
$site_id = \Jetpack_Options::get_option( 'id' );
$wpcom_endpoint = sprintf( '/sites/%1$d/jetpack-recommendations/evaluation?goals=%2$s', $site_id, implode( ',', $goals ) );
$response = Client::wpcom_json_api_request_as_blog( $wpcom_endpoint, '2', array(), null, 'wpcom' );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $body ) || 200 !== $response_code ) {
return new WP_Error( 'recommendations_evaluation_fetch_failed', 'Evaluation processing failed', array( 'status' => $response_code ? $response_code : 400 ) );
}
return rest_ensure_response( $body );
}
/**
* Endpoint to save recommendations results.
*
* @param \WP_REST_Request $request Query request.
*
* @return \WP_REST_Response|WP_Error success response.
*/
public static function save_evaluation_recommendations( $request ) {
$json = $request->get_json_params();
if ( ! isset( $json['recommendations'] ) ) {
return new WP_Error( 'missing_recommendations', 'Recommendations are required', array( 'status' => 400 ) );
}
\Jetpack_Options::update_option( 'recommendations_evaluation', $json['recommendations'] );
\Jetpack_Options::delete_option( 'dismissed_recommendations' );
return rest_ensure_response( Initializer::get_recommended_modules() );
}
/**
* Endpoint to dismiss the recommendation section
*
* @param \WP_REST_Request $request Query request.
*
* @return \WP_REST_Response|WP_Error success response.
*/
public static function dismiss_evaluation_recommendations( $request ) {
$show_welcome_banner = $request->get_param( 'showWelcomeBanner' );
\Jetpack_Options::update_option( 'dismissed_recommendations', true );
if ( isset( $show_welcome_banner ) && $show_welcome_banner === 'true' ) {
\Jetpack_Options::update_option( 'recommendations_first_run', false );
\Jetpack_Options::delete_option( 'dismissed_welcome_banner' );
}
return rest_ensure_response( array() );
}
}
@@ -0,0 +1,124 @@
<?php
/**
* Sets up the Zendesk Chat REST API endpoints.
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use WP_Error;
use WP_REST_Response;
/**
* Registers the REST routes for Zendesk Chat.
*
* @phan-constructor-used-for-side-effects
*/
class REST_Zendesk_Chat {
const TRANSIENT_EXPIRY = 1 * MINUTE_IN_SECONDS * 60 * 24 * 7; // 1 week (JWT is actually 2 weeks, but lets be on the safe side)
const ZENDESK_AUTH_TOKEN = 'zendesk_auth_token';
/**
* Constructor.
*/
public function __construct() {
register_rest_route(
'my-jetpack/v1',
'chat/availability',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_chat_availability',
'permission_callback' => __CLASS__ . '::chat_authentication_permissions_callback',
)
);
register_rest_route(
'my-jetpack/v1',
'chat/authentication',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_chat_authentication',
'args' => array(
'type' => array(
'required' => false,
'type' => 'string',
),
'test_mode' => array(
'required' => false,
'type' => 'boolean',
),
),
'permission_callback' => __CLASS__ . '::chat_authentication_permissions_callback',
)
);
}
/**
* Ensure user is logged in if making an authentication request
*
* @access public
* @static
*
* @return WP_Error|true
*/
public static function chat_authentication_permissions_callback() {
if ( ! get_current_user_id() ) {
return new WP_Error( 'unauthorized', 'You must be logged in to access this resource.', array( 'status' => 401 ) );
}
return true;
}
/**
* Gets the chat authentication token.
*
* @return WP_Error|WP_REST_Response { token: string }
*/
public static function get_chat_authentication() {
$authentication = get_transient( self::ZENDESK_AUTH_TOKEN );
if ( $authentication ) {
return rest_ensure_response( $authentication );
}
$proxied = function_exists( 'wpcom_is_proxied_request' ) ? wpcom_is_proxied_request() : false;
$wpcom_endpoint = 'help/authenticate/chat';
$wpcom_api_version = '2';
$body = array(
'type' => 'zendesk',
'test_mode' => $proxied ? true : false,
);
$response = Client::wpcom_json_api_request_as_user( $wpcom_endpoint, $wpcom_api_version, array( 'method' => 'POST' ), $body );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
return new WP_Error( 'chat_authentication_failed', 'Chat authentication failed', array( 'status' => $response_code ) );
}
set_transient( self::ZENDESK_AUTH_TOKEN, $body, self::TRANSIENT_EXPIRY );
return rest_ensure_response( $body );
}
/**
* Calls `wpcom/v2/presales/chat?group=jp_presales` endpoint.
* This endpoint returns whether or not the Jetpack presales chat group is available
*
* @return WP_Error|WP_REST_Response { is_available: bool }
*/
public static function get_chat_availability() {
$wpcom_endpoint = '/presales/chat?group=jp_presales';
$wpcom_api_version = '2';
$response = Client::wpcom_json_api_request_as_user( $wpcom_endpoint, $wpcom_api_version );
$response_code = wp_remote_retrieve_response_code( $response );
$body = json_decode( wp_remote_retrieve_body( $response ) );
if ( is_wp_error( $response ) || empty( $response['body'] ) ) {
return new WP_Error( 'chat_config_data_fetch_failed', 'Chat config data fetch failed', array( 'status' => $response_code ) );
}
return rest_ensure_response( $body );
}
}
@@ -0,0 +1,405 @@
<?php
/**
* Fetches and store the list of Jetpack products available in WPCOM
*
* @package automattic/my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Current_Plan;
use Automattic\Jetpack\Status\Visitor;
use Jetpack_Options;
use WP_Error;
/**
* Stores the list of products available for purchase in WPCOM
*/
class Wpcom_Products {
/**
* The meta name used to store the cache date
*
* @var string
*/
const CACHE_DATE_META_NAME = 'my-jetpack-cache-date';
/**
* The meta name used to store the cache
*
* @var string
*/
const CACHE_META_NAME = 'my-jetpack-cache';
const CACHE_CHECK_HASH_NAME = 'my-jetpack-wpcom-product-check-hash';
const MY_JETPACK_PURCHASES_TRANSIENT_KEY = 'my-jetpack-purchases';
/**
* Store the data on failed WPCOM requests.
*
* @var array
*/
private static $wpcom_request_failures = array();
/**
* Fetches the list of products from WPCOM
*
* @return Object|WP_Error
*/
private static function get_products_from_wpcom() {
$connection = new Connection_Manager();
$blog_id = \Jetpack_Options::get_option( 'id' );
$ip = ( new Visitor() )->get_ip( true );
$headers = array(
'X-Forwarded-For' => $ip,
);
if ( $blog_id ) {
$request_label = 'get_products_from_wpcom_blog_' . $blog_id;
$request_failure = static::get_request_failure( $request_label );
if ( null !== $request_failure ) {
return $request_failure;
}
// If has a blog id, use connected endpoint.
$endpoint = sprintf( '/sites/%d/products/?_locale=%s&type=jetpack', $blog_id, get_user_locale() );
// If available in the user data, set the user's currency as one of the params
if ( $connection->is_user_connected() ) {
$user_details = $connection->get_connected_user_data();
if ( ! empty( $user_details['user_currency'] ) && $user_details['user_currency'] !== 'USD' ) {
$endpoint .= sprintf( '&currency=%s', $user_details['user_currency'] );
}
}
$wpcom_request = Client::wpcom_json_api_request_as_blog(
$endpoint,
'1.1',
array(
'method' => 'GET',
'headers' => $headers,
)
);
} else {
$request_label = 'get_products_from_wpcom';
$request_failure = static::get_request_failure( $request_label );
if ( null !== $request_failure ) {
return $request_failure;
}
$endpoint = 'https://public-api.wordpress.com/rest/v1.1/products?locale=' . get_user_locale() . '&type=jetpack';
$wpcom_request = wp_remote_get(
esc_url_raw( $endpoint ),
array(
'headers' => $headers,
)
);
}
$response_code = wp_remote_retrieve_response_code( $wpcom_request );
if ( 200 === $response_code ) {
return json_decode( wp_remote_retrieve_body( $wpcom_request ) );
} else {
$error = new WP_Error(
'failed_to_fetch_wpcom_products',
esc_html__( 'Unable to fetch the products list from WordPress.com', 'jetpack-my-jetpack' ),
array( 'status' => $response_code )
);
static::set_request_failure( $request_label, $error );
return $error;
}
}
/**
* Super unintelligent hash string that can help us reset the cache after connection changes
* This is important because the currency can change after a user connects depending on what is set in their profile
*
* @return string
*/
private static function build_check_hash() {
static $has_user_data_fetch_error = false;
$hash_string = 'check_hash_';
$connection = new Connection_Manager();
if ( $connection->is_connected() ) {
$hash_string .= 'site_connected_';
}
if ( $connection->is_user_connected() ) {
$hash_string .= 'user_connected';
// Add the user's currency
$user_details = $has_user_data_fetch_error ? false : $connection->get_connected_user_data();
if ( $user_details === false ) {
$has_user_data_fetch_error = true;
} elseif ( ! empty( $user_details['user_currency'] ) ) {
$hash_string .= '_' . $user_details['user_currency'];
}
}
return md5( $hash_string );
}
/**
* Update the cache with new information retrieved from WPCOM
*
* We store one cache for each user, as the information is internationalized based on user preferences
* Also, the currency is based on the user IP address
*
* @param Object $products_list The products list as received from WPCOM.
* @return bool
*/
private static function update_cache( $products_list ) {
update_user_meta( get_current_user_id(), self::CACHE_DATE_META_NAME, time() );
update_user_meta( get_current_user_id(), self::CACHE_CHECK_HASH_NAME, self::build_check_hash() );
return update_user_meta( get_current_user_id(), self::CACHE_META_NAME, $products_list );
}
/**
* Checks if the cache is old, meaning we need to fetch new data from WPCOM
*/
private static function is_cache_old() {
if ( empty( self::get_products_from_cache() ) ) {
return true;
}
// This allows the cache to reset after the site or user connects/ disconnects
$check_hash = get_user_meta( get_current_user_id(), self::CACHE_CHECK_HASH_NAME, true );
if ( $check_hash !== self::build_check_hash() ) {
return true;
}
$cache_date = get_user_meta( get_current_user_id(), self::CACHE_DATE_META_NAME, true );
return time() - (int) $cache_date > DAY_IN_SECONDS;
}
/**
* Gets the product list from the user cache
*/
private static function get_products_from_cache() {
return get_user_meta( get_current_user_id(), self::CACHE_META_NAME, true );
}
/**
* Gets the product list
*
* Attempts to retrieve the products list from the user cache if cache is not too old.
* If cache is old, it will attempt to fetch information from WPCOM. If it fails, we return what we have in cache, if anything, otherwise we return an error.
*
* @param bool $skip_cache If true it will ignore the cache and attempt to fetch fresh information from WPCOM.
*
* @return Object|WP_Error
*/
public static function get_products( $skip_cache = false ) {
// This is only available for logged in users.
if ( ! get_current_user_id() ) {
return null;
}
if ( ! self::is_cache_old() && ! $skip_cache ) {
return self::get_products_from_cache();
}
$products = self::get_products_from_wpcom();
if ( is_wp_error( $products ) ) {
// Let's see if we have it cached.
$cached = self::get_products_from_cache();
if ( ! empty( $cached ) ) {
return $cached;
} else {
return $products;
}
}
self::update_cache( $products );
return $products;
}
/**
* Get one product
*
* @param string $product_slug The product slug.
* @param bool $renew_cache A flag to force the cache to be renewed.
*
* @return ?Object The product details if found
*/
public static function get_product( $product_slug, $renew_cache = false ) {
$products = self::get_products( $renew_cache );
if ( ! empty( $products->$product_slug ) ) {
return $products->$product_slug;
}
}
/**
* Get only the product currency code and price in an array
*
* @param string $product_slug The product slug.
*
* @return array An array with currency_code and full_price. Empty array if product not found.
*/
public static function get_product_pricing( $product_slug ) {
$product = self::get_product( $product_slug );
if ( empty( $product ) ) {
return array();
}
$cost = $product->cost;
$discount_price = $cost;
$is_introductory_offer = false;
$introductory_offer = null;
// Get/compute the discounted price.
if ( isset( $product->introductory_offer->cost_per_interval ) ) {
$discount_price = $product->introductory_offer->cost_per_interval;
$is_introductory_offer = true;
$introductory_offer = $product->introductory_offer;
}
$pricing = array(
'currency_code' => $product->currency_code,
'full_price' => $cost,
'discount_price' => $discount_price,
'is_introductory_offer' => $is_introductory_offer,
'introductory_offer' => $introductory_offer,
'product_term' => $product->product_term,
);
return self::populate_with_discount( $product, $pricing, $discount_price );
}
/**
* Populate the pricing array with the discount information.
*
* @param object $product - The product object.
* @param array $pricing - The pricing array.
* @param float $price - The price to be discounted.
* @return array The pricing array with the discount information.
*/
public static function populate_with_discount( $product, $pricing, $price ) {
// Check whether the product has a coupon.
if ( ! isset( $product->sale_coupon ) ) {
return $pricing;
}
// Check whether it is still valid.
$coupon = $product->sale_coupon;
$coupon_start_date = strtotime( $coupon->start_date );
$coupon_expires = strtotime( $coupon->expires );
if ( $coupon_start_date > time() || $coupon_expires < time() ) {
return $pricing;
}
$coupon_discount = intval( $coupon->discount );
// Populate response with coupon discount.
$pricing['coupon_discount'] = $coupon_discount;
// Apply coupon discount to the price.
$pricing['discount_price'] = $price * ( 100 - $coupon_discount ) / 100;
return $pricing;
}
/**
* Gets the site purchases from WPCOM.
*
* @return Object|WP_Error
*/
public static function get_site_current_purchases() {
static $purchases = null;
if ( $purchases !== null ) {
return $purchases;
}
// Check for a cached value before doing lookup
$stored_purchases = get_transient( self::MY_JETPACK_PURCHASES_TRANSIENT_KEY );
if ( $stored_purchases !== false ) {
return $stored_purchases;
}
$request_failure = static::get_request_failure( 'get_site_current_purchases' );
if ( null !== $request_failure ) {
return $request_failure;
}
$site_id = Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog(
sprintf( '/upgrades?site=%d', $site_id ),
'1.2',
array(
'method' => 'GET',
)
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$error = new WP_Error( 'purchases_state_fetch_failed' );
static::set_request_failure( 'get_site_current_purchases', $error );
return $error;
}
$body = wp_remote_retrieve_body( $response );
$purchases = json_decode( $body );
// Set short transient to help with repeated lookups on the same page load
set_transient( self::MY_JETPACK_PURCHASES_TRANSIENT_KEY, $purchases, 5 );
return $purchases;
}
/**
* Gets the site's currently active "plan" (bundle).
*
* @param bool $reload Whether to refresh data from wpcom or not.
* @return array
*/
public static function get_site_current_plan( $reload = false ) {
static $reloaded_already = false;
if ( $reload && ! $reloaded_already ) {
Current_Plan::refresh_from_wpcom();
$reloaded_already = true;
}
return Current_Plan::get();
}
/**
* Reset the request failures to retry the API requests.
*
* @return void
*/
public static function reset_request_failures() {
static::$wpcom_request_failures = array();
}
/**
* Record the request failure to prevent repeated requests.
*
* @param string $request_label The request label.
* @param WP_Error $error The error.
*
* @return void
*/
private static function set_request_failure( $request_label, WP_Error $error ) {
static::$wpcom_request_failures[ $request_label ] = $error;
}
/**
* Get the pre-saved request failure if exists.
*
* @param string $request_label The request label.
*
* @return null|WP_Error
*/
private static function get_request_failure( $request_label ) {
if ( array_key_exists( $request_label, static::$wpcom_request_failures ) ) {
return static::$wpcom_request_failures[ $request_label ];
}
return null;
}
}
@@ -0,0 +1,198 @@
<?php
/**
* Anti_Spam product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Anti_Spam product
*/
class Anti_Spam extends Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'anti-spam';
/**
* The filename (id) of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
*
* @var string
*/
public static $plugin_filename = 'akismet/akismet.php';
/**
* The slug of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
*
* @var string
*/
public static $plugin_slug = 'akismet';
/**
* The category of the product
*
* @var string
*/
public static $category = 'security';
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'antispam';
/**
* Whether this product requires a user connection
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Akismet has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Akismet Anti-spam';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Akismet Anti-spam';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Automatically clear spam from comments and forms.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Save time and get better responses by automatically blocking spam from your comments and forms.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Boost features list
*/
public static function get_features() {
return array(
_x( 'Comment and form spam protection', 'Anti-Spam Product Feature', 'jetpack-my-jetpack' ),
_x( 'Block spam without CAPTCHAs', 'Anti-Spam Product Feature', 'jetpack-my-jetpack' ),
_x( 'Advanced stats', 'Anti-Spam Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_anti_spam',
'jetpack_anti_spam_monthly',
'jetpack_anti_spam_bi_yearly',
);
}
/**
* Check if the product has a free plan
* In this case we are only checking for an API key. The has_paid_plan_for_product will check to see if the specific site has a paid plan
*
* @return bool
*/
public static function has_free_plan_for_product() {
$akismet_api_key = apply_filters( 'akismet_get_api_key', defined( 'WPCOM_API_KEY' ) ? constant( 'WPCOM_API_KEY' ) : get_option( 'wordpress_api_key' ) );
if ( ! empty( $akismet_api_key ) ) {
return true;
}
return false;
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_anti_spam';
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security', 'complete' );
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=akismet-key-config' );
}
}
@@ -0,0 +1,489 @@
<?php
/**
* Backup product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Redirect;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Backup product
*/
class Backup extends Hybrid_Product {
public const BACKUP_STATUS_TRANSIENT_KEY = 'my-jetpack-backup-status';
/**
* The product slug
*
* @var string
*/
public static $slug = 'backup';
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-backup/jetpack-backup.php',
'backup/jetpack-backup.php',
'jetpack-backup-dev/jetpack-backup.php',
);
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
public static $plugin_slug = 'jetpack-backup';
/**
* The category of the product
*
* @var string
*/
public static $category = 'security';
/**
* Backup has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = false;
/**
* Whether this product requires a plan to work at all
*
* @var bool
*/
public static $requires_plan = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'backups';
/**
* Backup initialization
*
* @return void
*/
public static function register_endpoints(): void {
parent::register_endpoints();
// Get backup undo event
register_rest_route(
'my-jetpack/v1',
'/site/backup/undo-event',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_backup_undo_event',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'VaultPress Backup';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack VaultPress Backup';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Real-time backups save every change, and one-click restores get you back online quickly.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Never lose a word, image, page, or time worrying about your site with automated backups & one-click restores.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Backup features list
*/
public static function get_features() {
return array(
_x( 'Real-time cloud backups', 'Backup Product Feature', 'jetpack-my-jetpack' ),
_x( '10GB of backup storage', 'Backup Product Feature', 'jetpack-my-jetpack' ),
_x( '30-day archive & activity log*', 'Backup Product Feature', 'jetpack-my-jetpack' ),
_x( 'One-click restores', 'Backup Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get disclaimers corresponding to a feature
*
* @return array Backup disclaimers list
*/
public static function get_disclaimers() {
return array(
array(
'text' => _x( '* Subject to your usage and storage limit.', 'Backup Product Disclaimer', 'jetpack-my-jetpack' ),
'link_text' => _x( 'Learn more', 'Backup Product Disclaimer', 'jetpack-my-jetpack' ),
'url' => Redirect::get_url( 'jetpack-faq-backup-disclaimer' ),
),
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_backup_t1_yearly';
}
/**
* Get the URL where the user should be redirected after checkout
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Checks if the user has the correct permissions
*/
public static function permissions_callback() {
return current_user_can( 'manage_options' );
}
/**
* This will fetch the last rewindable event from the Activity Log and
* the last rewind_id prior to that.
*
* @return array|WP_Error|null
*/
public static function get_site_backup_undo_event() {
$blog_id = \Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_user(
'/sites/' . $blog_id . '/activity/rewindable?force=wpcom',
'v2',
array(),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
return null;
}
$body = json_decode( $response['body'], true );
if ( ! isset( $body['current'] ) ) {
return null;
}
// Preparing the response structure
$undo_event = array(
'last_rewindable_event' => null,
'undo_backup_id' => null,
);
// List of events that will not be considered to be undo.
// Basically we should not `undo` a full backup event, but we could
// use them to undo any other action like plugin updates.
$last_event_exceptions = array(
'rewind__backup_only_complete_full',
'rewind__backup_only_complete_initial',
'rewind__backup_only_complete',
'rewind__backup_complete_full',
'rewind__backup_complete_initial',
'rewind__backup_complete',
);
// Looping through the events to find the last rewindable event and the last backup_id.
// The idea is to find the last rewindable event and then the last rewind_id before that.
$found_last_event = false;
foreach ( $body['current']['orderedItems'] as $event ) {
if ( $event['is_rewindable'] ) {
if ( ! $found_last_event && ! in_array( $event['name'], $last_event_exceptions, true ) ) {
$undo_event['last_rewindable_event'] = $event;
$found_last_event = true;
} elseif ( $found_last_event ) {
$undo_event['undo_backup_id'] = $event['rewind_id'];
break;
}
}
}
return rest_ensure_response( $undo_event );
}
/**
* Hits the wpcom api to check rewind status.
*
* @todo Maybe add caching.
*
* @return object|WP_Error
*/
private static function get_state_from_wpcom() {
static $status = null;
if ( $status !== null ) {
return $status;
}
$site_id = \Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog(
sprintf( '/sites/%d/rewind', $site_id ) . '?force=wpcom',
'2',
array( 'timeout' => 2 ),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$status = new WP_Error( 'rewind_state_fetch_failed' );
return $status;
}
$body = wp_remote_retrieve_body( $response );
$status = json_decode( $body );
return $status;
}
/**
* Hits the wpcom api to retrieve the last 10 backup records.
*
* @return object|WP_Error
*/
public static function get_latest_backups() {
static $backups = null;
if ( $backups !== null ) {
return $backups;
}
$site_id = \Jetpack_Options::get_option( 'id' );
$response = Client::wpcom_json_api_request_as_blog(
sprintf( '/sites/%d/rewind/backups', $site_id ) . '?force=wpcom',
'2',
array( 'timeout' => 2 ),
null,
'wpcom'
);
if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
$backups = new WP_Error( 'rewind_backups_fetch_failed' );
return $backups;
}
$body = wp_remote_retrieve_body( $response );
$backups = json_decode( $body );
return $backups;
}
/**
* Determines whether the module/plugin/product needs the users attention.
* Typically due to some sort of error where user troubleshooting is needed.
*
* @return boolean|array
*/
public static function does_module_need_attention() {
$previous_backup_status = get_transient( self::BACKUP_STATUS_TRANSIENT_KEY );
// If we have a previous backup status, show it.
if ( ! empty( $previous_backup_status ) ) {
return $previous_backup_status === 'no_errors' ? false : $previous_backup_status;
}
$backup_failed_status = false;
// First check the status of Rewind for failure.
$rewind_state = self::get_state_from_wpcom();
if ( ! is_wp_error( $rewind_state ) ) {
// Special case: 'unavailable' with 'site_new' reason is a normal provisioning state for brand new sites.
$is_new_site_provisioning = ( 'unavailable' === $rewind_state->state &&
'site_new' === ( $rewind_state->reason ?? '' ) );
if (
! in_array( $rewind_state->state, array( 'active', 'provisioning', 'awaiting_credentials' ), true ) &&
! $is_new_site_provisioning
) {
$backup_failed_status = array(
'type' => 'error',
'data' => array(
'source' => 'rewind',
'status' => isset( $rewind_state->reason ) && ! empty( $rewind_state->reason ) ? $rewind_state->reason : $rewind_state->state,
'last_updated' => $rewind_state->last_updated,
),
);
}
}
// Next check for a failed last backup.
$latest_backups = self::get_latest_backups();
if ( ! is_wp_error( $latest_backups ) ) {
// Get the last/latest backup record.
$last_backup = null;
foreach ( $latest_backups as $backup ) {
if ( $backup->is_backup ) {
$last_backup = $backup;
break;
}
}
if ( $last_backup && isset( $last_backup->status ) ) {
if ( $last_backup->status !== 'started' && ! preg_match( '/-will-retry$/', $last_backup->status ) && $last_backup->status !== 'finished' ) {
$backup_failed_status = array(
'type' => 'error',
'data' => array(
'source' => 'last_backup',
'status' => $last_backup->status,
'last_updated' => $last_backup->last_updated,
),
);
}
}
}
if ( is_array( $backup_failed_status ) ) {
set_transient( self::BACKUP_STATUS_TRANSIENT_KEY, $backup_failed_status, 5 * MINUTE_IN_SECONDS );
} else {
set_transient( self::BACKUP_STATUS_TRANSIENT_KEY, 'no_errors', HOUR_IN_SECONDS );
}
return $backup_failed_status;
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security', 'complete' );
}
/**
* Get the URL the user is taken after activating the product
*
* @return ?string
*/
public static function get_post_activation_url() {
return ''; // stay in My Jetpack page or continue the purchase flow if needed.
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
// check standalone first
if ( static::is_standalone_plugin_active() ) {
return admin_url( 'admin.php?page=jetpack-backup' );
// otherwise, check for the main Jetpack plugin
} elseif ( static::is_jetpack_plugin_active() ) {
return Redirect::get_url( 'my-jetpack-manage-backup' );
}
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_backup_daily',
'jetpack_backup_daily_monthly',
'jetpack_backup_realtime',
'jetpack_backup_realtime_monthly',
'jetpack_backup_t1_yearly',
'jetpack_backup_t1_monthly',
'jetpack_backup_t1_bi_yearly',
'jetpack_backup_t2_yearly',
'jetpack_backup_t2_monthly',
'jetpack_backup_t0_yearly',
'jetpack_backup_t0_monthly',
);
}
/**
* Override the product status to return INACTIVE when backups are deactivated.
*
* @return string
*/
public static function get_status() {
// Get the default status from parent.
$status = parent::get_status();
// Check if backups are deactivated (not an error, just manually turned off).
$needs_attention = static::does_module_need_attention();
if (
is_array( $needs_attention ) &&
isset( $needs_attention['data']['status'] ) &&
'backups-deactivated' === $needs_attention['data']['status']
) {
// Preserve NEEDS_PLAN status - user must purchase before reactivating.
if ( \Automattic\Jetpack\My_Jetpack\Products::STATUS_NEEDS_PLAN === $status ) {
return $status;
}
return \Automattic\Jetpack\My_Jetpack\Products::STATUS_INACTIVE;
}
return $status;
}
}
@@ -0,0 +1,409 @@
<?php
/**
* Boost product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Boost product
*/
class Boost extends Product {
const FREE_TIER_SLUG = 'free';
const UPGRADED_TIER_SLUG = 'upgraded';
const UPGRADED_TIER_PRODUCT_SLUG = 'jetpack_boost_yearly';
/**
* The product slug
*
* @var string
*/
public static $slug = 'boost';
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-boost/jetpack-boost.php',
'boost/jetpack-boost.php',
'jetpack-boost-dev/jetpack-boost.php',
);
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
public static $plugin_slug = 'jetpack-boost';
/**
* The category of the product
*
* @var string
*/
public static $category = 'performance';
/**
* Defines whether or not to show a product interstitial as tiered pricing or not
*
* @var bool
*/
public static $is_tiered_pricing = true;
/**
* Boost has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Whether this product requires a user connection
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'cloud-critical-css';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Boost';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Boost';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Improves your site speed and performance.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Fast sites get more page visits, more conversions, and better SEO rankings. Boost speeds up your site in seconds.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Boost features list
*/
public static function get_features() {
return array(
__( 'Check your site performance', 'jetpack-my-jetpack' ),
__( 'Enable improvements in one click', 'jetpack-my-jetpack' ),
__( 'Standalone free plugin for those focused on speed', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product's available tiers
*
* @return string[] Slugs of the available tiers
*/
public static function get_tiers() {
return array(
self::UPGRADED_TIER_SLUG,
self::FREE_TIER_SLUG,
);
}
/**
* Get the internationalized comparison of free vs upgraded features
*
* @return array[] Protect features comparison
*/
public static function get_features_by_tier() {
return array(
array(
'name' => __( 'Auto CSS Optimization', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Move important styling information to the start of the page, which helps pages display your content sooner, so your users dont have to wait for the entire page to load. Commonly referred to as Critical CSS.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => false,
'description' => __( 'Manual', 'jetpack-my-jetpack' ),
'info' => array(
'title' => __( 'Manual Critical CSS regeneration', 'jetpack-my-jetpack' ),
'content' => __(
'<p>To enhance the speed of your site, with this plan you will need to optimize CSS by using the Manual Critical CSS generation feature whenever you:</p>
<ul>
<li>Make theme changes.</li>
<li>Write a new post/page.</li>
<li>Edit a post/page.</li>
<li>Activate, deactivate, or update plugins that impact your site layout or HTML structure.</li>
<li>Change settings of plugins that impact your site layout or HTML structure.</li>
<li>Upgrade your WordPress version if the new release includes core CSS changes.</li>
</ul>',
'jetpack-my-jetpack'
),
),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Included', 'jetpack-my-jetpack' ),
'info' => array(
'title' => __( 'Automatic Critical CSS regeneration', 'jetpack-my-jetpack' ),
'content' => __(
'<p>Its essential to regenerate Critical CSS to optimize your site speed whenever your HTML or CSS structure changes. Being on top of this can be tedious and time-consuming.</p>
<p>Boosts cloud service can automatically detect when your site needs the Critical CSS regenerated, and perform this function behind the scenes without requiring you to monitor it manually.</p>',
'jetpack-my-jetpack'
),
),
),
),
),
array(
'name' => __( 'Historical performance scores', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Get access to your historical performance scores and see advanced Core Web Vitals data.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Dedicated email support', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>Paid customers get dedicated email support from our world-class Happiness Engineers to help with any issue.</p>
<p>All other questions are handled by our team as quickly as we are able to go through the WordPress support forum.</p>',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Page Cache', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Page caching speeds up load times by storing a copy of each web page on the first visit, allowing subsequent visits to be served instantly. This reduces server load and improves user experience by delivering content faster, without waiting for the page to be generated again.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Image CDN Quality Settings', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Fine-tune image quality settings to your liking.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Image CDN Auto-Resize Lazy Images', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Optimizes lazy-loaded images by dynamically serving perfectly sized images for each device.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Image CDN', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Deliver images from Jetpack\'s Content Delivery Network. Automatically resizes your images to an appropriate size, converts them to modern efficient formats like WebP, and serves them from a worldwide network of servers.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Image guide', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Discover and fix images with a suboptimal resolution, aspect ratio, or file size, improving user experience and page speed.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Defer non-essential JavaScript', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Run non-essential JavaScript after the page has loaded so that styles and images can load more quickly.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Concatenate JS and CSS', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Boost your website performance by merging and compressing JavaScript and CSS files, reducing site loading time and number of requests.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
);
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'tiers' => array(
self::FREE_TIER_SLUG => array(
'available' => true,
'is_free' => true,
),
self::UPGRADED_TIER_SLUG => array_merge(
array(
'available' => true,
'wpcom_product_slug' => self::UPGRADED_TIER_PRODUCT_SLUG,
),
Wpcom_Products::get_product_pricing( self::UPGRADED_TIER_PRODUCT_SLUG )
),
),
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack-boost' );
}
/**
* Activates the product by installing and activating its plugin
*
* @param bool|WP_Error $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return boolean|WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
// A bundle is not a module. There's nothing in the plugin to be activated, so it's ok to fail to activate the module.
$product_activation = true;
}
// We just "got started" in My Jetpack, so skip the in-plugin experience.
update_option( 'jb_get_started', false );
return $product_activation;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_boost_yearly',
'jetpack_boost_monthly',
'jetpack_boost_bi_yearly',
);
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'complete' );
}
}
@@ -0,0 +1,276 @@
<?php
/**
* Complete plan
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Complete plan
*/
class Complete extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'complete';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'complete';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Complete Bundle';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Complete';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'The ultimate tool kit for best-in-class websites.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_long_description() {
return __( 'Get the full Jetpack suite with real-time security tools, improved site performance, and tools to grow your business.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
* Most of these are not translated as they are product names
*
* @return array Complete features list
*/
public static function get_features() {
return array(
'VaultPress Backup',
'Scan',
'Akismet Anti-spam',
'VideoPress',
'Boost',
'Social',
'Search',
'AI Assistant',
_x( 'Stats (100K site views, upgradeable)', 'Complete Product Feature', 'jetpack-my-jetpack' ),
_x( 'CRM Entrepreneur', 'Complete Product Feature', 'jetpack-my-jetpack' ),
_x( 'Newsletter and monetization tools', 'Complete Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
$product_slug = static::get_wpcom_product_slug();
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => $product_slug,
),
Wpcom_Products::get_product_pricing( $product_slug )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_complete';
}
/**
* Checks whether the Jetpack module is active
*
* This is a bundle and not a product. We should not use this information for anything
*
* @return bool
*/
public static function is_module_active() {
return false;
}
/**
* Activates the product by installing and activating its plugin
*
* @param WP_Error|bool $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return bool|\WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
// A bundle is not a module. There's nothing in the plugin to be activated, so it's ok to fail to activate the module.
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
return $product_activation;
}
// At this point, Jetpack plugin is installed. Let's activate each individual product.
$activation = Social::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Stats::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Anti_Spam::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Backup::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Scan::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Boost::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = CRM::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Search::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = VideoPress::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
return $activation;
}
/**
* Checks whether the Product is active
*
* Security is a bundle and not a module. Activation takes place on WPCOM. So lets consider it active if jetpack is active and has the plan.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active() && static::has_required_plan();
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_required_plan() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_complete',
'jetpack_complete_monthly',
'jetpack_complete_bi_yearly',
);
}
/**
* Checks whether product is a bundle.
*
* @return boolean True
*/
public static function is_bundle_product() {
return true;
}
/**
* Return all the products it contains.
*
* @return array Product slugs
*/
public static function get_supported_products() {
return array(
'anti-spam',
'backup',
'boost',
'crm',
'scan',
'search',
'social',
'stats',
'videopress',
'jetpack-ai',
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return '';
}
}
@@ -0,0 +1,360 @@
<?php
/**
* Creator product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Creator product
*/
class Creator extends Product {
const FREE_TIER_SLUG = 'free';
const UPGRADED_TIER_SLUG = 'upgraded';
const UPGRADED_TIER_PRODUCT_SLUG = 'jetpack_creator_yearly';
/**
* The product slug
*
* @var string
*/
public static $slug = 'creator';
/**
* The slug of the plugin associated with this product - Creator functionalities are part of Jetpack's main plugin
*
* @var string
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* Get the plugin filename - ovewrite it and return Jetpack's
*
* @return ?string
*/
public static function get_plugin_filename() {
return self::JETPACK_PLUGIN_FILENAME;
}
/**
* Whether this product requires a user connection
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Creator';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Creator';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Get more subscribers and keep them engaged with our creator tools', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Craft stunning content, boost your subscriber base, and monetize your audience with subscriptions.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Features list
*/
public static function get_features() {
return array(
__( 'Create content that stands out', 'jetpack-my-jetpack' ),
__( 'Grow your subscribers through our creator network and tools', 'jetpack-my-jetpack' ),
__( 'Monetize your online presence and earn from your website', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product's available tiers
*
* @return string[] Slugs of the available tiers
*/
public static function get_tiers() {
return array(
self::UPGRADED_TIER_SLUG,
self::FREE_TIER_SLUG,
);
}
/**
* Get the internationalized comparison of free vs upgraded features
*
* @return array[] Protect features comparison
*/
public static function get_features_by_tier() {
return array(
array(
'name' => __( 'Import subscribers', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Import a CSV file of your existing subscribers to be sent your Newsletter.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => true,
'description' => __( '100 subscribers', 'jetpack-my-jetpack' ),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Unlimited subscribers', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Transaction fees', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>Fees are only collected when you accept payments.</p>
<p>Fees are based on the Jetpack plan you have and are calculated as a percentage of your revenue from 10% on the Free plan to 2% on the Creator plan (plus Stripe fees).</p>',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => true,
'description' => __( '10%', 'jetpack-my-jetpack' ),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( '2%', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Jetpack Blocks', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Jetpack has over 40 Gutenberg blocks to help you with your content creation, such as displaying your podcasts, showing different content to repeat visitors, creating contact forms and many more.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Paid content gating', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Lock your content behind a paid content block. To access the content, readers will need to pay a one-time fee or a recurring subscription.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Paywall access', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Add a Paywall to your content which lets your visitors read a section of your content before being asked to subscribe to continue reading.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Newsletter', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Start a Newsletter by sending your content as an email newsletter direct to your fans email inboxes.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Pay with PayPal', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'Accept payment with PayPal for simple payments like eBooks, courses and more.',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'WordAds', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>WordAds adds advertisements to your website. Start earning from your website traffic.</p>
<p>Over 50 internet advertisers including Google AdSense & Adx, AppNexus, Amazon A9, AOL Marketplace, Yahoo, Criteo, and more bid to display ads in WordAds spots.</p>',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Dedicated email support', 'jetpack-my-jetpack' ),
'info' => array(
'content' => __(
'<p>Paid customers get dedicated email support from our world-class Happiness Engineers to help with any issue.</p>
<p>All other questions are handled by our team as quickly as we are able to go through the WordPress support forum.</p>',
'jetpack-my-jetpack'
),
),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'tiers' => array(
self::FREE_TIER_SLUG => array(
'available' => true,
'is_free' => true,
),
self::UPGRADED_TIER_SLUG => array_merge(
array(
'available' => true,
'wpcom_product_slug' => self::UPGRADED_TIER_PRODUCT_SLUG,
),
Wpcom_Products::get_product_pricing( self::UPGRADED_TIER_PRODUCT_SLUG )
),
),
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack#/settings?term=creator' );
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_creator_yearly';
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_biyearly_product_slug() {
return 'jetpack_creator_bi_yearly';
}
/**
* Get the WPCOM monthly product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_monthly_product_slug() {
return 'jetpack_creator_monthly';
}
/**
* Get the product-slugs of the paid bundles/plans that this product/module is included in
*
* @return array
*/
public static function get_paid_bundles_that_include_product() {
return array(
'jetpack_complete',
'jetpack_complete_monthly',
'jetpack_complete_bi-yearly',
);
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_creator_yearly',
'jetpack_creator_monthly',
'jetpack_creator_bi_yearly',
);
}
/**
* Checks whether the product can be upgraded - i.e. this shows the /#add-creator interstitial
*
* @return boolean
*/
public static function is_upgradable() {
return ! self::has_paid_plan_for_product();
}
}
@@ -0,0 +1,209 @@
<?php
/**
* Boost product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the CRM product
*/
class Crm extends Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'crm';
/**
* The filename (id) of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
*
* @var string
*/
public static $plugin_filename = array(
'zero-bs-crm/ZeroBSCRM.php',
'crm/ZeroBSCRM.php',
);
/**
* The slug of the plugin associated with this product. If not defined, it will default to the Jetpack plugin
*
* @var string
*/
public static $plugin_slug = 'zero-bs-crm';
/**
* The category of the product
*
* @var string
*/
public static $category = 'management';
/**
* Whether this product requires a user connection
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* CRM has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'CRM';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack CRM';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'All of the tools you need to grow your business.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Build better relationships with your customers and grow your business.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array CRM features list
*/
public static function get_features() {
return array(
__( 'Manage unlimited contacts', 'jetpack-my-jetpack' ),
__( 'Manage billing and create invoices', 'jetpack-my-jetpack' ),
__( 'Fully integrated with WordPress & WooCommerce', 'jetpack-my-jetpack' ),
__( 'Infinitely customizable with integrations and extensions', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
// We are hard coding pricing info for CRM because it is not available to us through the CRM API.
return array(
'available' => true,
'is_free' => false,
'full_price' => 132,
'discount_price' => 132,
'is_introductory_offer' => false,
'product_term' => 'year',
'introductory_offer' => null,
// CRM is only sold in USD
'currency_code' => 'USD',
);
}
/**
* Get the URL the user is taken after activating the product
*
* @return ?string
*/
public static function get_post_activation_url() {
return admin_url( 'admin.php?page=zerobscrm-plugin' ); // Welcome page.
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=zerobscrm-dash' );
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
* CRM is available as part of Jetpack Complete
*
* @return boolean
*/
public static function has_paid_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
// TODO: check if CRM has a separate plan
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ) {
return true;
}
}
}
return false;
}
/**
* Get the product-slugs of the paid bundles/plans that this product/module is included in.
*
* @return array
*/
public static function get_paid_bundles_that_include_product() {
return array(
'jetpack_complete',
'jetpack_complete_monthly',
'jetpack_complete_bi_yearly',
);
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'complete' );
}
}
@@ -0,0 +1,144 @@
<?php
/**
* Extras product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Product;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Extras product.
* Extras, so far, could be considered as Jetpack plugin bridge.
*/
class Extras extends Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'extras';
/**
* The slug of the plugin associated with this product.
* Extras, is in short, Jetpack plugin bridge so far.
*
* @var string
*/
public static $plugin_slug = 'jetpack';
/**
* Whether this product requires a user connection
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Extras';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Extras';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Basic tools for a successful site', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( "Secure and speed up your site for free with Jetpack's powerful WordPress tools.", 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Boost features list
*/
public static function get_features() {
return array(
__( 'Speed up your site with optimized images', 'jetpack-my-jetpack' ),
__( 'Protect your site against bot attacks', 'jetpack-my-jetpack' ),
__( 'Get notifications if your site goes offline', 'jetpack-my-jetpack' ),
__( 'Enhance your site with dozens of other features', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'available' => true,
'is_free' => true,
);
}
/**
* Checks whether the Product is active.
* If Jetpack plugin is active, then Extras will be inactive.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active();
}
/**
* Checks whether the plugin is installed
* If Jetpack plugin is installed, then Extras will be inactive.
*
* @return boolean
*/
public static function is_plugin_installed() {
return static::is_jetpack_plugin_installed();
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack' );
}
/**
* Activates the Jetpack plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin() {
return activate_plugin( static::get_installed_plugin_filename( 'jetpack' ) );
}
}
@@ -0,0 +1,227 @@
<?php
/**
* Growth plan
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Growth plan
*/
class Growth extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'growth';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'growth';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Growth Bundle';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Growth';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Grow and track your audience effortlessly.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_long_description() {
return __( 'Essential tools to help you grow your audience, track visitor engagement, and turn leads into loyal customers and advocates.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized feature list
*
* @return array Growth features list
*/
public static function get_features() {
return array(
_x( 'Jetpack Social', 'Growth Product Feature', 'jetpack-my-jetpack' ),
_x( 'Jetpack Stats (10K site views, upgradeable)', 'Growth Product Feature', 'jetpack-my-jetpack' ),
_x( 'Unlimited subscriber imports', 'Growth Product Feature', 'jetpack-my-jetpack' ),
_x( 'Earn more from your content', 'Growth Product Feature', 'jetpack-my-jetpack' ),
_x( 'Accept payments with PayPal', 'Growth Product Feature', 'jetpack-my-jetpack' ),
_x( 'Increase earnings with WordAds', 'Growth Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
$product_slug = static::get_wpcom_product_slug();
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => $product_slug,
),
Wpcom_Products::get_product_pricing( $product_slug )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_growth_yearly';
}
/**
* Checks whether the Jetpack module is active
*
* This is a bundle and not a product. We should not use this information for anything
*
* @return bool
*/
public static function is_module_active() {
return false;
}
/**
* Activates the product by installing and activating its plugin
*
* @param WP_Error|bool $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return bool|\WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
// A bundle is not a module. There's nothing in the plugin to be activated, so it's ok to fail to activate the module.
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
return $product_activation;
}
// At this point, Jetpack plugin is installed. Let's activate each individual product.
$activation = Social::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Stats::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
return $activation;
}
/**
* Checks whether the Product is active
*
* Growth is a bundle and not a module. Activation takes place on WPCOM. So lets consider it active if jetpack is active and has the plan.
*
* @return bool
*/
public static function is_active() {
return static::is_jetpack_plugin_active() && static::has_required_plan();
}
/**
* Checks whether the current plan (or purchase) of the site already supports the product
*
* @return bool
*/
public static function has_required_plan() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if (
str_starts_with( $purchase->product_slug, 'jetpack_growth' ) ||
str_starts_with( $purchase->product_slug, 'jetpack_complete' )
) {
return true;
}
}
}
return false;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_growth_yearly',
'jetpack_growth_monthly',
'jetpack_growth_bi_yearly',
);
}
/**
* Checks whether the product is a bundle
*
* @return bool
*/
public static function is_bundle_product() {
return true;
}
/**
* Returns all products it contains.
*
* @return array Product slugs
*/
public static function get_supported_products() {
return array( 'social', 'stats' );
}
/**
* Get the URL where the user manages the product
*
* @return string
*/
public static function get_manage_url() {
return '';
}
}
@@ -0,0 +1,184 @@
<?php
/**
* Base product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Modules;
use Automattic\Jetpack\Plugins_Installer;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the hybrid products
*
* Hybrid products are those that may work both as a stand-alone plugin or with the Jetpack plugin.
*/
abstract class Hybrid_Product extends Product {
/**
* All hybrid products have a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* For Hybrid products, we can use either the standalone or Jetpack plugin
*
* @return bool
*/
public static function is_plugin_installed() {
return parent::is_plugin_installed() || parent::is_jetpack_plugin_installed();
}
/**
* Checks whether the Product is active
*
* @return boolean
*/
public static function is_plugin_active() {
return parent::is_plugin_active() || parent::is_jetpack_plugin_active();
}
/**
* Checks whether the standalone plugin for this product is active
*
* @return boolean
*/
public static function is_standalone_plugin_active() {
return parent::is_plugin_active();
}
/**
* Checks whether the Product is active
*
* @return boolean
*/
public static function is_active() {
return parent::is_active() && static::is_module_active();
}
/**
* Activates the plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin() {
/*
* Activate self-installed plugin if it's installed.
*/
if ( parent::is_plugin_installed() ) {
return activate_plugin( static::get_installed_plugin_filename() );
}
/*
* Otherwise, activate Jetpack plugin.
*/
if ( static::is_jetpack_plugin_installed() ) {
return activate_plugin( static::get_installed_plugin_filename( 'jetpack' ) );
}
return new WP_Error( 'plugin_not_found', __( 'Activation failed. Plugin is not installed', 'jetpack-my-jetpack' ) );
}
/**
* Activates the product. If the Hybrid product has declared a jetpack module name, let's try to activate it if Jetpack plugin is active
*
* @param bool|WP_Error $product_activation Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return bool|WP_Error
*/
public static function do_product_specific_activation( $product_activation ) {
if ( is_wp_error( $product_activation ) ) {
// If we failed to install the stand-alone plugin because the package was not found, let's try and install Jetpack plugin instead.
// This might happen, for example, while the stand-alone plugin was not released to the WP.org repository yet.
if ( 'no_package' === $product_activation->get_error_code() ) {
$product_activation = Plugins_Installer::install_plugin( self::JETPACK_PLUGIN_SLUG );
if ( ! is_wp_error( $product_activation ) ) {
$product_activation = static::activate_plugin();
}
}
if ( is_wp_error( $product_activation ) ) {
return $product_activation;
}
}
if ( ! empty( static::$module_name ) ) {
// Only activate the module if the plan supports it
// We don't want to throw an error for a missing plan here since we try activation before purchase
if ( static::$requires_plan && ! static::has_any_plan_for_product() ) {
return true;
}
$module_activation = ( new Modules() )->activate( static::$module_name, false, false );
if ( ! $module_activation ) {
return new WP_Error( 'module_activation_failed', __( 'Error activating Jetpack module', 'jetpack-my-jetpack' ) );
}
return $module_activation;
}
return true;
}
/**
* Deactivates the product.
*
* In addition to the parent's plugin deactivation, also deactivates the
* matching Jetpack module when `static::$module_name` is set and the
* module is currently active otherwise Hybrid products activated via
* the Jetpack-module path stay half-on after "deactivation".
*
* @return bool|WP_Error True on success (matches Product::deactivate()),
* WP_Error if the module deactivation failed.
*/
public static function deactivate() {
$result = parent::deactivate();
$modules = new Modules();
if ( ! empty( static::$module_name ) && $modules->is_active( static::$module_name ) ) {
if ( ! $modules->deactivate( static::$module_name ) ) {
return new WP_Error(
'module_deactivation_failed',
__( 'Error deactivating Jetpack module', 'jetpack-my-jetpack' )
);
}
}
return $result;
}
/**
* Install and activate the standalone plugin in the case it's missing.
*
* @return boolean|WP_Error
*/
public static function install_and_activate_standalone() {
$result = parent::install_and_activate_standalone();
if ( is_wp_error( $result ) ) {
return $result;
}
/**
* Activate the module as well, if the user has a plan
* or the product does not require a plan to work
*/
if ( static::has_any_plan_for_product() && isset( static::$module_name ) ) {
$module_activation = ( new Modules() )->activate( static::$module_name, false, false );
if ( ! $module_activation ) {
return new WP_Error( 'module_activation_failed', __( 'Error activating Jetpack module', 'jetpack-my-jetpack' ) );
}
}
return true;
}
}
@@ -0,0 +1,681 @@
<?php
/**
* AI product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\My_Jetpack\Initializer;
use Automattic\Jetpack\My_Jetpack\Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Post;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Jetpack AI product
*/
class Jetpack_Ai extends Product {
const CURRENT_TIER_SLUG = 'free';
const UPGRADED_TIER_SLUG = 'upgraded';
/**
* The product slug
*
* @var string
*/
public static $slug = 'jetpack-ai';
/**
* The category of the product
*
* @var string
*/
public static $category = 'create';
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'ai-assistant';
/**
* Get the plugin slug - ovewrite it and return Jetpack's
*
* @return ?string
*/
public static function get_plugin_slug() {
return self::JETPACK_PLUGIN_SLUG;
}
/**
* Get the plugin filename - ovewrite it and return Jetpack's
*
* @return ?string
*/
public static function get_plugin_filename() {
return self::JETPACK_PLUGIN_FILENAME;
}
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'AI';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack AI';
}
/**
* Get the product's available tiers
*
* @return string[] Slugs of the available tiers
*/
public static function get_tiers() {
if ( ! self::are_tier_plans_enabled() ) {
return parent::get_tiers();
}
return array(
self::UPGRADED_TIER_SLUG,
self::CURRENT_TIER_SLUG,
);
}
/**
* Get the internationalized comparison of free vs upgraded features
*
* @return array[] Protect features comparison
*/
public static function get_features_by_tier() {
if ( ! self::are_tier_plans_enabled() ) {
return parent::get_features_by_tier();
}
$current_tier = self::get_current_usage_tier();
$current_description = 0 === $current_tier
? __( 'Up to 20 requests', 'jetpack-my-jetpack' )
/* translators: number of requests */
: sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $current_tier );
$next_tier = self::get_next_usage_tier();
$next_description = $next_tier === null
? __( 'Let\'s get in touch', 'jetpack-my-jetpack' )
/* translators: number of requests */
: sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $next_tier );
return array(
array(
'name' => __( 'Number of requests', 'jetpack-my-jetpack' ),
'info' => array(
'title' => __( 'Requests', 'jetpack-my-jetpack' ),
'content' => __( 'Increase your monthly request limit. Upgrade now and have the option to further increase your requests with additional upgrades.', 'jetpack-my-jetpack' ),
),
'tiers' => array(
self::CURRENT_TIER_SLUG => array(
'included' => true,
'description' => $current_description,
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => $next_description,
),
),
),
array(
'name' => __( 'Generate and edit content', 'jetpack-my-jetpack' ),
'tiers' => array(
self::CURRENT_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Build forms from prompts', 'jetpack-my-jetpack' ),
'tiers' => array(
self::CURRENT_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Get feedback on posts', 'jetpack-my-jetpack' ),
'tiers' => array(
self::CURRENT_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Generate featured images', 'jetpack-my-jetpack' ),
'tiers' => array(
self::CURRENT_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
);
}
/**
* Get the current usage tier
*
* @return int
*/
public static function get_current_usage_tier() {
if ( ! self::is_site_connected() ) {
return 0;
}
$info = self::get_ai_assistant_feature();
// Bail early if it's not possible to fetch the feature data.
if ( is_wp_error( $info ) ) {
return 0;
}
$current_tier = $info['current-tier']['value'] ?? null;
return $current_tier;
}
/**
* Get the next usage tier
*
* @return int
*/
public static function get_next_usage_tier() {
if ( ! self::is_site_connected() || ! self::has_paid_plan_for_product() ) {
// without site connection we can't know if tiers are enabled or not,
// hence we can't know if the next tier is 100 or 1 (unlimited).
return 100;
}
$info = self::get_ai_assistant_feature();
// Bail early if it's not possible to fetch the feature data or if it's included in a plan.
if ( is_wp_error( $info ) || empty( $info ) ) {
return null;
}
// Trust the next tier provided by the feature data.
$next_tier = $info['next-tier']['value'] ?? null;
return $next_tier;
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Turn your ideas into readytopublish content at lightspeed.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized usage tier long description by tier
*
* @param int|null $tier The usage tier.
* @return string
*/
public static function get_long_description_by_usage_tier( $tier ) {
switch ( (int) $tier ) {
case 1:
return __( 'Jetpack AI Assistant brings the power of AI right into your WordPress editor, letting your content creation soar to new heights.', 'jetpack-my-jetpack' );
case 100:
return __( 'The most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' );
default:
return __( 'Upgrade and increase the amount of your available monthly requests to continue using the most advanced AI technology Jetpack has to offer.', 'jetpack-my-jetpack' );
}
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
$next_tier = self::get_next_usage_tier();
return self::get_long_description_by_usage_tier( $next_tier );
}
/**
* Get the internationalized usage tier features by tier
*
* @param int $tier The usage tier.
* @return string
*/
public static function get_features_by_usage_tier( $tier ) {
$is_tier_plan = $tier && intval( $tier ) > 1;
if ( $tier === 100 && ( ! self::is_site_connected() || ! self::has_paid_plan_for_product() ) ) {
// in these cases, get_next_usage_tier() will return 100
// 100 is fine as default when tiered plans are enabled, but not otherwise
$is_tier_plan = false;
}
$features = array(
__( 'High request capacity *', 'jetpack-my-jetpack' ),
__( 'Generate text, tables, lists, and forms', 'jetpack-my-jetpack' ),
__( 'Easily refine content to your liking', 'jetpack-my-jetpack' ),
__( 'Make your content easier to read', 'jetpack-my-jetpack' ),
__( 'Generate images with one-click', 'jetpack-my-jetpack' ),
__( 'Optimize your titles for better performance', 'jetpack-my-jetpack' ),
__( 'Priority support', 'jetpack-my-jetpack' ),
);
$tiered_features = array(
__( 'Prompt based content generation', 'jetpack-my-jetpack' ),
__( 'Generate text, tables, and lists', 'jetpack-my-jetpack' ),
__( 'Adaptive tone adjustment', 'jetpack-my-jetpack' ),
__( 'Superior spelling and grammar correction', 'jetpack-my-jetpack' ),
__( 'Title & summary generation', 'jetpack-my-jetpack' ),
__( 'Priority support', 'jetpack-my-jetpack' ),
/* translators: %d is the number of requests. */
sprintf( __( 'Up to %d requests per month', 'jetpack-my-jetpack' ), $tier ),
);
return $is_tier_plan ? $tiered_features : $features;
}
/**
* Get the internationalized features list
*
* @return array Jetpack AI features list
*/
public static function get_features() {
$next_tier = self::get_next_usage_tier();
return self::get_features_by_usage_tier( $next_tier );
}
/**
* Get the product pricing details by tier
*
* @param int|null $tier The usage tier.
* @return array Pricing details
*/
public static function get_pricing_for_ui_by_usage_tier( $tier ) {
if ( $tier === null ) {
return array();
}
$product = Wpcom_Products::get_product( static::get_wpcom_product_slug() );
if ( empty( $product ) ) {
return array();
}
$tier_plans_enabled = self::are_tier_plans_enabled();
/*
* when tiers are enabled and the price tier list is empty,
* we may need to renew the cache for the product data so
* we get the new price tier list.
*
* if the list is still empty after the fresh data, we will
* default to empty pricing (by returning an empty array).
*/
if ( empty( $product->price_tier_list ) && $tier_plans_enabled ) {
$product = Wpcom_Products::get_product( static::get_wpcom_product_slug(), true );
}
// get the base pricing for the unlimited plan, for compatibility
$base_pricing = Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() );
$price_tier_list = $product->price_tier_list;
$yearly_prices = array();
foreach ( $price_tier_list as $price_tier ) {
if ( isset( $price_tier->maximum_units ) && isset( $price_tier->maximum_price ) ) {
// The prices are in cents
$yearly_prices[ $price_tier->maximum_units ] = $price_tier->maximum_price / 100;
}
}
// add the base pricing to the list
$prices = array( 1 => $base_pricing );
foreach ( $yearly_prices as $units => $price ) {
$prices[ $units ] = array_merge(
$base_pricing,
array(
'full_price' => $price,
'discount_price' => $price,
'is_introductory_offer' => false,
'introductory_offer' => null,
)
);
}
return $prices[ $tier ] ?? array();
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
// no tiers
if ( ! self::are_tier_plans_enabled() ) {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
// hardcoding 1 as next tier if tiers are not enabled
self::get_pricing_for_ui_by_usage_tier( 1 )
);
}
$next_tier = self::get_next_usage_tier();
$current_tier = self::get_current_usage_tier();
$current_call_to_action = $current_tier === 0
? __( 'Continue for free', 'jetpack-my-jetpack' )
: __( 'I\'m fine with my plan, thanks', 'jetpack-my-jetpack' );
$next_call_to_action = $next_tier === null
? __( 'Contact Us', 'jetpack-my-jetpack' )
: __( 'Upgrade', 'jetpack-my-jetpack' );
return array(
'tiers' => array(
self::CURRENT_TIER_SLUG => array_merge(
self::get_pricing_for_ui_by_usage_tier( $current_tier ),
array(
'available' => true,
'is_free' => true,
'call_to_action' => $current_call_to_action,
)
),
self::UPGRADED_TIER_SLUG => array_merge(
self::get_pricing_for_ui_by_usage_tier( $next_tier ),
array(
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'quantity' => $next_tier,
'call_to_action' => $next_call_to_action,
)
),
),
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_ai_yearly';
}
/**
* Get the WPCOM monthly product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_monthly_product_slug() {
return 'jetpack_ai_monthly';
}
/**
* Get the WPCOM bi-yearly product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_bi_yearly_product_slug() {
return 'jetpack_ai_bi_yearly';
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_ai_yearly',
'jetpack_ai_monthly',
'jetpack_ai_bi_yearly',
);
}
/**
* Checks whether the product can be upgraded to a different product.
*
* @return boolean
*/
public static function is_upgradable() {
$has_ai_feature = static::does_site_have_feature( 'ai-assistant' );
$tier_plans_enabled = self::are_tier_plans_enabled();
$current_tier = self::get_current_usage_tier();
if ( $has_ai_feature && ! $tier_plans_enabled && $current_tier >= 1 ) {
return false;
}
$next_tier = self::get_next_usage_tier();
// The check below is debatable, not having the feature should not flag as not upgradable.
// If user is free (tier = 0), not unlimited (tier = 1) and has a next tier, then it's upgradable.
if ( $current_tier !== null && $current_tier !== 1 && $next_tier ) {
return true;
}
// Mark as not upgradable if user is on unlimited tier or does not have any plan.
if ( ! $has_ai_feature || null === $current_tier || 1 === $current_tier ) {
return false;
}
return true;
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the URL the user is taken after activating the product through the checkout
*
* @return ?string
*/
public static function get_post_activation_url() {
return self::get_manage_url();
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=my-jetpack#/jetpack-ai' );
}
/**
* Checks whether the plugin is installed
*
* @return boolean
*/
public static function is_plugin_installed() {
return self::is_jetpack_plugin_installed();
}
/**
* Checks whether the plugin is active
*
* @return boolean
*/
public static function is_plugin_active() {
return (bool) static::is_jetpack_plugin_active();
}
/**
* Checks whether the Product is active
*
* Overrides the parent method to respect the jetpack_ai_enabled filter.
*
* @return boolean
*/
public static function is_active() {
/**
* Filter to enable/disable Jetpack AI.
*
* @since 5.28.3
*
* @param boolean $enabled True if Jetpack AI should be enabled, false otherwise. Default true.
*/
$is_enabled = apply_filters( 'jetpack_ai_enabled', true );
return $is_enabled && parent::is_active();
}
/**
* Get data about the AI Assistant feature
*
* @return array
*/
public static function get_ai_assistant_feature() {
// Bail early if the plugin is not active.
if ( ! self::is_jetpack_plugin_installed() ) {
return array();
}
// Check if the global constant is defined.
if ( ! defined( 'JETPACK__PLUGIN_DIR' ) ) {
return array();
}
// Bail early if the site is not connected.
if ( ! self::is_site_connected() ) {
return array();
}
// Check if class exists. If not, try to require it once.
if ( ! class_exists( 'Jetpack_AI_Helper' ) ) {
$class_file_path = JETPACK__PLUGIN_DIR . '_inc/lib/class-jetpack-ai-helper.php';
// Check whether the file exists
if ( ! file_exists( $class_file_path ) ) {
return array();
}
require_once $class_file_path;
}
return \Jetpack_AI_Helper::get_ai_assistance_feature();
}
/**
* Get the AI Assistant tiered plans status
*
* @return boolean
*/
public static function are_tier_plans_enabled() {
$info = self::get_ai_assistant_feature();
if ( is_wp_error( $info ) ) {
// this is another faulty default value, we'll assume disabled while
// production is enabled
return false;
}
if ( ! empty( $info ) && isset( $info['tier-plans-enabled'] ) ) {
return boolval( $info['tier-plans-enabled'] );
}
return false;
}
/**
* Checks whether the site is connected to WordPress.com.
*
* @return boolean
*/
private static function is_site_connected() {
return ( new Connection_Manager() )->is_connected();
}
/**
* Get the URL where the user manages the product
*
* NOTE: this method is the only thing that resembles an initialization for the product.
*
* @return void
*/
public static function extend_plugin_action_links() {
add_action( 'myjetpack_enqueue_scripts', array( static::class, 'admin_enqueue_scripts' ) );
add_filter( 'default_content', array( static::class, 'add_ai_block' ), 10, 2 );
}
/**
* Enqueue the AI Assistant script
*
* The script is just a global variable used for the nonce, needed for the create post link.
*
* @return void
*/
public static function admin_enqueue_scripts() {
wp_register_script(
'my_jetpack_ai_app',
false,
array(),
Initializer::PACKAGE_VERSION,
array( 'in_footer' => true )
);
wp_localize_script(
'my_jetpack_ai_app',
'jetpackAi',
array(
'nonce' => wp_create_nonce( 'ai-assistant-content-nonce' ),
)
);
wp_enqueue_script( 'my_jetpack_ai_app' );
}
/**
* Add AI block to the post content
*
* Used only from the link on the product page, the filter will insert an AI Assistant block in the post content.
*
* @param string $content The post content.
* @param WP_Post $post The post object.
* @return string
*/
public static function add_ai_block( $content, $post ) {
if ( isset( $_GET['use_ai_block'] ) && isset( $_GET['_wpnonce'] )
&& wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'ai-assistant-content-nonce' )
&& ! empty( $post )
&& ! is_wp_error( $post )
&& current_user_can( 'edit_post', $post->ID )
&& '' === $content
) {
return '<!-- wp:jetpack/ai-assistant /-->';
}
return $content;
}
}
@@ -0,0 +1,195 @@
<?php
/**
* Forms product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Jetpack Forms product.
*
* Forms is a feature available as part of the Jetpack plugin, backed by the
* `contact-form` module.
*/
class Jetpack_Forms extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'jetpack-forms';
/**
* The slug of the plugin associated with this product.
* Forms is a feature available as part of the Jetpack plugin.
*
* @var string
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* The Plugin file associated with Forms.
*
* @var string|null
*/
public static $plugin_filename = self::JETPACK_PLUGIN_FILENAME;
/**
* The Jetpack module name associated with this product.
*
* @var string|null
*/
public static $module_name = 'contact-form';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* Whether this module is a Jetpack feature
*
* @var boolean
*/
public static $is_feature = true;
/**
* Whether this product requires a user connection
*
* @var boolean
*/
public static $requires_user_connection = false;
/**
* Whether this product has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Whether the product requires a plan to run.
* The plan could be paid or free.
*
* @var bool
*/
public static $requires_plan = false;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Forms';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Forms';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Build and share forms to collect leads, feedback, and payments.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Create contact forms, surveys, and registration forms in minutes — then manage every response right from your dashboard, no third-party tools required.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized feature list
*
* @return array Forms features list
*/
public static function get_features() {
return array(
__( 'Spam protection with Akismet', 'jetpack-my-jetpack' ),
__( 'Export your data anytime', 'jetpack-my-jetpack' ),
__( 'Manage all your responses in one place', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'available' => true,
'is_free' => true,
);
}
/**
* Checks whether the plugin is installed
*
* @return boolean
*/
public static function is_plugin_installed() {
return static::is_jetpack_plugin_installed();
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
// Defer to the Forms package for the canonical admin URL when it's available
// (it accounts for the responses dashboard variant and the admin URL filter).
$dashboard = 'Automattic\Jetpack\Forms\Dashboard\Dashboard';
if ( method_exists( $dashboard, 'get_forms_admin_url' ) ) {
return $dashboard::get_forms_admin_url();
}
return admin_url( 'admin.php?page=jetpack-forms-admin' );
}
/**
* Activates the Jetpack plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin(): ?WP_Error {
$plugin_filename = static::get_installed_plugin_filename( self::JETPACK_PLUGIN_SLUG );
if ( $plugin_filename ) {
return activate_plugin( $plugin_filename );
}
}
}
@@ -0,0 +1,168 @@
<?php
/**
* Base Module product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Jetpack;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Module products
*
* Module products are those that are a Jetpack module behind the scenes.
*
* They require Jetpack plugin and will then activate/deactivate a module.
*/
abstract class Module_Product extends Product {
/**
* The Jetpack module name associated with this product
*
* @var string|null
*/
public static $module_name = null;
/**
* Get the plugin slug - ovewrite it ans return Jetpack's
*
* @return ?string
*/
public static function get_plugin_slug() {
return self::JETPACK_PLUGIN_SLUG;
}
/**
* Get the plugin filename - ovewrite it ans return Jetpack's
*
* @return ?string
*/
public static function get_plugin_filename() {
return self::JETPACK_PLUGIN_FILENAME;
}
/**
* Ensure that child classes define $module_name attribute
*
* @throws \Exception If required attribute is not declared in the child class.
* @return void
*/
private static function check_for_module_name() {
if ( empty( static::$module_name ) ) {
throw new \Exception( 'Module Product classes must declare the $module_name attribute.' );
}
}
/**
* Checks whether the Product is active
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active() && static::is_module_active();
}
/**
* Checks whether the Jetpack module is active
*
* @return bool
*/
public static function is_module_active() {
self::check_for_module_name();
if ( ! class_exists( 'Jetpack' ) ) {
return false;
}
return Jetpack::is_module_active( static::$module_name );
}
/**
* Get the product status.
* We don't use parent::get_status() to avoid complexity.
*
* @return string Product status.
*/
private static function get_feature_status() {
if ( ! static::is_plugin_installed() ) {
return Products::STATUS_PLUGIN_ABSENT;
}
if ( ! static::is_plugin_active() ) {
return Products::STATUS_INACTIVE;
}
if ( static::$requires_user_connection && ! ( new Connection_Manager() )->has_connected_owner() ) {
return Products::STATUS_USER_CONNECTION_ERROR;
}
if ( ! static::is_module_active() ) {
return Products::STATUS_MODULE_DISABLED;
}
return Products::STATUS_ACTIVE;
}
/**
* Gets the current status of the product
*
* @return string
*/
public static function get_status() {
if ( static::$is_feature ) {
return static::get_feature_status();
}
$status = parent::get_status();
if ( Products::STATUS_INACTIVE === $status && ! static::is_module_active() ) {
$status = Products::STATUS_MODULE_DISABLED;
}
return $status;
}
/**
* Activates the product by installing and activating its plugin
*
* @param bool|WP_Error $plugin_activation Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return boolean|\WP_Error
*/
public static function do_product_specific_activation( $plugin_activation ) {
self::check_for_module_name();
if ( is_wp_error( $plugin_activation ) ) {
return $plugin_activation;
}
if ( ! class_exists( 'Jetpack' ) ) {
return new WP_Error( 'plugin_activation_failed', __( 'Error activating Jetpack plugin', 'jetpack-my-jetpack' ) );
}
$module_activation = Jetpack::activate_module( static::$module_name, false, false );
if ( ! $module_activation ) {
return new WP_Error( 'module_activation_failed', __( 'Error activating Jetpack module', 'jetpack-my-jetpack' ) );
}
return $module_activation;
}
/**
* Deactivate the module
*
* @return boolean
*/
public static function deactivate() {
self::check_for_module_name();
if ( ! class_exists( 'Jetpack' ) ) {
return true;
}
return Jetpack::deactivate_module( static::$module_name );
}
}
@@ -0,0 +1,190 @@
<?php
/**
* Feature: Newsletter
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Newsletter module.
*/
class Newsletter extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'newsletter';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* The slug of the plugin associated with this product.
* Newsletter is a feature available as part of the Jetpack plugin.
*
* @var string
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* The Plugin file associated with stats
*
* @var string|null
*/
public static $plugin_filename = self::JETPACK_PLUGIN_FILENAME;
/**
* The Jetpack module name associated with this product
*
* @var string|null
*/
public static $module_name = 'subscriptions';
/**
* Whether this module is a Jetpack feature
*
* @var boolean
*/
public static $is_feature = true;
/**
* Whether this product requires a user connection
*
* @var boolean
*/
public static $requires_user_connection = true;
/**
* Whether this product has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Whether the product requires a plan to run
* The plan could be paid or free
*
* @var bool
*/
public static $requires_plan = false;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Newsletter';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Newsletter';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Draw your readers from one post to another', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Draw your readers from one post to another, increasing overall traffic on your site', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized feature list
*
* @return array Newsletter features list
*/
public static function get_features() {
return array();
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'available' => true,
'is_free' => true,
);
}
/**
* Checks whether the Product is active.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active();
}
/**
* Checks whether the plugin is installed
*
* @return boolean
*/
public static function is_plugin_installed() {
return static::is_jetpack_plugin_installed();
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack-newsletter' );
}
/**
* Activates the Jetpack plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin(): ?WP_Error {
$plugin_filename = static::get_installed_plugin_filename( self::JETPACK_PLUGIN_SLUG );
if ( $plugin_filename ) {
return activate_plugin( $plugin_filename );
}
}
}
@@ -0,0 +1,466 @@
<?php
/**
* Protect product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Protect_Status\Status as Protect_Status;
use Automattic\Jetpack\Redirect;
use Automattic\Jetpack\Waf\Waf_Runner;
use WP_Error;
use WP_REST_Response;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Protect product
*/
class Protect extends Hybrid_Product {
const FREE_TIER_SLUG = 'free';
const UPGRADED_TIER_SLUG = 'upgraded';
const UPGRADED_TIER_PRODUCT_SLUG = 'jetpack_scan';
const SCAN_FEATURE_SLUG = 'scan';
const FIREWALL_FEATURE_SLUG = 'firewall';
/**
* The product slug
*
* @var string
*/
public static $slug = 'protect';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'protect';
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-protect/jetpack-protect.php',
'protect/jetpack-protect.php',
'jetpack-protect-dev/jetpack-protect.php',
);
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
public static $plugin_slug = 'jetpack-protect';
/**
* The category of the product
*
* @var string
*/
public static $category = 'security';
/**
* Defines whether or not to show a product interstitial as tiered pricing or not
*
* @var bool
*/
public static $is_tiered_pricing = true;
/**
* Whether this product requires a user connection
*
* @var string
*/
public static $requires_user_connection = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Protect has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'scan';
/**
* Setup Protect REST API endpoints
*
* @return void
*/
public static function register_endpoints(): void {
parent::register_endpoints();
// Get Jetpack Protect data.
register_rest_route(
'my-jetpack/v1',
'/site/protect/data',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_protect_data',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Checks if the user has the correct permissions
*/
public static function permissions_callback() {
return current_user_can( 'edit_posts' );
}
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Protect';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Protect';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Guard against malware and bad actors 24/7', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Protect your site from bad actors and malware 24/7. Clean up security vulnerabilities with one click.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Protect features list
*/
public static function get_features() {
return array(
__( 'Over 20,000 listed vulnerabilities', 'jetpack-my-jetpack' ),
__( 'Daily automatic scans', 'jetpack-my-jetpack' ),
__( 'Check plugin and theme version status', 'jetpack-my-jetpack' ),
__( 'Easy to navigate and use', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product's available tiers
*
* @return string[] Slugs of the available tiers
*/
public static function get_tiers() {
return array(
self::UPGRADED_TIER_SLUG,
self::FREE_TIER_SLUG,
);
}
/**
* Get the internationalized comparison of free vs upgraded features
*
* @return array[] Protect features comparison
*/
public static function get_features_by_tier() {
return array(
array(
'name' => __( 'Scan for threats and vulnerabilities', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => true,
'description' => __( 'Check items against database', 'jetpack-my-jetpack' ),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Line by line malware scanning', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Daily automated scans', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Plus on-demand manual scans', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Web Application Firewall', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array(
'included' => false,
'description' => __( 'Manual rules only', 'jetpack-my-jetpack' ),
),
self::UPGRADED_TIER_SLUG => array(
'included' => true,
'description' => __( 'Automatic protection and rule updates', 'jetpack-my-jetpack' ),
),
),
),
array(
'name' => __( 'Brute force protection', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Account protection', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => true ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Access to scan on Cloud', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'One-click auto fixes', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Notifications', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
array(
'name' => __( 'Severity labels', 'jetpack-my-jetpack' ),
'tiers' => array(
self::FREE_TIER_SLUG => array( 'included' => false ),
self::UPGRADED_TIER_SLUG => array( 'included' => true ),
),
),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'tiers' => array(
self::FREE_TIER_SLUG => array(
'available' => true,
'is_free' => true,
),
self::UPGRADED_TIER_SLUG => array_merge(
array(
'available' => true,
'wpcom_product_slug' => self::UPGRADED_TIER_PRODUCT_SLUG,
),
Wpcom_Products::get_product_pricing( self::UPGRADED_TIER_PRODUCT_SLUG )
),
),
);
}
/**
* Determines whether the module/plugin/product needs the users attention.
* Typically due to some sort of error where user troubleshooting is needed.
*
* @return boolean|array
*/
public static function does_module_need_attention() {
$protect_threat_status = false;
$scan_data = Protect_Status::get_status();
// Check if there are scan threats.
$protect_data = $scan_data;
if ( is_wp_error( $protect_data ) ) {
return $protect_threat_status; // false
}
$critical_threat_count = false;
if ( ! empty( $protect_data->threats ) ) {
$critical_threat_count = array_reduce(
$protect_data->threats,
function ( $accum, $threat ) {
return $threat->severity >= 5 ? ++$accum : $accum;
},
0
);
$protect_threat_status = array(
'type' => $critical_threat_count ? 'error' : 'warning',
'data' => array(
'threat_count' => count( $protect_data->threats ),
'critical_threat_count' => $critical_threat_count,
'fixable_threat_ids' => $protect_data->fixable_threat_ids,
),
);
}
return $protect_threat_status;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_scan',
'jetpack_scan_monthly',
'jetpack_scan_bi_yearly',
);
}
/**
* Checks whether the product can be upgraded - i.e. this shows the /#add-protect interstitial
*
* @return boolean
*/
public static function is_upgradable() {
return ! self::has_paid_plan_for_product();
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the URL the user is taken after purchasing the product through the checkout for each product feature
*
* @return ?array
*/
public static function get_post_checkout_urls_by_feature() {
return array(
self::SCAN_FEATURE_SLUG => self::get_post_checkout_url(),
self::FIREWALL_FEATURE_SLUG => admin_url( 'admin.php?page=jetpack-protect#/firewall' ),
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
if ( static::is_standalone_plugin_active() ) {
// Protect admin dashboard.
return admin_url( 'admin.php?page=jetpack-protect' );
}
if ( static::has_paid_plan_for_product() ) {
// Paid users without standalone plugin go to Jetpack Cloud Scan dashboard.
return Redirect::get_url( 'my-jetpack-manage-scan' );
}
// Free users without standalone plugin go to the Protect details page.
return admin_url( 'admin.php?page=my-jetpack#/protect-details' );
}
/**
* Get the URL where the user manages the product for each product feature
*
* @return ?array
*/
public static function get_manage_urls_by_feature() {
return array(
self::SCAN_FEATURE_SLUG => self::get_manage_url(),
self::FIREWALL_FEATURE_SLUG => admin_url( 'admin.php?page=jetpack-protect#/firewall' ),
);
}
/**
* Return product bundles list
* that supports the product.
*
* @return array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security', 'complete' );
}
/**
* Return site Jetpack Protect data for the REST API.
*
* @return WP_Rest_Response|WP_Error
*/
public static function get_site_protect_data() {
$scan_data = Protect_Status::get_status();
$waf_config = array();
$waf_supported = false;
$is_waf_enabled = false;
if ( class_exists( 'Automattic\Jetpack\Waf\Waf_Runner' ) ) {
// @phan-suppress-next-line PhanUndeclaredClassMethod
$waf_config = Waf_Runner::get_config();
// @phan-suppress-next-line PhanUndeclaredClassMethod
$is_waf_enabled = Waf_Runner::is_enabled();
// @phan-suppress-next-line PhanUndeclaredClassMethod
$waf_supported = Waf_Runner::is_supported_environment();
}
return rest_ensure_response(
array(
'scanData' => $scan_data,
'wafConfig' => array_merge(
$waf_config,
array(
'waf_supported' => $waf_supported,
'waf_enabled' => $is_waf_enabled,
),
array( 'blocked_logins' => (int) get_site_option( 'jetpack_protect_blocked_attempts', 0 ) )
),
)
);
}
}
@@ -0,0 +1,190 @@
<?php
/**
* Feature: Related Posts
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Related Posts module.
*/
class Related_Posts extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'related-posts';
/**
* The slug of the plugin associated with this product.
* Related Posts is a feature available as part of the Jetpack plugin.
*
* @var string
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* The Plugin file associated with stats
*
* @var string|null
*/
public static $plugin_filename = self::JETPACK_PLUGIN_FILENAME;
/**
* The Jetpack module name associated with this product
*
* @var string|null
*/
public static $module_name = 'related-posts';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* Whether this module is a Jetpack feature
*
* @var boolean
*/
public static $is_feature = true;
/**
* Whether this product requires a user connection
*
* @var boolean
*/
public static $requires_user_connection = false;
/**
* Whether this product has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Whether the product requires a plan to run
* The plan could be paid or free
*
* @var bool
*/
public static $requires_plan = false;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Related Posts';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Related Posts';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Draw your readers from one post to another', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Draw your readers from one post to another, increasing overall traffic on your site', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized feature list
*
* @return array Newsletter features list
*/
public static function get_features() {
return array();
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'available' => true,
'is_free' => true,
);
}
/**
* Checks whether the Product is active.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active();
}
/**
* Checks whether the plugin is installed
*
* @return boolean
*/
public static function is_plugin_installed() {
return static::is_jetpack_plugin_installed();
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack#/traffic?term=related%20posts' );
}
/**
* Activates the Jetpack plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin(): ?WP_Error {
$plugin_filename = static::get_installed_plugin_filename( self::JETPACK_PLUGIN_SLUG );
if ( $plugin_filename ) {
return activate_plugin( $plugin_filename );
}
}
}
@@ -0,0 +1,221 @@
<?php
/**
* Scan product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Redirect;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Scan product
*/
class Scan extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'scan';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'scan';
/**
* The category of the product
*
* @var string
*/
public static $category = 'security';
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'scan';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Scan';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Scan';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Aroundtheclock web application firewall, and automated malware scanning.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Automatic scanning and one-click fixes keep your site one step ahead of security threats and malware.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Scan features list
*/
public static function get_features() {
return array(
_x( 'Automated daily scanning', 'Scan Product Feature', 'jetpack-my-jetpack' ),
_x( 'One-click fixes for most issues', 'Scan Product Feature', 'jetpack-my-jetpack' ),
_x( 'Instant email notifications', 'Scan Product Feature', 'jetpack-my-jetpack' ),
_x( 'Access to latest Firewall rules', 'Scan Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_scan';
}
/**
* Checks whether the Product is active
*
* Scan is not actually a module. Activation takes place on WPCOM. So lets consider it active if jetpack is active and has the plan.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active();
}
/**
* Activates the product by installing and activating its plugin
*
* @param bool|WP_Error $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return boolean|\WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
// Scan is not a module. There's nothing in the plugin to be activated, so it's ok to fail to activate the module.
$product_activation = true;
}
return $product_activation;
}
/**
* Checks whether the Jetpack module is active
*
* Scan is not a module. Nothing needs to be active. Let's always consider it active.
*
* @return bool
*/
public static function is_module_active() {
return true;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_scan',
'jetpack_scan_monthly',
'jetpack_scan_bi_yearly',
);
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'security', 'complete' );
}
/**
* Get the URL the user is taken after activating the product
*
* @return ?string
*/
public static function get_post_activation_url() {
return ''; // stay in My Jetpack page.
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return Redirect::get_url( 'my-jetpack-manage-scan' );
}
/**
* Get the URL where the user should be redirected after checkout
*/
public static function get_post_checkout_url() {
if ( static::is_jetpack_plugin_active() ) {
return 'admin.php?page=jetpack#/recommendations';
}
// If Jetpack is not active, it means that the user has another standalone plugin active
// and it follows the `Protect` plugin flow instead of `Scan` so for now it would be safe
// to return null and let the user go back to the My Jetpack page.
return null;
}
}
@@ -0,0 +1,192 @@
<?php
/**
* Get search stats for use in the wp-admin dashboard.
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Client;
use Jetpack_Options;
/**
* Search stats (e.g. post count, post type breakdown)
*/
class Search_Stats {
const EXCLUDED_POST_TYPES = array(
'elementor_library', // Used by Elementor.
'jp_sitemap', // Used by Jetpack.
'revision',
'vip-legacy-redirect',
'scheduled-action',
'nbcs_video_lookup',
'reply', // bbpress, these get included in the topic
'product_variation', // woocommerce, not really public
'nav_menu_item',
'shop_order', // woocommerce, not really public
'redirect_rule', // Used by the Safe Redirect plugin.
);
const DO_NOT_EXCLUDE_POST_TYPES = array(
'topic', // bbpress
'forum', // bbpress
);
const CACHE_EXPIRY = 1 * MINUTE_IN_SECONDS;
const CACHE_GROUP = 'jetpack_search';
const POST_TYPE_BREAKDOWN_CACHE_KEY = 'post_type_break_down';
const TOTAL_POSTS_COUNT_CACHE_KEY = 'total-post-count';
const POST_COUNT_QUERY_LIMIT = 1e5;
/**
* Get stats from the WordPress.com API for the current blog ID.
*/
public function get_stats_from_wpcom() {
$blog_id = Jetpack_Options::get_option( 'id' );
if ( ! is_numeric( $blog_id ) ) {
return null;
}
$response = Client::wpcom_json_api_request_as_blog(
'/sites/' . (int) $blog_id . '/jetpack-search/stats',
'2',
array(),
null,
'wpcom'
);
return $response;
}
/**
* Queue querying the post type breakdown from WordPress.com API for the current blog ID.
*/
public function queue_post_count_query_from_wpcom() {
$blog_id = Jetpack_Options::get_option( 'id' );
if ( ! is_numeric( $blog_id ) ) {
return null;
}
Client::wpcom_json_api_request_as_blog(
'/sites/' . (int) $blog_id . '/jetpack-search/queue-post-count',
'2',
array(),
null,
'wpcom'
);
}
/**
* Estimate record counts via a local database query.
*/
public static function estimate_count() {
return array_sum( static::get_post_type_breakdown() );
}
/**
* Calculate breakdown of post types for the site.
*/
public static function get_post_type_breakdown() {
$indexable_post_types = get_post_types(
array(
'public' => true,
'exclude_from_search' => false,
)
);
$indexable_status_array = get_post_stati(
array(
'public' => true,
'exclude_from_search' => false,
)
);
$raw_posts_counts = static::get_raw_post_type_breakdown();
if ( ! $raw_posts_counts || is_wp_error( $raw_posts_counts ) ) {
return array();
}
$posts_counts = static::get_post_type_breakdown_with( $raw_posts_counts, $indexable_post_types, $indexable_status_array );
return $posts_counts;
}
/**
* Calculate breakdown of post types with passed in indexable post types and statuses.
* The function is going to be used from WPCOM as well for consistency.
*
* @param array $raw_posts_counts Array of post types with counts as value.
* @param array $indexable_post_types Array of indexable post types.
* @param array $indexable_status_array Array of indexable post statuses.
*/
public static function get_post_type_breakdown_with( $raw_posts_counts, $indexable_post_types, $indexable_status_array ) {
$posts_counts = array();
foreach ( $raw_posts_counts as $row ) {
// ignore if status is not public.
if ( ! in_array( $row['post_status'], $indexable_status_array, true ) ) {
continue;
}
// ignore if post type is in excluded post types.
if ( in_array( $row['post_type'], self::EXCLUDED_POST_TYPES, true ) ) {
continue;
}
// ignore if post type is not public and is not explicitly included.
if ( ! in_array( $row['post_type'], $indexable_post_types, true ) &&
! in_array( $row['post_type'], self::DO_NOT_EXCLUDE_POST_TYPES, true )
) {
continue;
}
// add up post type counts of potentially multiple post_status.
if ( ! isset( $posts_counts[ $row['post_type'] ] ) ) {
$posts_counts[ $row['post_type'] ] = 0;
}
$posts_counts[ $row['post_type'] ] += intval( $row['num_posts'] );
}
arsort( $posts_counts, SORT_NUMERIC );
return $posts_counts;
}
/**
* Get raw post type breakdown from the database or a remote request if posts count is high.
*/
protected static function get_raw_post_type_breakdown() {
global $wpdb;
$results = wp_cache_get( self::POST_TYPE_BREAKDOWN_CACHE_KEY, self::CACHE_GROUP );
if ( false !== $results ) {
return $results;
}
$total_posts_count = wp_cache_get( self::TOTAL_POSTS_COUNT_CACHE_KEY, self::CACHE_GROUP );
if ( false === $total_posts_count ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery */
$total_posts_counts = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts}" );
wp_cache_set( self::TOTAL_POSTS_COUNT_CACHE_KEY, $total_posts_counts, self::CACHE_GROUP, self::CACHE_EXPIRY );
}
// Get post type breakdown from a remote request if the post count is high
if ( $total_posts_count > self::POST_COUNT_QUERY_LIMIT ) {
$search_stats = new Search_Stats();
$search_stats->queue_post_count_query_from_wpcom();
$wpcom_stats = json_decode( wp_remote_retrieve_body( $search_stats->get_stats_from_wpcom() ), true );
if ( ! empty( $wpcom_stats['raw_post_type_breakdown'] ) ) {
$results = $wpcom_stats['raw_post_type_breakdown'];
wp_cache_set( self::POST_TYPE_BREAKDOWN_CACHE_KEY, $results, self::CACHE_GROUP, self::CACHE_EXPIRY );
return $results;
} else {
return array();
}
}
$query = "SELECT post_type, post_status, COUNT( * ) AS num_posts
FROM {$wpdb->posts}
WHERE post_password = ''
GROUP BY post_type, post_status";
// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared, WordPress.DB.DirectDatabaseQuery.DirectQuery
$results = $wpdb->get_results( $query, ARRAY_A );
wp_cache_set( self::POST_TYPE_BREAKDOWN_CACHE_KEY, $results, self::CACHE_GROUP, self::CACHE_EXPIRY );
return $results;
}
}
@@ -0,0 +1,414 @@
<?php
/**
* Search product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\Connection\Client;
use Automattic\Jetpack\Connection\Manager as Connection_Manager;
use Automattic\Jetpack\Constants;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Search\Module_Control as Search_Module_Control;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Search product
*/
class Search extends Hybrid_Product {
/**
* Fallback starting price (USD, billed yearly) for the entry record tier, used when
* the WPCOM pricing fetch fails so the dashboard still shows a price, not "$0".
*
* @var float
*/
const FALLBACK_STARTING_PRICE_USD = 100;
/**
* Search "new pricing" version identifier (introduced 202208).
*
* Intentionally duplicated from Automattic\Jetpack\Search\Plan::JETPACK_SEARCH_NEW_PRICING_VERSION
* rather than referencing that class. My Jetpack is bundled into standalone plugins (e.g. Jetpack
* Boost) that do NOT ship the jetpack-search package, so referencing Search\Plan here fatals with
* "Class not found" the moment this product builds its pricing data (introduced by PR #48892).
*
* @var string
*/
const SEARCH_NEW_PRICING_VERSION = '202208';
/**
* The product slug
*
* @var string
*/
public static $slug = 'search';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'search';
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
public static $plugin_slug = 'jetpack-search';
/**
* The category of the product
*
* @var string
*/
public static $category = 'performance';
/**
* Search has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Whether this product requires a plan to work at all
*
* @var bool
*/
public static $requires_plan = true;
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-search/jetpack-search.php',
'search/jetpack-search.php',
'jetpack-search-dev/jetpack-search.php',
);
/**
* Search only requires site connection
*
* @var boolean
*/
public static $requires_user_connection = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'search';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Search';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Search';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Instantly deliver the most relevant results to your visitors.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Help your site visitors find answers instantly so they keep reading and buying. Great for sites with a lot of content.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Boost features list
*/
public static function get_features() {
return array(
__( 'Instant search and indexing', 'jetpack-my-jetpack' ),
__( 'Powerful filtering', 'jetpack-my-jetpack' ),
__( 'Supports 38 languages', 'jetpack-my-jetpack' ),
__( 'Spelling correction', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
// Basic pricing info.
$pricing = array_merge(
array(
'available' => true,
'trial_available' => static::has_trial_support(),
'wpcom_product_slug' => static::get_wpcom_product_slug(),
'wpcom_free_product_slug' => static::get_wpcom_free_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
$record_count = intval( Search_Stats::estimate_count() );
$search_pricing = static::get_pricing_from_wpcom( $record_count );
if ( is_wp_error( $search_pricing ) ) {
// Default to the current pricing experience when the WPCOM fetch fails so the
// dashboard degrades to the production default, not the legacy single-card view.
$pricing['pricing_version'] = self::SEARCH_NEW_PRICING_VERSION;
// If the generic product pricing was also unavailable, fall back to a USD
// starting price so the pricing grid renders a price instead of "$0".
if ( empty( $pricing['full_price'] ) ) {
$pricing['currency_code'] = 'USD';
$pricing['full_price'] = self::FALLBACK_STARTING_PRICE_USD;
$pricing['discount_price'] = self::FALLBACK_STARTING_PRICE_USD;
}
return $pricing;
}
$pricing['estimated_record_count'] = $record_count;
return array_merge( $pricing, $search_pricing );
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_search';
}
/**
* Get the WPCOM free product slug
*
* @return ?string
*/
public static function get_wpcom_free_product_slug() {
return 'jetpack_search_free';
}
/**
* Returns true if the new_pricing_202208 is set to not empty in URL for testing purpose, or it's active.
*/
public static function is_new_pricing_202208() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
if ( isset( $_GET['new_pricing_202208'] ) && $_GET['new_pricing_202208'] ) {
return true;
}
$record_count = intval( Search_Stats::estimate_count() );
$search_pricing = static::get_pricing_from_wpcom( $record_count );
if ( is_wp_error( $search_pricing ) ) {
// Default to the current pricing experience when the WPCOM fetch fails.
return true;
}
return self::SEARCH_NEW_PRICING_VERSION === $search_pricing['pricing_version'];
}
/**
* Override status to `needs_activation` when status is `needs_plan`.
*/
public static function get_status() {
$status = parent::get_status();
return $status;
}
/**
* Use centralized Search pricing API.
*
* The function is also used by the search package, as a result it could be called before site connection - i.e. blog token might not be available.
*
* @param int $record_count Record count to estimate pricing.
*
* @return array|WP_Error
*/
public static function get_pricing_from_wpcom( $record_count ) {
static $pricings = array();
$connection = new Connection_Manager();
$blog_id = \Jetpack_Options::get_option( 'id' );
if ( isset( $pricings[ $record_count ] ) ) {
return $pricings[ $record_count ];
}
// If the site is connected, request pricing with the blog token
if ( $blog_id ) {
$endpoint = sprintf( '/jetpack-search/pricing?record_count=%1$d&locale=%2$s', $record_count, get_user_locale() );
// If available in the user data, set the user's currency as one of the params
if ( $connection->is_user_connected() ) {
$user_details = $connection->get_connected_user_data();
if ( ! empty( $user_details['user_currency'] ) && $user_details['user_currency'] !== 'USD' ) {
$endpoint .= sprintf( '&currency=%s', $user_details['user_currency'] );
}
}
$response = Client::wpcom_json_api_request_as_blog(
$endpoint,
'2',
array( 'timeout' => 5 ),
null,
'wpcom'
);
} else {
$response = wp_remote_get(
sprintf( Constants::get_constant( 'JETPACK__WPCOM_JSON_API_BASE' ) . '/wpcom/v2/jetpack-search/pricing?record_count=%1$d&locale=%2$s', $record_count, get_user_locale() ),
array( 'timeout' => 5 )
);
}
if ( is_wp_error( $response ) || 200 !== wp_remote_retrieve_response_code( $response ) ) {
// Cache the failure too: get_pricing_for_ui() reaches this twice per request
// (once via has_trial_support(), once directly), and each miss is a 5s timeout.
$pricings[ $record_count ] = new WP_Error( 'search_pricing_fetch_failed' );
return $pricings[ $record_count ];
}
$body = wp_remote_retrieve_body( $response );
$pricings[ $record_count ] = json_decode( $body, true );
return $pricings[ $record_count ];
}
/**
* Checks whether the product supports trial or not
*
* Returns true if it supports. Return false otherwise.
*
* Free products will always return false.
*
* @return boolean
*/
public static function has_trial_support() {
return static::is_new_pricing_202208();
}
/**
* Get the product-slugs of the paid plans for this product (not including bundles)
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_search',
'jetpack_search_monthly',
'jetpack_search_bi_yearly',
);
}
/**
* Checks if the site purchases contain a free search plan
*
* @return bool
*/
public static function has_free_plan_for_product() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( str_contains( $purchase->product_slug, 'jetpack_search_free' ) ) {
return true;
}
}
}
return false;
}
/**
* Activates the product. Try to enable instant search after the Search module was enabled.
*
* @param bool|WP_Error $product_activation Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return bool|WP_Error
*/
public static function do_product_specific_activation( $product_activation ) {
$product_activation = parent::do_product_specific_activation( $product_activation );
if ( is_wp_error( $product_activation ) ) {
return $product_activation;
}
if ( class_exists( 'Automattic\Jetpack\Search\Module_Control' ) ) {
( new Search_Module_Control() )->enable_instant_search();
}
// we don't want to change the success of the activation if we fail to activate instant search. That's not mandatory.
return $product_activation;
}
/**
* Get the URL the user is taken after activating the product
*
* @return ?string
*/
public static function get_post_activation_url() {
return ''; // stay in My Jetpack page or continue the purchase flow if needed.
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack-search' );
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'complete' );
}
}
@@ -0,0 +1,233 @@
<?php
/**
* Security product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Security product
*/
class Security extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'security';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'security';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Security Bundle';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Security';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Comprehensive site security, including VaultPress Backup, Scan, and Akismet Anti-spam.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Comprehensive site security, including VaultPress Backup, Scan, and Akismet Anti-spam.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Security features list
*/
public static function get_features() {
return array(
_x( 'Real-time cloud backups with 10GB storage', 'Security Product Feature', 'jetpack-my-jetpack' ),
_x( 'Automated real-time malware scan', 'Security Product Feature', 'jetpack-my-jetpack' ),
_x( 'One-click fixes for most threats', 'Security Product Feature', 'jetpack-my-jetpack' ),
_x( 'Comment & form spam protection', 'Security Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
$product_slug = static::get_wpcom_product_slug();
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => $product_slug,
),
Wpcom_Products::get_product_pricing( $product_slug )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_security_t1_yearly';
}
/**
* Checks whether the Jetpack module is active
*
* This is a bundle and not a product. We should not use this information for anything
*
* @return bool
*/
public static function is_module_active() {
return false;
}
/**
* Activates the product by installing and activating its plugin
*
* @param bool|WP_Error $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return boolean|\WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
// A bundle is not a module. There's nothing in the plugin to be activated, so it's ok to fail to activate the module.
$product_activation = true;
}
// At this point, Jetpack plugin is installed. Let's activate each individual product.
$activation = Anti_Spam::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Backup::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Scan::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
return $activation;
}
/**
* Checks whether the Product is active
*
* Security is a bundle and not a module. Activation takes place on WPCOM. So lets consider it active if jetpack is active and has the plan.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active() && static::has_required_plan();
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_security_t1_yearly',
'jetpack_security_t1_monthly',
'jetpack_security_t1_bi_yearly',
'jetpack_security_t2_yearly',
'jetpack_security_t2_monthly',
);
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_required_plan() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if (
str_starts_with( $purchase->product_slug, 'jetpack_security' ) ||
str_starts_with( $purchase->product_slug, 'jetpack_complete' )
) {
return true;
}
}
}
return false;
}
/**
* Checks whether product is a bundle.
*
* @return boolean True
*/
public static function is_bundle_product() {
return true;
}
/**
* Return all the products it contains.
*
* @return array Product slugs
*/
public static function get_supported_products() {
return array( 'backup', 'scan', 'anti-spam' );
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return '';
}
}
@@ -0,0 +1,190 @@
<?php
/**
* Feature: Site Accelerator
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Site Accelerator module.
*/
class Site_Accelerator extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'site-accelerator';
/**
* The category of the product
*
* @var string
*/
public static $category = 'performance';
/**
* The slug of the plugin associated with this product.
* Site Accelerator is a feature available as part of the Jetpack plugin.
*
* @var string
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* The Plugin file associated with stats
*
* @var string|null
*/
public static $plugin_filename = self::JETPACK_PLUGIN_FILENAME;
/**
* The Jetpack module name associated with this product
*
* @var string|null
*/
public static $module_name = 'site-accelerator';
/**
* Whether this module is a Jetpack feature
*
* @var boolean
*/
public static $is_feature = true;
/**
* Whether this product requires a user connection
*
* @var boolean
*/
public static $requires_user_connection = false;
/**
* Whether this product has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* Whether the product requires a plan to run
* The plan could be paid or free
*
* @var bool
*/
public static $requires_plan = false;
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Site Accelerator';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Site Accelerator';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Draw your readers from one post to another', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Draw your readers from one post to another, increasing overall traffic on your site', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized feature list
*
* @return array Site Accelerattor features list
*/
public static function get_features() {
return array();
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array(
'available' => true,
'is_free' => true,
);
}
/**
* Checks whether the Product is active.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active();
}
/**
* Checks whether the plugin is installed
*
* @return boolean
*/
public static function is_plugin_installed() {
return static::is_jetpack_plugin_installed();
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack#/settings?term=site%20accelerator' );
}
/**
* Activates the Jetpack plugin
*
* @return null|WP_Error Null on success, WP_Error on invalid file.
*/
public static function activate_plugin(): ?WP_Error {
$plugin_filename = static::get_installed_plugin_filename( self::JETPACK_PLUGIN_SLUG );
if ( $plugin_filename ) {
return activate_plugin( $plugin_filename );
}
}
}
@@ -0,0 +1,239 @@
<?php
/**
* Jetpack Social product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Status\Host;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Social product
*/
class Social extends Hybrid_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'social';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'publicize';
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
public static $plugin_slug = 'jetpack-social';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* Social has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-social/jetpack-social.php',
'social/jetpack-social.php',
'jetpack-social-dev/jetpack-social.php',
);
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'social-enhanced-publishing';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Social';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Social';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Autoshare your posts to social networks and track engagement in one place.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Grow your following by sharing your content across social media automatically.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Social features list
*/
public static function get_features() {
return array(
__( 'Post to social networks', 'jetpack-my-jetpack' ),
__( 'Schedule publishing', 'jetpack-my-jetpack' ),
__( 'Supports the major social networks', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_social_v1_yearly';
}
/**
* Gets the 'status' of the Social product
*
* @return string
*/
public static function get_status() {
$status = parent::get_status();
if ( Products::STATUS_NEEDS_PLAN === $status ) {
// If the status says that the site needs a plan,
// My Jetpack shows "Learn more" CTA,
// We want to instead show the "Activate" CTA.
$status = Products::STATUS_NEEDS_ACTIVATION;
}
return $status;
}
/**
* Get the product-slugs of the paid plans for this product (not including bundles)
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_social_v1_yearly',
'jetpack_social_v1_monthly',
'jetpack_social_v1_bi_yearly',
'jetpack_social_basic_yearly',
'jetpack_social_monthly',
'jetpack_social_basic_monthly',
'jetpack_social_basic_bi_yearly',
'jetpack_social_advanced_yearly',
'jetpack_social_advanced_monthly',
'jetpack_social_advanced_bi_yearly',
);
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_paid_plan_for_product() {
if ( parent::has_paid_plan_for_product() ) {
return true;
}
// For atomic sites, do a feature check to see if the republicize feature is available
// This feature is available by default on all Jetpack sites
if ( ( new Host() )->is_woa_site() && static::does_site_have_feature( 'republicize' ) ) {
return true;
}
return false;
}
/**
* Get the URL where the user manages the product.
*
* @return string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=jetpack-social' );
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'growth', 'complete' );
}
}
@@ -0,0 +1,220 @@
<?php
/**
* Starter plan
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use WP_Error;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Starter plan
*/
class Starter extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'starter';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'starter';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Starter';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Starter';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Essential security tools: real-time backups and comment spam protection.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Essential security tools: real-time backups and comment spam protection.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Starter features list
*/
public static function get_features() {
return array(
_x( 'Real-time cloud backups with 1GB storage', 'Starter Product Feature', 'jetpack-my-jetpack' ),
_x( 'One-click fixes for most threats', 'Starter Product Feature', 'jetpack-my-jetpack' ),
_x( 'Comment & form spam protection', 'Starter Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_starter_yearly';
}
/**
* Checks whether the Jetpack module is active
*
* This is a bundle and not a product. We should not use this information for anything
*
* @return bool
*/
public static function is_module_active() {
return false;
}
/**
* Activates the product by installing and activating its plugin
*
* @param bool|WP_Error $current_result Is the result of the top level activation actions. You probably won't do anything if it is an WP_Error.
* @return boolean|\WP_Error
*/
public static function do_product_specific_activation( $current_result ) {
$product_activation = parent::do_product_specific_activation( $current_result );
if ( is_wp_error( $product_activation ) && 'module_activation_failed' === $product_activation->get_error_code() ) {
// A bundle is not a module. There's nothing in the plugin to be activated, so it's ok to fail to activate the module.
$product_activation = true;
}
// At this point, Jetpack plugin is installed. Let's activate each individual product.
$activation = Anti_Spam::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
$activation = Backup::activate();
if ( is_wp_error( $activation ) ) {
return $activation;
}
return $activation;
}
/**
* Checks whether the Product is active
*
* Jetpack Starter is a bundle and not a module. Activation takes place on WPCOM. So lets consider it active if jetpack is active and has the plan.
*
* @return boolean
*/
public static function is_active() {
return static::is_jetpack_plugin_active() && static::has_required_plan();
}
/**
* Checks whether the current plan (or purchases) of the site already supports the product
*
* @return boolean
*/
public static function has_required_plan() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( is_wp_error( $purchases_data ) ) {
return false;
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if ( str_starts_with( $purchase->product_slug, 'jetpack_starter' ) ) {
return true;
}
}
}
return false;
}
/**
* Get the product-slugs of the paid plans for this product.
* (Do not include bundle plans, unless it's a bundle plan itself).
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_starter_yearly',
'jetpack_starter_monthly',
);
}
/**
* Checks whether product is a bundle.
*
* @return boolean True
*/
public static function is_bundle_product() {
return true;
}
/**
* Return all the products it contains.
*
* @return Array Product slugs
*/
public static function get_supported_products() {
return array( 'backup', 'anti-spam' );
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return '';
}
}
@@ -0,0 +1,338 @@
<?php
/**
* Jetpack Stats product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Initializer;
use Automattic\Jetpack\My_Jetpack\Module_Product;
use Automattic\Jetpack\My_jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\Status\Host;
use Jetpack_Options;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the Jetpack Stats product
*/
class Stats extends Module_Product {
/**
* The product slug
*
* @var string
*/
public static $slug = 'stats';
/**
* The Jetpack module name associated with this product
*
* @var string|null
*/
public static $module_name = 'stats';
/**
* The category of the product
*
* @var string
*/
public static $category = 'growth';
/**
* The Plugin slug associated with stats
*
* @var string|null
*/
public static $plugin_slug = self::JETPACK_PLUGIN_SLUG;
/**
* The Plugin file associated with stats
*
* @var string|null
*/
public static $plugin_filename = self::JETPACK_PLUGIN_FILENAME;
/**
* Stats only requires site connection, not user connection
*
* @var bool
*/
public static $requires_user_connection = false;
/**
* Stats does not have a standalone plugin (yet?)
*
* @var bool
*/
public static $has_standalone_plugin = false;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'stats-paid';
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'Stats';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack Stats';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Clear, concise, and actionable analysis of your site performance.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'With Jetpack Stats, you dont need to be a data scientist to see how your site is performing, understand your visitors, and grow your site.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array CRM features list
*/
public static function get_features() {
return array(
__( 'Real-time data on visitors', 'jetpack-my-jetpack' ),
__( 'Traffic stats and trends for post and pages', 'jetpack-my-jetpack' ),
__( 'Detailed statistics about links leading to your site', 'jetpack-my-jetpack' ),
__( 'GDPR compliant', 'jetpack-my-jetpack' ),
__( 'Access to upcoming advanced features', 'jetpack-my-jetpack' ),
__( 'Priority support', 'jetpack-my-jetpack' ),
__( 'Commercial use', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product pricing details
* Only showing the pricing details for the commercial product
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_stats_yearly';
}
/**
* Get the WPCOM Pay Whatever You Want product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_pwyw_product_slug() {
return 'jetpack_stats_pwyw_yearly';
}
/**
* Get the WPCOM free product slug
*
* @return ?string
*/
public static function get_wpcom_free_product_slug() {
return 'jetpack_stats_free_yearly';
}
/**
* Gets the 'status' of the Stats product
*
* @return string
*/
public static function get_status() {
$status = parent::get_status();
if ( Products::STATUS_MODULE_DISABLED === $status && ! Initializer::is_registered() ) {
// If the site has never been connected before, show the "Learn more" CTA,
// that points to the add Stats product interstitial.
$status = Products::STATUS_NEEDS_FIRST_SITE_CONNECTION;
}
return $status;
}
/**
* Checks whether the product can be upgraded to a different product.
* Stats Commercial plan (highest tier) & Jetpack Complete are not upgradable.
* Also we don't push PWYW users to upgrade.
*
* @return boolean
*/
public static function is_upgradable() {
// For now, atomic sites with stats are not in a position to upgrade
if ( ( new Host() )->is_woa_site() ) {
return false;
}
$purchases_data = Wpcom_Products::get_site_current_purchases();
if ( ! is_wp_error( $purchases_data ) && is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
// Jetpack complete includes Stats commercial & cannot be upgraded
if (
str_starts_with( $purchase->product_slug, 'jetpack_complete' ) ||
str_starts_with( $purchase->product_slug, 'jetpack_growth' )
) {
return false;
} elseif (
// Stats commercial purchased with highest tier cannot be upgraded.
in_array(
$purchase->product_slug,
array( 'jetpack_stats_yearly', 'jetpack_stats_monthly', 'jetpack_stats_bi_yearly' ),
true
) && $purchase->current_price_tier_slug === 'more_than_1m_views'
) {
return false;
} elseif (
// If user already has Stats PWYW, we won't push them to upgrade.
$purchase->product_slug === 'jetpack_stats_pwyw_yearly'
) {
return false;
}
}
}
return true;
}
/**
* Get the product-slugs of the paid plans for this product (not including bundles)
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_stats_yearly',
'jetpack_stats_monthly',
'jetpack_stats_bi_yearly',
'jetpack_stats_pwyw_yearly',
);
}
/**
* Returns a productType parameter for an upgrade URL, determining whether
* to show the PWYW upgrade interstitial or commercial upgrade interstitial.
*
* @return string
*/
public static function get_url_product_type() {
$purchases_data = Wpcom_Products::get_site_current_purchases();
$is_commercial_site = Initializer::is_commercial_site();
if ( is_wp_error( $purchases_data ) ) {
return $is_commercial_site ? '&productType=commercial' : '';
}
if ( $is_commercial_site ) {
return '&productType=commercial';
}
if ( is_array( $purchases_data ) && ! empty( $purchases_data ) ) {
foreach ( $purchases_data as $purchase ) {
if (
str_starts_with( $purchase->product_slug, static::get_wpcom_free_product_slug() )
) {
return '&productType=personal';
} elseif (
in_array(
$purchase->product_slug,
array( 'jetpack_stats_yearly', 'jetpack_stats_monthly', 'jetpack_stats_bi_yearly' ),
true
) &&
! empty( $purchase->current_price_tier_slug )
) {
return '&productType=commercial';
}
}
}
return '';
}
/**
* Checks whether the product supports trial or not.
* Since Jetpack Stats has been widely available as a free product in the past, it "supports" a trial.
*
* @return boolean
*/
public static function has_trial_support() {
return true;
}
/**
* Get the WordPress.com URL for purchasing Jetpack Stats for the current site.
*
* @return ?string
*/
public static function get_purchase_url() {
$status = static::get_status();
if ( $status === Products::STATUS_NEEDS_FIRST_SITE_CONNECTION ) {
return null;
}
// The returning URL could be customized by changing the `redirect_uri` param with relative path.
return sprintf(
'%s#!/stats/purchase/%d?from=jetpack-my-jetpack%s&redirect_uri=%s',
admin_url( 'admin.php?page=stats' ),
Jetpack_Options::get_option( 'id' ),
static::get_url_product_type(),
rawurlencode( 'admin.php?page=stats' )
);
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
return admin_url( 'admin.php?page=stats' );
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'growth', 'complete' );
}
}
@@ -0,0 +1,319 @@
<?php
/**
* VideoPress product
*
* @package my-jetpack
*/
namespace Automattic\Jetpack\My_Jetpack\Products;
use Automattic\Jetpack\My_Jetpack\Hybrid_Product;
use Automattic\Jetpack\My_Jetpack\Wpcom_Products;
use Automattic\Jetpack\VideoPress\Stats as VideoPress_Stats;
use WP_Error;
use WP_REST_Response;
if ( ! defined( 'ABSPATH' ) ) {
exit( 0 );
}
/**
* Class responsible for handling the VideoPress product
*/
class Videopress extends Hybrid_Product {
private const VIDEOPRESS_STATS_KEY = 'my-jetpack-videopress-stats';
private const VIDEOPRESS_PERIOD_KEY = 'my-jetpack-videopress-period';
/**
* The product slug
*
* @var string
*/
public static $slug = 'videopress';
/**
* The Jetpack module name
*
* @var string
*/
public static $module_name = 'videopress';
/**
* The slug of the plugin associated with this product.
*
* @var string
*/
public static $plugin_slug = 'jetpack-videopress';
/**
* The category of the product
*
* @var string
*/
public static $category = 'performance';
/**
* The filename (id) of the plugin associated with this product.
*
* @var string
*/
public static $plugin_filename = array(
'jetpack-videopress/jetpack-videopress.php',
'videopress/jetpack-videopress.php',
'jetpack-videopress-dev/jetpack-videopress.php',
);
/**
* Search only requires site connection
*
* @var boolean
*/
public static $requires_user_connection = true;
/**
* VideoPress has a standalone plugin
*
* @var bool
*/
public static $has_standalone_plugin = true;
/**
* Whether this product has a free offering
*
* @var bool
*/
public static $has_free_offering = true;
/**
* The feature slug that identifies the paid plan
*
* @var string
*/
public static $feature_identifying_paid_plan = 'videopress';
/**
* Setup VideoPress REST API endpoints
*
* @return void
*/
public static function register_endpoints(): void {
parent::register_endpoints();
// Get Jetpack VideoPress data.
register_rest_route(
'my-jetpack/v1',
'/site/videopress/data',
array(
'methods' => \WP_REST_Server::READABLE,
'callback' => __CLASS__ . '::get_site_videopress_data',
'permission_callback' => __CLASS__ . '::permissions_callback',
)
);
}
/**
* Checks if the user has the correct permissions
*/
public static function permissions_callback() {
return current_user_can( 'edit_posts' );
}
/**
* Get the product name
*
* @return string
*/
public static function get_name() {
return 'VideoPress';
}
/**
* Get the product title
*
* @return string
*/
public static function get_title() {
return 'Jetpack VideoPress';
}
/**
* Get the internationalized product description
*
* @return string
*/
public static function get_description() {
return __( 'Powerful and flexible video hosting.', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized product long description
*
* @return string
*/
public static function get_long_description() {
return __( 'Stunning-quality, ad-free video in the WordPress Editor', 'jetpack-my-jetpack' );
}
/**
* Get the internationalized features list
*
* @return array Boost features list
*/
public static function get_features() {
return array(
_x( '1TB of storage', 'VideoPress Product Feature', 'jetpack-my-jetpack' ),
_x( 'Built into WordPress editor', 'VideoPress Product Feature', 'jetpack-my-jetpack' ),
_x( 'Ad-free and customizable player', 'VideoPress Product Feature', 'jetpack-my-jetpack' ),
_x( 'Unlimited users', 'VideoPress Product Feature', 'jetpack-my-jetpack' ),
);
}
/**
* Get the product princing details
*
* @return array Pricing details
*/
public static function get_pricing_for_ui() {
return array_merge(
array(
'available' => true,
'wpcom_product_slug' => static::get_wpcom_product_slug(),
),
Wpcom_Products::get_product_pricing( static::get_wpcom_product_slug() )
);
}
/**
* Get the URL the user is taken after purchasing the product through the checkout
*
* @return ?string
*/
public static function get_post_checkout_url() {
return self::get_manage_url();
}
/**
* Get the WPCOM product slug used to make the purchase
*
* @return ?string
*/
public static function get_wpcom_product_slug() {
return 'jetpack_videopress';
}
/**
* Get the URL the user is taken after activating the product
*
* @return ?string
*/
public static function get_post_activation_url() {
return ''; // stay in My Jetpack page.
}
/**
* Get the URL where the user manages the product
*
* @return ?string
*/
public static function get_manage_url() {
if ( method_exists( 'Automattic\Jetpack\VideoPress\Initializer', 'should_initialize_admin_ui' ) && \Automattic\Jetpack\VideoPress\Initializer::should_initialize_admin_ui() ) {
return \Automattic\Jetpack\VideoPress\Admin_UI::get_admin_page_url();
} else {
return admin_url( 'admin.php?page=jetpack#/settings?term=videopress' );
}
}
/**
* Get the product-slugs of the paid plans for this product (not including bundles)
*
* @return array
*/
public static function get_paid_plan_product_slugs() {
return array(
'jetpack_videopress',
'jetpack_videopress_monthly',
'jetpack_videopress_bi_yearly',
);
}
/**
* Return product bundles list
* that supports the product.
*
* @return boolean|array Products bundle list.
*/
public static function is_upgradable_by_bundle() {
return array( 'complete' );
}
/**
* Get stats for VideoPress
*
* @return array|WP_Error
*/
private static function get_videopress_stats() {
$video_count = array_sum( (array) wp_count_attachments( 'video' ) );
if ( ! class_exists( 'Automattic\Jetpack\VideoPress\Stats' ) ) {
return array(
'videoCount' => $video_count,
);
}
$featured_stats = get_transient( self::VIDEOPRESS_STATS_KEY );
if ( $featured_stats ) {
return array(
'featuredStats' => $featured_stats,
'videoCount' => $video_count,
);
}
$stats_period = get_transient( self::VIDEOPRESS_PERIOD_KEY );
$videopress_stats = new VideoPress_Stats();
// If the stats period exists, retrieve that information without checking the view count.
// If it does not, check the view count of monthly stats and determine if we want to show yearly or monthly stats.
if ( $stats_period ) {
if ( $stats_period === 'day' ) {
$featured_stats = $videopress_stats->get_featured_stats( 60, 'day' );
} else {
$featured_stats = $videopress_stats->get_featured_stats( 2, 'year' );
}
} else {
$featured_stats = $videopress_stats->get_featured_stats( 60, 'day' );
if (
! is_wp_error( $featured_stats ) &&
$featured_stats &&
( $featured_stats['data']['views']['current'] < 500 || $featured_stats['data']['views']['previous'] < 500 )
) {
$featured_stats = $videopress_stats->get_featured_stats( 2, 'year' );
}
}
if ( is_wp_error( $featured_stats ) || ! $featured_stats ) {
return array(
'videoCount' => $video_count,
);
}
set_transient( self::VIDEOPRESS_PERIOD_KEY, $featured_stats['period'], WEEK_IN_SECONDS );
set_transient( self::VIDEOPRESS_STATS_KEY, $featured_stats, DAY_IN_SECONDS );
return array(
'featuredStats' => $featured_stats,
'videoCount' => $video_count,
);
}
/**
* Get VideoPress data for the REST API
*
* @return WP_REST_Response|WP_Error
*/
public static function get_site_videopress_data() {
$videopress_stats = self::get_videopress_stats();
return rest_ensure_response( $videopress_stats );
}
}