├── .gitignore ├── README.md ├── assets ├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .stylelintrc ├── build │ ├── css │ │ ├── category.css │ │ └── settings.css │ ├── js │ │ ├── category.js │ │ ├── editor.js │ │ └── settings.js │ └── manifest.json ├── images │ ├── banner-1544x500.png │ ├── banner-772x250.png │ ├── icon-128x128.png │ ├── icon-256x256.png │ ├── screenshot-1.png │ ├── screenshot-2.png │ ├── screenshot-3.png │ ├── screenshot-4.png │ ├── screenshot-5.png │ └── screenshot-6.png ├── package-lock.json ├── package.json ├── src │ ├── js │ │ ├── category.js │ │ ├── editor.js │ │ ├── gutenberg │ │ │ └── slot-fill.js │ │ └── settings.js │ └── scss │ │ ├── base │ │ └── _colors.scss │ │ ├── category.scss │ │ └── settings.scss └── webpack.config.js ├── composer.json ├── headless-cms.php ├── inc ├── classes │ ├── api │ │ ├── class-api-settings.php │ │ ├── class-get-post.php │ │ ├── class-get-posts.php │ │ ├── class-header-footer-api.php │ │ ├── class-home-page.php │ │ ├── class-post-by-tax.php │ │ ├── class-wc-cart.php │ │ ├── class-wc-countries.php │ │ └── class-wc-states.php │ ├── class-assets.php │ ├── class-category.php │ ├── class-customizer.php │ ├── class-plugin.php │ ├── class-preview.php │ ├── class-settings.php │ ├── mutations │ │ ├── class-add-wishlist.php │ │ └── class-delete-wishlist.php │ └── queries │ │ ├── class-get-wishlist.php │ │ ├── class-header-footer-schema.php │ │ ├── class-post-schema.php │ │ ├── class-product.php │ │ ├── class-register-countries.php │ │ ├── class-register-shipping.php │ │ ├── class-register-states.php │ │ ├── class-seo.php │ │ └── class-sticky-post.php ├── helpers │ ├── autoloader.php │ └── custom-functions.php └── traits │ └── trait-singleton.php ├── phpcs.xml ├── readme.txt └── templates ├── category-img-form.php ├── comments-section.php ├── featured-post-section.php ├── frontend-site-details-section.php ├── hero-section.php ├── latest-posts-section.php ├── post-preview-section.php ├── search-section.php └── settings-form-template.php /.gitignore: -------------------------------------------------------------------------------- 1 | # node/bower and dependency directories 2 | node_modules 3 | bower_components 4 | .next 5 | .sass-cache 6 | 7 | # Log files and databases 8 | *.log 9 | *.sql 10 | 11 | # Code Editor files 12 | .idea 13 | .vscode 14 | *.sublime-project 15 | *.sublime-workspace 16 | 17 | # OS generated files 18 | .DS_Store 19 | Thumbs.db 20 | 21 | # Other 22 | .cache 23 | .env 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Headless CMS 2 | 3 | [![Project Status: Active.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) 4 | 5 | 6 | A WordPress plugin that adds features to use WordPress as a headless CMS with any front-end environment using REST API. 7 | This plugin provides multiple features and you can use the one's that are relevant to your front-end application. 8 | You don't necessarily need to use all. 9 | 10 | 11 | 12 | Headless CMS WordPress Plugin 13 | 14 | 15 | ## Install via Composer 16 | 17 | ```shell script 18 | composer require imranhsayed/headless-cms 19 | ``` 20 | 21 | - [https://packagist.org/packages/imranhsayed/headless-cms](https://packagist.org/packages/imranhsayed/headless-cms) 22 | 23 | 24 | ## Maintainer 25 | 26 | | Name | Github Username | 27 | |--------------------------------------------------------|-----------------| 28 | | [Imran Sayed](mailto:codeytek.academy@gmail.com) | @imranhsayed | 29 | 30 | ## Assets 31 | 32 | Assets folder contains webpack setup and can be used for creating blocks or adding any other custom scripts like javascript for admin. 33 | 34 | - Run `npm i` from `assets` folder to install required npm packages. 35 | - Use `npm run dev` during development for assets. 36 | - Use `npm run prod` for production. 37 | - Use `npm run eslint:fix js/fileName.js` for fixing and linting eslint errors and warning. 38 | 39 | # REST API ENDPOINT 40 | 41 | > This plugin provides you different endpoints using WordPress REST API. 42 | 43 | ## Getting Started :clipboard: 44 | 45 | These instructions will get you a copy of the project up and running on your local machine for development purposes. 46 | 47 | ## Prerequisites :door: 48 | 49 | You need to have any WordPress theme activated on your WordPress project, which has REST API enabled. 50 | 51 | ## Installation :wrench: 52 | 53 | 1. Clone the plugin directory in the `/wp-content/plugins/` directory, or install a zipped directory of this plugin through the [WordPress plugins](https://wordpress.org/plugins/headless-cms/) screen directly. 54 | 2. Activate the plugin through the 'Plugins' screen in WordPress 55 | 56 | ## Example Frontend applications 57 | 58 | Example of front-end applications where this plugin can be used: 59 | 60 | 1. [Gatsby WordPress Theme](https://github.com/imranhsayed/gatsby-wordpress-themes) 61 | 2. [React Wordpress Theme](https://github.com/imranhsayed/react-wordpress-theme) 62 | 63 | ## Features 64 | 65 | 1. Custom REST API Endpoints. 66 | 2. Social links in customizer. 67 | 3. Image uploads for categories. 68 | 4. Custom header and footer menus. 69 | 5. Custom Widgets. 70 | 6. Custom Header and Footer GraphQL fields when using [wp-graphql](https://github.com/wp-graphql/wp-graphql) plugin 71 | 7. Adds coAuthors data in the GraphQL Api. 72 | 73 | * Adds option to add social links in customizer 74 | * Registers two custom menus for header ( menu location = hcms-menu-header ) and for footer ( menu location = hcms-menu-footer ) 75 | * Registers the following sidebars 76 | 77 | 1. HCMS Footer #1 with sidebar id 'hcms-sidebar-1' 78 | 79 | ![](assets/images/screenshot-4.png) 80 | 81 | 2. HCMS Footer #2 with sidebar id 'hcms-sidebar-2' 82 | 83 | ![](assets/images/screenshot-5.png) 84 | 85 | * Adds frontend preview link option in the editor. 86 | For Preview to work , you also need the [wp-graphql-jwt-authentication](https://github.com/wp-graphql/wp-graphql-jwt-authentication) plugin as well 87 | 88 | ### More Features 89 | 1. Registers the sections for socials icons in the customizer 90 | 91 | * Social icons urls for 'facebook', 'twitter', 'instagram', 'youtube' 92 | 93 | ![](assets/images/screenshot-6.png) 94 | 95 | 2. Image upload features for categories 96 | 97 | * Provides Image upload features for categories. 98 | 99 | ![](assets/images/screenshot-3.png) 100 | 101 | 3. Plugin Settings Page 102 | 103 | * Settings for getting data for a custom page like Hero section, Search section, Featured post section, latest posts heading. 104 | 105 | * Registers custom end points 106 | 107 | 4. Following fields when using [wp-graphql](https://github.com/wp-graphql/wp-graphql) plugin. 108 | 109 | * Custom Header and Footer GraphQL 110 | ![](assets/images/screenshot-2.png) 111 | 112 | * WooCommerce Countries and States 113 | 114 | ```javascript 115 | { 116 | wooCountries { 117 | billingCountries { 118 | countryCode 119 | countryName 120 | } 121 | shippingCountries { 122 | countryCode 123 | countryName 124 | } 125 | } 126 | wooStates(countryCode: "in") { 127 | states { 128 | stateCode 129 | stateName 130 | } 131 | } 132 | } 133 | ``` 134 | 135 | * WooCommerce Shipping Zones. 136 | 137 | ```javascript 138 | { 139 | shippingInfo { 140 | shippingZones 141 | storePostCode 142 | } 143 | } 144 | ``` 145 | 146 | * Add Product to Wishlist Mutation ( Authenticated request ) 147 | 148 | ```javascript 149 | mutation ADD_ITEM { 150 | addToWishlist(input: {clientMutationId: "example", productId: 340}) { 151 | clientMutationId 152 | error 153 | added 154 | productId 155 | wishlistProductIds 156 | } 157 | } 158 | ``` 159 | 160 | * Remove Product from Wishlist Mutation( Authenticated request ) 161 | 162 | ```javascript 163 | mutation REMOVE_ITEM{ 164 | removeFromWishlist(input: {clientMutationId: "example", productId: 340}) { 165 | error 166 | productId 167 | removed 168 | wishlistProductIds 169 | } 170 | } 171 | ``` 172 | 173 | * Get Products from Wishlist Mutation ( Authenticated request ) 174 | ```javascript 175 | query GET_WISHLIST { 176 | getWishList { 177 | productIds 178 | error 179 | products { 180 | databaseId 181 | name 182 | slug 183 | buttonText 184 | image { 185 | alt 186 | src 187 | attachmentId 188 | } 189 | priceHtml 190 | productUrl 191 | stockQuantity 192 | stockStatus 193 | typename 194 | } 195 | } 196 | } 197 | ``` 198 | 199 | * Schema Details 200 | 201 | ```javascript 202 | posts { 203 | nodes { 204 | seo { 205 | schemaDetails 206 | } 207 | } 208 | } 209 | ``` 210 | 211 | ## Available Endpoints: 212 | 213 | ### Get single post ( GET request ) 214 | * `http://example.com/wp-json/rae/v1/post?post_id=1` 215 | 216 | ### Get posts by page no: ( GET Request ) 217 | * `http://example.com/wp-json/rae/v1/posts?page_no=1` 218 | 219 | ### Get header and footer date: ( GET Request ) 220 | * Get the header data ( site title, site description , site logo URL, menu items ) and footer data ( footer menu items, social icons ) 221 | * `http://example.com/wp-json/rae/v1/header-footer?header_location_id=hcms-menu-header&footer_location_id=hcms-menu-footer` 222 | 223 | ## Get WooCommerce Country and states ( GET Request ) 224 | - `/wp-json/rae/v1/wc/countries/` 225 | - `/wp-json/rae/v1/wc/states?countryCode=IN` 226 | 227 | ## Contributing :busts_in_silhouette: 228 | 229 | Please read [CONTRIBUTING.md](https://gist.github.com/PurpleBooth/b24679402957c63ec426) for details on our code of conduct, and the process for submitting pull requests to us. 230 | 231 | ## Versioning 232 | 233 | I use [Git](https://github.com/) for versioning. 234 | 235 | ## Author :pencil: 236 | 237 | * **[Imran Sayed](https://codeytek.com)** 238 | 239 | ## License :scroll: 240 | 241 | ![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-blue.svg) 242 | 243 | - **[GPLv2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)** 244 | -------------------------------------------------------------------------------- /assets/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /assets/.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.min.js 2 | **/node_modules/** 3 | **/vendor/** 4 | **/build/** 5 | -------------------------------------------------------------------------------- /assets/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "wordpress", 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "ecmaVersion": 7, 6 | "ecmaFeatures": { 7 | "jsx": true, 8 | "arrowFunctions": true, 9 | "blockBindings": true, 10 | "classes": true, 11 | "defaultParams": true, 12 | "modules": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "env": { 17 | "es6": true, 18 | "browser": true, 19 | "node": true, 20 | "commonjs": true, 21 | "jquery": true 22 | }, 23 | "rules": { 24 | "camelcase": [1], 25 | "space-in-parens": [1, "always"], 26 | "no-trailing-spaces": [1], 27 | "spaced-comment": [0], 28 | "padded-blocks": [0], 29 | "prefer-template": [0], 30 | "max-len": [0], 31 | "no-else-return": [0], 32 | "func-names": [0], 33 | "object-shorthand": [0], 34 | "indent": ["error", "tab"], 35 | "space-before-function-paren": 0, 36 | "no-tabs": 0, 37 | "prefer-destructuring": 0, 38 | "no-undef": 0, 39 | "no-param-reassign": 0 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /assets/.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ignoreFiles": "", 3 | "rules": { 4 | "at-rule-empty-line-before": null, 5 | "block-no-empty": true, 6 | "color-hex-case": "lower", 7 | "color-no-invalid-hex": true, 8 | "comment-no-empty": true, 9 | "declaration-block-no-shorthand-property-overrides": true, 10 | "declaration-colon-space-before": "never", 11 | "declaration-no-important": true, 12 | "font-family-name-quotes": "always-where-recommended", 13 | "font-weight-notation": "numeric", 14 | "function-url-quotes": "always", 15 | "indentation": "tab", 16 | "max-empty-lines": 1, 17 | "no-missing-end-of-source-newline": true, 18 | "number-leading-zero": "always", 19 | "property-case": "lower", 20 | "rule-empty-line-before": null, 21 | "selector-combinator-space-after": "always", 22 | "selector-list-comma-newline-after": null, 23 | "selector-max-specificity": "0,3,1", 24 | "selector-pseudo-element-case": "lower", 25 | "selector-pseudo-element-colon-notation": "double", 26 | "unit-case": "lower", 27 | "unit-no-unknown": true 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /assets/build/css/category.css: -------------------------------------------------------------------------------- 1 | .custom_media_image{margin:0;padding:0;max-height:100px;float:none}.wp-core-ui .hcms_hide{display:none} -------------------------------------------------------------------------------- /assets/build/css/settings.css: -------------------------------------------------------------------------------- 1 | .hcms-settings-header{margin:24px 0 36px}.hcms-hero-img-section input:focus,.hcms-srch-back-img-section input:focus{outline:none}.hcms-hero-img-section .hcms-hero-img,.hcms-srch-back-img-section .hcms-hero-img{display:none;margin-bottom:10px}.hcms-hero-img-section .hcms-hero-remove-btn,.hcms-srch-back-img-section .hcms-hero-remove-btn{display:none;color:#0071a1;background:#f3f5f6;vertical-align:top;text-decoration:none;font-size:13px;line-height:2;min-height:30px;margin:0;padding:0 10px;cursor:pointer;border:1px solid #0071a1;-webkit-appearance:none;border-radius:3px;white-space:nowrap;box-sizing:border-box}.hcms-hero-img-section .hcms-hero-upload-btn,.hcms-srch-back-img-section .hcms-hero-upload-btn{width:200px;position:relative;text-align:center;border:1px dashed #b4b9be;box-sizing:border-box;padding:9px 0;line-height:1.6;cursor:pointer;background-color:#edeff0;color:#32373c}.hcms-hero-img-section.uploaded .hcms-hero-img,.hcms-srch-back-img-section.uploaded .hcms-hero-img{display:block}.hcms-hero-img-section.uploaded .hcms-hero-remove-btn,.hcms-srch-back-img-section.uploaded .hcms-hero-remove-btn{display:inline-block}.hcms-hero-img-section.uploaded .hcms-hero-upload-btn,.hcms-srch-back-img-section.uploaded .hcms-hero-upload-btn{width:auto;color:#0071a1;background:#f3f5f6;vertical-align:top;display:inline-block;text-decoration:none;font-size:13px;line-height:2;min-height:30px;margin:0 8px 0 0;padding:0 10px;cursor:pointer;border:1px solid #0071a1;-webkit-appearance:none;border-radius:3px;white-space:nowrap;box-sizing:border-box} -------------------------------------------------------------------------------- /assets/build/js/category.js: -------------------------------------------------------------------------------- 1 | !function(a){var n={};function r(e){if(n[e])return n[e].exports;var t=n[e]={i:e,l:!1,exports:{}};return a[e].call(t.exports,t,t.exports,r),t.l=!0,t.exports}r.m=a,r.c=n,r.d=function(e,t,a){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:a})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var a=Object.create(null);if(r.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var n in t)r.d(a,n,function(e){return t[e]}.bind(null,n));return a},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=2)}([,,function(e,t,a){"use strict";a.r(t);var i;a(3);function r(e,t){for(var a=0;a'),i("#hcms_tax_media_button").toggleClass("hcms_hide"),i("#hcms_tax_media_remove").toggleClass("hcms_hide")})}},{key:"mediaUpload",value:function(e){var n=!0,r=wp.media.editor.send.attachment;i("body").on("click",e,function(e){var a="#"+i(this).attr("id"),t=i(a);return n=!0,wp.media.editor.send.attachment=function(e,t){if(!n)return r.apply(a,[e,t]);i("#category-image-id").val(t.id),i("#category-image-wrapper").html(''),i("#category-image-wrapper .custom_media_image").attr("src",t.url).css("display","block"),i("#hcms_tax_media_button").toggleClass("hcms_hide"),i("#hcms_tax_media_remove").toggleClass("hcms_hide")},wp.media.editor.open(t),!1})}},{key:"ajaxRequest",value:function(){i(document).ajaxComplete(function(e,t,a){var n=a.data.split("&");if(-1!==i.inArray("action=add-tag",n)){var r=t.responseXML;""!=i(r).find("term_id").text()&&i("#category-image-wrapper").html("")}})}}])&&r(t.prototype,a),n&&r(t,n),e}())},function(e,t,a){}]); -------------------------------------------------------------------------------- /assets/build/js/editor.js: -------------------------------------------------------------------------------- 1 | !function(n){var r={};function i(e){if(r[e])return r[e].exports;var t=r[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,i),t.l=!0,t.exports}i.m=n,i.c=r,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(t,e){if(1&e&&(t=i(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var r in t)i.d(n,r,function(e){return t[e]}.bind(null,r));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=4)}({4:function(e,t,n){"use strict";n.r(t);n(5)},5:function(e,t){var n=wp.plugins.registerPlugin,d=wp.i18n.__,r=wp.editPost,s=r.PluginSidebar,p=r.PluginSidebarMoreMenuItem,f=wp.element.Fragment;n("plugin-sidebar-expanded-test",{render:function(){var e,t,n,r,i;if("1"!==(null===(e=frontendConfig)||void 0===e?void 0:e.isPreviewLinkActive))return null;var o=null===(t=frontendConfig)||void 0===t?void 0:t.frontendSiteUrl.replace(/\/$/,""),a=wp.data.select("core/editor").getCurrentPost(),l=wp.data.select("core/editor").getEditedPostAttribute("status"),u="".concat(o,"/api/preview/?postType=").concat(null!==(n=null==a?void 0:a.type)&&void 0!==n?n:"","&postId=").concat(null!==(r=null==a?void 0:a.id)&&void 0!==r?r:""),c=React.createElement("a",{href:u,target:"wp-preview-".concat(null!==(i=null==a?void 0:a.id)&&void 0!==i?i:""),style:{margin:"20px",boxShadow:"0 0 0 1.5px #ccc"},className:"components-button editor-post-preview is-button is-default is-large"},d("Preview on frontend","headless-cms"),React.createElement("span",{className:"screen-reader-text"},d("(opens in a new tab)","headless-cms")));return React.createElement(f,null,React.createElement(p,{target:"sidebar-name",icon:"visibility",title:"Frontend Preview"},d("Frontend Preview","headless-cms")),React.createElement(s,{name:"sidebar-name",icon:"visibility",title:"Frontend Preview"},"draft"===l||"publish"===l?c:""))}})}}); -------------------------------------------------------------------------------- /assets/build/js/settings.js: -------------------------------------------------------------------------------- 1 | !function(n){var o={};function r(e){if(o[e])return o[e].exports;var t=o[e]={i:e,l:!1,exports:{}};return n[e].call(t.exports,t,t.exports,r),t.l=!0,t.exports}r.m=n,r.c=o,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)r.d(n,o,function(e){return t[e]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,n){"use strict";n.r(t);var i;n(1);function r(e,t){for(var n=0;n { 13 | 14 | /** 15 | * Category Class. 16 | */ 17 | class Category { 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @return {void} 23 | */ 24 | constructor() { 25 | this.init(); 26 | } 27 | 28 | /** 29 | * Init 30 | * 31 | * @return {void} 32 | */ 33 | init() { 34 | this.mediaUpload( '.hcms_tax_media_button.button' ); 35 | this.addEvents(); 36 | this.ajaxRequest(); 37 | } 38 | 39 | addEvents() { 40 | $( 'body' ).on( 'click', '.hcms_tax_media_remove', function () { 41 | $( '#category-image-id' ).val( '' ); 42 | $( '#category-image-wrapper' ).html( '' ); 43 | $( '#hcms_tax_media_button' ).toggleClass( 'hcms_hide' ); 44 | $( '#hcms_tax_media_remove' ).toggleClass( 'hcms_hide' ); 45 | } ); 46 | } 47 | 48 | mediaUpload( btnClass ) { 49 | let customMedia = true; 50 | let origSendAttachment = wp.media.editor.send.attachment; 51 | 52 | $( 'body' ).on( 'click', btnClass, function ( e ) { 53 | let btnID = '#' + $( this ).attr( 'id' ); 54 | let button = $( btnID ); 55 | customMedia = true; 56 | wp.media.editor.send.attachment = function ( props, attachment ) { 57 | if ( customMedia ) { 58 | $( '#category-image-id' ).val( attachment.id ); 59 | $( '#category-image-wrapper' ).html( '' ); 60 | $( '#category-image-wrapper .custom_media_image' ).attr( 'src', attachment.url ).css( 'display', 'block' ); 61 | $( '#hcms_tax_media_button' ).toggleClass( 'hcms_hide' ); 62 | $( '#hcms_tax_media_remove' ).toggleClass( 'hcms_hide' ); 63 | } else { 64 | return origSendAttachment.apply( btnID, [ props, attachment ] ); 65 | } 66 | }; 67 | wp.media.editor.open( button ); 68 | return false; 69 | } ); 70 | } 71 | 72 | ajaxRequest() { 73 | $( document ).ajaxComplete( function ( event, xhr, settings ) { 74 | let queryStringArr = settings.data.split( '&' ); 75 | if ( -1 !== $.inArray( 'action=add-tag', queryStringArr ) ) { 76 | let xml = xhr.responseXML; 77 | const response = $( xml ).find( 'term_id' ).text(); 78 | if ( '' != response ) { 79 | 80 | // Clear the thumb image 81 | $( '#category-image-wrapper' ).html( '' ); 82 | } 83 | } 84 | } ); 85 | } 86 | } 87 | 88 | new Category(); 89 | 90 | } )( jQuery ); 91 | -------------------------------------------------------------------------------- /assets/src/js/editor.js: -------------------------------------------------------------------------------- 1 | import '../js/gutenberg/slot-fill'; 2 | -------------------------------------------------------------------------------- /assets/src/js/gutenberg/slot-fill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Slot fill for preview button. 3 | * 4 | * @package headless-cms 5 | */ 6 | 7 | const {registerPlugin} = wp.plugins; 8 | const {__} = wp.i18n; 9 | 10 | const { 11 | PluginSidebar, 12 | PluginSidebarMoreMenuItem 13 | } = wp.editPost; 14 | 15 | const {Fragment} = wp.element; 16 | 17 | /** 18 | * PluginSidebarMoreMenuItemTest 19 | * 20 | * @return {Object} Content 21 | */ 22 | const PluginSidebarMoreMenuItemTest = () => { 23 | 24 | if ( '1' !== frontendConfig?.isPreviewLinkActive ) { 25 | return null; 26 | } 27 | 28 | 29 | const frontendSiteUrl = frontendConfig?.frontendSiteUrl.replace( /\/$/, '' ); 30 | const currentPost = wp.data.select( 'core/editor' ).getCurrentPost(); 31 | const myPostStatus = wp.data.select( 'core/editor' ).getEditedPostAttribute( 'status' ); 32 | const previewURL = `${frontendSiteUrl}/api/preview/?postType=${currentPost?.type ?? ''}&postId=${currentPost?.id ?? ''}`; 33 | 34 | const frontendPreviewBtn = ( 35 | 38 | {__( 'Preview on frontend', 'headless-cms' )} 39 | 40 | {__( '(opens in a new tab)', 'headless-cms' )} 41 | 42 | 43 | ); 44 | 45 | return ( 46 | 47 | 51 | {__( 'Frontend Preview', 'headless-cms' )} 52 | 53 | 57 | {( 'draft' === myPostStatus || 'publish' === myPostStatus ) ? ( 58 | frontendPreviewBtn 59 | ) : ''} 60 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | registerPlugin( 'plugin-sidebar-expanded-test', {render: PluginSidebarMoreMenuItemTest} ); 67 | -------------------------------------------------------------------------------- /assets/src/js/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Settings scripts, loaded plugin's settings page. 3 | * 4 | * @package headless-cms 5 | */ 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import '../scss/settings.scss'; 11 | 12 | ( ( $ ) => { 13 | 14 | /** 15 | * Settings Class. 16 | */ 17 | class Settings { 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @return {void} 23 | */ 24 | constructor() { 25 | this.init(); 26 | } 27 | 28 | /** 29 | * Init 30 | * 31 | * @return {void} 32 | */ 33 | init() { 34 | 35 | this.handleMediaUpload( '#hcms-hero-img-section' ); 36 | this.handleMediaUpload( '#hcms-srch-back-img-section' ); 37 | 38 | } 39 | 40 | /** 41 | * Handle Media Upload 42 | * 43 | * @param {string} sectionId Section Id. 44 | * 45 | * @return {void} 46 | */ 47 | handleMediaUpload( sectionId ) { 48 | 49 | /** 50 | * Upload media. 51 | */ 52 | let mediaUploader; 53 | 54 | // When the Upload Button is clicked, open the WordPress Media Uploader to select/change the image. 55 | $( sectionId + ' .hcms-hero-upload-btn' ).click( ( event ) => { 56 | 57 | event.preventDefault(); 58 | 59 | if ( mediaUploader ) { 60 | mediaUploader.open(); 61 | return; 62 | } 63 | 64 | /* eslint-disable */ 65 | mediaUploader = wp.media.frames.file_frame = wp.media( { 66 | title: 'Choose Image', 67 | button: { 68 | text: 'Choose Image' 69 | }, multiple: false 70 | } ); 71 | /* eslint-enable */ 72 | 73 | mediaUploader.on( 'select', function () { 74 | 75 | let attachment = mediaUploader.state().get( 'selection' ).first().toJSON(); 76 | const inputEl = $( sectionId + ' .hcms-hero-input' ); 77 | const imgEl = $( sectionId + ' .hcms-hero-img' ); 78 | const uploadBtnEl = $( sectionId + ' .hcms-hero-upload-btn' ); 79 | 80 | imgEl.attr( 'src', attachment.url ); 81 | inputEl.val( attachment.url ); 82 | uploadBtnEl.val( 'Change Logo' ); 83 | $( sectionId ).addClass( 'uploaded' ); 84 | 85 | } ); 86 | 87 | mediaUploader.open(); 88 | 89 | } ); 90 | 91 | this.handleRemoveMedia( sectionId ); 92 | } 93 | 94 | /** 95 | * Handles Remove Media. 96 | * 97 | * @param {string} sectionId Section Id. 98 | * 99 | * @return {void} 100 | */ 101 | handleRemoveMedia( sectionId ) { 102 | 103 | // When the remove media button is clicked, remove the image url and the image. 104 | $( sectionId + ' .hcms-hero-remove-btn' ).on( 'click', () => { 105 | 106 | const inputEl = $( sectionId + ' .hcms-hero-input' ); 107 | const imgEl = $( sectionId + ' .hcms-hero-img' ); 108 | const uploadBtnEl = $( sectionId + ' .hcms-hero-upload-btn' ); 109 | 110 | imgEl.attr( 'src', '' ); 111 | inputEl.val( '' ); 112 | uploadBtnEl.val( 'Select Logo' ); 113 | $( sectionId ).removeClass( 'uploaded' ); 114 | 115 | } ); 116 | } 117 | } 118 | 119 | new Settings(); 120 | 121 | } )( jQuery ); 122 | 123 | -------------------------------------------------------------------------------- /assets/src/scss/base/_colors.scss: -------------------------------------------------------------------------------- 1 | // Default Black Color 2 | $black: #000000; 3 | 4 | // Default White Color 5 | $white: #ffffff; 6 | 7 | $blue_primary: #0071a1; 8 | 9 | $color_text-gray: #32373c; 10 | 11 | $button_background-gray: #edeff0; 12 | $button_background-gray-secondary: #f3f5f6; 13 | $border_btn-gray: #b4b9be; 14 | -------------------------------------------------------------------------------- /assets/src/scss/category.scss: -------------------------------------------------------------------------------- 1 | .custom_media_image { 2 | margin: 0; 3 | padding: 0; 4 | max-height: 100px; 5 | float:none; 6 | } 7 | 8 | .wp-core-ui { 9 | .hcms_hide { 10 | display: none; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /assets/src/scss/settings.scss: -------------------------------------------------------------------------------- 1 | /* styles */ 2 | 3 | @import "base/colors"; 4 | 5 | .hcms-settings-header { 6 | margin: 24px 0 36px 0; 7 | } 8 | 9 | .hcms-hero-img-section, 10 | .hcms-srch-back-img-section { 11 | 12 | input:focus { 13 | outline: none; 14 | } 15 | 16 | .hcms-hero-img { 17 | display: none; 18 | margin-bottom: 10px; 19 | } 20 | 21 | .hcms-hero-remove-btn { 22 | display: none; 23 | color: $blue_primary; 24 | border-color: $blue_primary; 25 | background: $button_background-gray-secondary; 26 | vertical-align: top; 27 | text-decoration: none; 28 | font-size: 13px; 29 | line-height: 2; 30 | min-height: 30px; 31 | margin: 0; 32 | padding: 0 10px; 33 | cursor: pointer; 34 | border-width: 1px; 35 | border-style: solid; 36 | -webkit-appearance: none; 37 | border-radius: 3px; 38 | white-space: nowrap; 39 | box-sizing: border-box; 40 | } 41 | 42 | .hcms-hero-upload-btn { 43 | width: 200px; 44 | position: relative; 45 | text-align: center; 46 | border: 1px dashed $border_btn-gray; 47 | box-sizing: border-box; 48 | padding: 9px 0; 49 | line-height: 1.6; 50 | cursor: pointer; 51 | background-color: $button_background-gray; 52 | color: $color_text-gray; 53 | } 54 | 55 | &.uploaded { 56 | 57 | .hcms-hero-img { 58 | display: block; 59 | } 60 | .hcms-hero-remove-btn { 61 | display: inline-block; 62 | } 63 | 64 | .hcms-hero-upload-btn { 65 | width: auto; 66 | color: $blue_primary; 67 | border-color: $blue_primary; 68 | background: $button_background-gray-secondary; 69 | vertical-align: top; 70 | display: inline-block; 71 | text-decoration: none; 72 | font-size: 13px; 73 | line-height: 2; 74 | min-height: 30px; 75 | margin: 0 8px 0 0; 76 | padding: 0 10px; 77 | cursor: pointer; 78 | border-width: 1px; 79 | border-style: solid; 80 | -webkit-appearance: none; 81 | border-radius: 3px; 82 | white-space: nowrap; 83 | box-sizing: border-box; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /assets/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* global process __dirname */ 2 | const DEV = 'production' !== process.env.NODE_ENV; 3 | 4 | /** 5 | * Plugins 6 | */ 7 | const path = require( 'path' ); 8 | const MiniCssExtractPlugin = require( 'mini-css-extract-plugin' ); 9 | const OptimizeCssAssetsPlugin = require( 'optimize-css-assets-webpack-plugin' ); 10 | const cssnano = require( 'cssnano' ); 11 | const CleanWebpackPlugin = require( 'clean-webpack-plugin' ); 12 | const UglifyJsPlugin = require( 'uglifyjs-webpack-plugin' ); 13 | const StyleLintPlugin = require( 'stylelint-webpack-plugin' ); 14 | const FriendlyErrorsPlugin = require( 'friendly-errors-webpack-plugin' ); 15 | const WebpackAssetsManifest = require( 'webpack-assets-manifest' ); 16 | 17 | // JS Directory path. 18 | const JSDir = path.resolve( __dirname, 'src/js' ); 19 | const IMG_DIR = path.resolve( __dirname, 'src/images' ); 20 | const FONTS_DIR = path.resolve( __dirname, 'src/fonts' ); 21 | const BUILD_DIR = path.resolve( __dirname, 'build' ); 22 | 23 | const entry = { 24 | settings: JSDir + '/settings.js', 25 | category: JSDir + '/category.js', 26 | editor: JSDir + '/editor.js', 27 | }; 28 | 29 | const output = { 30 | path: BUILD_DIR, 31 | filename: 'js/[name].js' 32 | }; 33 | 34 | /** 35 | * Note: argv.mode will return 'development' or 'production'. 36 | */ 37 | const plugins = ( argv ) => [ 38 | new CleanWebpackPlugin( [ BUILD_DIR ] ), 39 | 40 | new MiniCssExtractPlugin( { 41 | filename: 'css/[name].css' 42 | } ), 43 | 44 | new WebpackAssetsManifest( { 45 | done( manifest, stats ) { 46 | console.log( '\x1b[35m', `\n\nThe manifest has been written to ${manifest.getOutputPath()}` ); 47 | console.log( '\x1b[32m', `\n${manifest}\n\n` ); 48 | } 49 | } ), 50 | 51 | new StyleLintPlugin( { 52 | 'extends': 'stylelint-config-wordpress/scss' 53 | } ), 54 | 55 | new FriendlyErrorsPlugin( { 56 | clearConsole: false 57 | } ) 58 | ]; 59 | 60 | const rules = [ 61 | { 62 | enforce: 'pre', 63 | test: /\.(js|jsx)$/, 64 | exclude: /node_modules/, 65 | use: 'eslint-loader' 66 | }, 67 | { 68 | test: /\.js$/, 69 | include: [ JSDir ], 70 | exclude: /node_modules/, 71 | use: 'babel-loader' 72 | }, 73 | { 74 | test: /\.scss$/, 75 | exclude: /node_modules/, 76 | use: [ 77 | MiniCssExtractPlugin.loader, 78 | 'css-loader', 79 | 'sass-loader' 80 | ] 81 | }, 82 | { 83 | test: /\.(png|jpg|svg|jpeg|gif|ico)$/, 84 | exclude: [ FONTS_DIR, /node_modules/ ], 85 | use: { 86 | loader: 'file-loader', 87 | options: { 88 | name: '[path][name].[ext]', 89 | publicPath: '../' 90 | } 91 | } 92 | }, 93 | { 94 | test: /\.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/, 95 | exclude: [ IMG_DIR, /node_modules/ ], 96 | use: { 97 | loader: 'file-loader', 98 | options: { 99 | name: '[path][name].[ext]', 100 | publicPath: '../' 101 | } 102 | } 103 | } 104 | ]; 105 | 106 | const optimization = [ 107 | new OptimizeCssAssetsPlugin( { 108 | cssProcessor: cssnano 109 | } ), 110 | 111 | new UglifyJsPlugin( { 112 | cache: false, 113 | parallel: true, 114 | sourceMap: false 115 | } ) 116 | ]; 117 | 118 | module.exports = ( env, argv ) => ( { 119 | entry: entry, 120 | output: output, 121 | plugins: plugins( argv ), 122 | devtool: 'source-map', 123 | 124 | module: { 125 | 'rules': rules 126 | }, 127 | 128 | optimization: { 129 | minimizer: optimization 130 | }, 131 | 132 | externals: { 133 | jquery: 'jQuery' 134 | } 135 | } ); 136 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "imranhsayed/headless-cms", 3 | "description": "A WordPress plugin that adds features to use WordPress as a headless CMS with any front-end environment using REST API", 4 | "type": "wordpress-plugin", 5 | "license": "GPL-3.0+", 6 | "version": "2.0.3", 7 | "keywords": [ 8 | "wordpress", 9 | "plugin" 10 | ], 11 | "require": { 12 | "php": ">=5.4.0" 13 | }, 14 | "homepage": "https://github.com/imranhsayed/headless-cms", 15 | "authors": [ 16 | { 17 | "name": "Imran Sayed", 18 | "email": "codeytek.academy@gmail.com" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /headless-cms.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | // If the site url is same as home url, then don't make any updates. 34 | if ( get_site_url() === get_home_url() ) { 35 | return null; 36 | } 37 | 38 | /** 39 | * Update site and home URLs for rest api call. 40 | */ 41 | add_filter( 'rest_url', [ $this, 'force_update_rest_url' ] ); 42 | 43 | /** 44 | * Update site and home URLs for rest api call for WooCommerce. 45 | * 46 | * @see https://github.com/woocommerce/woocommerce/blob/0c44ab857a9b041727ab8c16fd182ee9b700818e/includes/class-wc-rest-authentication.php#L41 47 | */ 48 | add_action( 'determine_current_user', [ $this, 'update_site_and_home_url' ], 1, 1 ); 49 | 50 | /** 51 | * Reset the site and home URLs after authentication rest api call for WooCommerce. 52 | */ 53 | add_action( 'determine_current_user', [ $this, 'reset_site_and_home_url' ], 20, 1 ); 54 | 55 | } 56 | 57 | /** 58 | * Update the home and site url for the REST api authentication call. 59 | * 60 | * This is required because check_oauth_signature() of WooCommerce uses get_home_url to verify the signature. 61 | * At that point, the correct site url is not available. So we modify the URL to the backend url and then 62 | * later reset it back after the function completes it's work. 63 | * 64 | * @see https://github.com/woocommerce/woocommerce/blob/0c44ab857a9b041727ab8c16fd182ee9b700818e/includes/class-wc-rest-authentication.php#L390 65 | * 66 | * @param int $user_id User ID. 67 | * 68 | * @return mixed 69 | */ 70 | function update_site_and_home_url( $user_id ) { 71 | 72 | add_filter( 'home_url', [ $this, 'update_urls_callback' ], 1, 2 ); 73 | add_filter( 'site_url', [ $this, 'update_urls_callback' ], 1, 2 ); 74 | 75 | return $user_id; 76 | } 77 | 78 | /** 79 | * Update URL Callback. 80 | * 81 | * @param $url 82 | * @param $path 83 | * 84 | * @return string 85 | */ 86 | public function update_urls_callback( $url, $path ) { 87 | 88 | $url = get_option('siteurl'); 89 | 90 | return $url . $path; 91 | } 92 | 93 | /** 94 | * Reset the site and home URLs after authentication rest api call. 95 | * 96 | * @param int $user_id User id 97 | */ 98 | public function reset_site_and_home_url( $user_id ) { 99 | 100 | remove_filter( 'home_url', [ $this, 'update_urls_callback' ], 1 ); 101 | remove_filter( 'site_url', [ $this, 'update_urls_callback' ], 1 ); 102 | 103 | return $user_id; 104 | } 105 | 106 | /** 107 | * Customize rest base url. 108 | * When we set site address url to frontend url, by default wp rest endpoint gets that frontend url as base point. 109 | * So we must update that to actual backend url. 110 | * 111 | * @param string $url url of the backend. 112 | * 113 | * @return string backend url. 114 | */ 115 | public function force_update_rest_url( $url ) { 116 | return str_replace( get_home_url(), get_site_url(), $url ); 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /inc/classes/api/class-get-post.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 28 | } 29 | 30 | /** 31 | * To setup action/filter. 32 | * 33 | * @return void 34 | */ 35 | protected function setup_hooks() { 36 | 37 | $this->post_type = 'post'; 38 | $this->route = '/post'; 39 | 40 | add_action( 'rest_api_init', [ $this, 'rest_posts_endpoints' ] ); 41 | 42 | } 43 | 44 | /** 45 | * Register posts endpoints. 46 | */ 47 | public function rest_posts_endpoints() { 48 | 49 | /** 50 | * Handle Posts Request: GET Request 51 | * 52 | * This endpoint takes 'post_id' or 'post_slug' in query params of the request. 53 | * Returns the posts data object on success 54 | * Also handles error by returning the relevant error. 55 | * 56 | * Example: http://example.com/wp-json/rae/v1/post?post_id=1 57 | * http://example.com/wp-json/rae/v1/post?post_slug=hello-world 58 | */ 59 | register_rest_route( 60 | 'rae/v1', 61 | $this->route, 62 | [ 63 | 'method' => 'GET', 64 | 'callback' => [ $this, 'rest_endpoint_handler' ], 65 | 'permission_callback' => '__return_true', 66 | ] 67 | ); 68 | } 69 | 70 | /** 71 | * Get posts call back. 72 | * 73 | * Returns the posts data object on success 74 | * 75 | * @param WP_REST_Request $request request object. 76 | * 77 | * @return WP_Error|WP_REST_Response response object. 78 | */ 79 | public function rest_endpoint_handler( WP_REST_Request $request ) { 80 | $response = []; 81 | $parameters = $request->get_params(); 82 | $post_id = ! empty( $parameters['post_id'] ) ? intval( sanitize_text_field( $parameters['post_id'] ) ) : ''; 83 | $post_slug = ! empty( $parameters['post_slug'] ) ? sanitize_text_field( $parameters['post_slug'] ) : ''; 84 | 85 | // Error Handling. 86 | $error = new WP_Error(); 87 | 88 | // Get id from slug 89 | if ( ! empty( $post_slug ) ) { 90 | $the_post = get_page_by_path( $post_slug, OBJECT, 'post' ); 91 | $post_id = $the_post instanceof WP_Post ? $the_post->ID : $post_id; 92 | } 93 | 94 | $post_data = $this->get_required_post_data( $post_id ); 95 | 96 | // If posts found. 97 | if ( ! empty( $post_data ) ) { 98 | 99 | $response['status'] = 200; 100 | $response['post_data'] = $post_data; 101 | 102 | } else { 103 | 104 | // If the posts not found. 105 | $error->add( 406, __( 'Post not found', 'rest-api-endpoints' ) ); 106 | 107 | return $error; 108 | 109 | } 110 | 111 | return new WP_REST_Response( $response ); 112 | 113 | } 114 | 115 | /** 116 | * Construct a post data that contains, title, excerpt and featured image. 117 | * 118 | * @param {array} $post_ID post id. 119 | * 120 | * @return array 121 | */ 122 | public function get_required_post_data( $post_ID ) { 123 | 124 | $post_data = []; 125 | 126 | if ( empty( $post_ID ) && ! is_array( $post_ID ) ) { 127 | return $post_data; 128 | } 129 | 130 | $author_id = get_post_field( 'post_author', $post_ID ); 131 | $attachment_id = get_post_thumbnail_id( $post_ID ); 132 | 133 | $post_data = []; 134 | $post_data['id'] = $post_ID; 135 | $post_data['title'] = get_the_title( $post_ID ); 136 | $post_data['excerpt'] = get_the_excerpt( $post_ID ); 137 | $post_data['date'] = get_the_date( '', $post_ID ); 138 | $post_data['slug'] = get_post_field( 'post_name', $post_ID ); 139 | $post_data['permalink'] = get_the_permalink( $post_ID ); 140 | $post_data['content'] = get_post_field( 'post_content', $post_ID ); 141 | $post_data['attachment_image'] = [ 142 | 'img_sizes' => wp_get_attachment_image_sizes( $attachment_id ), 143 | 'img_src' => wp_get_attachment_image_src( $attachment_id, 'full' ), 144 | 'img_srcset' => wp_get_attachment_image_srcset( $attachment_id ), 145 | ]; 146 | $post_data['categories'] = get_the_category( $post_ID ); 147 | $post_data['meta'] = [ 148 | 'author_id' => $author_id, 149 | 'author_name' => get_the_author_meta( 'display_name', $author_id ), 150 | 'author_url' => get_author_posts_url( $author_id ), 151 | ]; 152 | 153 | 154 | return $post_data; 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /inc/classes/api/class-get-posts.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 28 | } 29 | 30 | /** 31 | * To setup action/filter. 32 | * 33 | * @return void 34 | */ 35 | protected function setup_hooks() { 36 | 37 | $this->post_type = 'post'; 38 | $this->route = '/posts'; 39 | $this->post_per_page = 9; 40 | 41 | add_action( 'rest_api_init', [ $this, 'rest_posts_endpoints' ] ); 42 | 43 | } 44 | 45 | /** 46 | * Register posts endpoints. 47 | */ 48 | public function rest_posts_endpoints() { 49 | 50 | /** 51 | * Handle Posts Request: GET Request 52 | * 53 | * This endpoint takes 'page_no' in query params of the request. 54 | * Returns the posts data object on success 55 | * Also handles error by returning the relevant error. 56 | * 57 | * Example: http://example.com/wp-json/rae/v1/posts?page_no=1 58 | */ 59 | register_rest_route( 60 | 'rae/v1', 61 | $this->route, 62 | [ 63 | 'method' => 'GET', 64 | 'callback' => [ $this, 'rest_endpoint_handler' ], 65 | 'permission_callback' => '__return_true', 66 | ] 67 | ); 68 | } 69 | 70 | /** 71 | * Get posts call back. 72 | * 73 | * Returns the posts data object on success 74 | * 75 | * @param WP_REST_Request $request request object. 76 | * 77 | * @return WP_Error|WP_REST_Response response object. 78 | */ 79 | public function rest_endpoint_handler( WP_REST_Request $request ) { 80 | $response = []; 81 | $parameters = $request->get_params(); 82 | $posts_page_no = ! empty( $parameters['page_no'] ) ? intval( sanitize_text_field( $parameters['page_no'] ) ) : ''; 83 | 84 | // Error Handling. 85 | $error = new WP_Error(); 86 | 87 | $posts_data = $this->get_posts( $posts_page_no ); 88 | 89 | // If posts found. 90 | if ( ! empty( $posts_data['posts_data'] ) ) { 91 | 92 | $response['status'] = 200; 93 | $response['posts_data'] = $posts_data['posts_data']; 94 | $response['found_posts'] = $posts_data['found_posts']; 95 | $response['page_count'] = $posts_data['page_count']; 96 | 97 | } else { 98 | 99 | // If the posts not found. 100 | $error->add( 406, __( 'Posts not found', 'rest-api-endpoints' ) ); 101 | 102 | return $error; 103 | 104 | } 105 | 106 | return new WP_REST_Response( $response ); 107 | 108 | } 109 | 110 | /** 111 | * Calculate page count. 112 | * 113 | * @param int $total_found_posts Total posts found. 114 | * @param int $post_per_page Post per page count. 115 | * 116 | * @return int 117 | */ 118 | public function calculate_page_count( $total_found_posts, $post_per_page ) { 119 | return ( (int) ( $total_found_posts / $post_per_page ) + ( ( $total_found_posts % $post_per_page ) ? 1 : 0 ) ); 120 | } 121 | 122 | 123 | /** 124 | * Get posts data. 125 | * 126 | * @param integer $page_no page no. 127 | * 128 | * @return array Posts. 129 | */ 130 | public function get_posts( $page_no = 1 ) { 131 | 132 | $args = [ 133 | 'post_type' => $this->post_type, 134 | 'post_status' => 'publish', 135 | 'posts_per_page' => $this->post_per_page, 136 | 'fields' => 'ids', 137 | 'orderby' => 'date', 138 | 'paged' => $page_no, 139 | 'update_post_meta_cache' => false, 140 | 'update_post_term_cache' => false, 141 | 142 | ]; 143 | 144 | $latest_post_ids = new WP_Query( $args ); 145 | 146 | $post_result = $this->get_required_posts_data( $latest_post_ids->posts ); 147 | $found_posts = $latest_post_ids->found_posts; 148 | $page_count = $this->calculate_page_count( $found_posts, $this->post_per_page ); 149 | 150 | return [ 151 | 'posts_data' => $post_result, 152 | 'found_posts' => $found_posts, 153 | 'page_count' => $page_count, 154 | 155 | ]; 156 | } 157 | 158 | /** 159 | * Construct a post array that contains, title, excerpt and featured image. 160 | * 161 | * @param {array} $post_ids post ids. 162 | * 163 | * @return array 164 | */ 165 | public function get_required_posts_data( $post_ids ) { 166 | 167 | $post_result = []; 168 | 169 | if ( empty( $post_ids ) && ! is_array( $post_ids ) ) { 170 | return $post_result; 171 | } 172 | 173 | foreach ( $post_ids as $post_ID ) { 174 | 175 | $author_id = get_post_field( 'post_author', $post_ID ); 176 | $attachment_id = get_post_thumbnail_id( $post_ID ); 177 | 178 | $post_data = []; 179 | $post_data['id'] = $post_ID; 180 | $post_data['title'] = get_the_title( $post_ID ); 181 | $post_data['excerpt'] = get_the_excerpt( $post_ID ); 182 | $post_data['date'] = get_the_date( '', $post_ID ); 183 | $post_data['slug'] = get_post_field( 'post_name', $post_ID ); 184 | $post_data['permalink'] = get_the_permalink($post_ID); 185 | $post_data['attach_id'] = $attachment_id; 186 | $post_data['attachment_image'] = [ 187 | 'img_sizes' => wp_get_attachment_image_sizes( $attachment_id ), 188 | 'img_src' => wp_get_attachment_image_src( $attachment_id, 'full' ), 189 | 'img_srcset' => wp_get_attachment_image_srcset( $attachment_id ), 190 | ]; 191 | $post_data['categories'] = get_the_category( $post_ID ); 192 | $post_data['meta'] = [ 193 | 'author_id' => $author_id, 194 | 'author_name' => get_the_author_meta( 'display_name', $author_id ), 195 | 'author_url' => get_author_posts_url( $author_id ), 196 | ]; 197 | 198 | array_push( $post_result, $post_data ); 199 | 200 | } 201 | 202 | return $post_result; 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /inc/classes/api/class-header-footer-api.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 27 | } 28 | 29 | /** 30 | * To setup action/filter. 31 | * 32 | * @return void 33 | */ 34 | protected function setup_hooks() { 35 | 36 | $this->route = '/header-footer'; 37 | 38 | /** 39 | * Action 40 | */ 41 | add_action( 'rest_api_init', [ $this, 'rest_posts_endpoints' ] ); 42 | 43 | } 44 | 45 | /** 46 | * Register posts endpoints. 47 | */ 48 | public function rest_posts_endpoints() { 49 | 50 | /** 51 | * Handle Posts Request: GET Request 52 | * 53 | * This api gets the header and footer of the site. 54 | * The data will include: 55 | * 1. header data ( siteTitle, siteDescription, siteLogoUrl, favicon, headerMenuItems( Header menu with the given menu location id ) ) 56 | * 2. footer data ( copyrightText, sidebarOne( widget ), sidebarTwo ( Widget ), socialLinks, footerMenuItems( Footer menu with the given menu location id ) ) 57 | * 58 | * The 'header_location_id' here is a string e.g. 'primary' or whatever 'header_location_id' name you have used at the time of registration of the menu. 59 | * 60 | * Example: http://example.com/wp-json/rae/v1/header-footer?header_location_id=primary&footer_location_id=secondary 61 | */ 62 | register_rest_route( 63 | 'rae/v1', 64 | $this->route, 65 | [ 66 | 'method' => 'GET', 67 | 'callback' => [ $this, 'rest_endpoint_handler' ], 68 | 'permission_callback' => '__return_true', 69 | ] 70 | ); 71 | } 72 | 73 | /** 74 | * Get posts call back. 75 | * 76 | * Returns the menu items array of object on success 77 | * 78 | * @param WP_REST_Request $request request object. 79 | * 80 | * @return WP_Error|WP_REST_Response response object. 81 | */ 82 | public function rest_endpoint_handler( WP_REST_Request $request ) { 83 | $response = []; 84 | $parameters = $request->get_params(); 85 | $header_menu_location_id = ! empty( $parameters['header_location_id'] ) ? sanitize_text_field( $parameters['header_location_id'] ) : ''; 86 | $footer_menu_location_id = ! empty( $parameters['footer_location_id'] ) ? sanitize_text_field( $parameters['footer_location_id'] ) : ''; 87 | 88 | // Error Handling. 89 | $error = new WP_Error(); 90 | 91 | $header_menu_items = $this->get_nav_menu_items( $header_menu_location_id ); 92 | $footer_menu_items = $this->get_nav_menu_items( $footer_menu_location_id ); 93 | 94 | // If any menus found. 95 | if ( ! empty( $header_menu_items ) || ! empty( $footer_menu_items ) ) { 96 | 97 | $response['status'] = 200; 98 | $response['data'] = [ 99 | 'header' => [ 100 | 'siteLogoUrl' => $this->get_custom_logo_url( 'custom_logo' ), 101 | 'siteTitle' => get_bloginfo( 'title' ), 102 | 'siteDescription' => get_bloginfo( 'description' ), 103 | 'favicon' => get_site_icon_url(), 104 | 'headerMenuItems' => $header_menu_items, 105 | ], 106 | 'footer' => [ 107 | 'footerMenuItems' => $footer_menu_items, 108 | 'socialLinks' => $this->get_social_icons(), 109 | 'copyrightText' => $this->get_copyright_text(), 110 | 'sidebarOne' => $this->get_sidebar( 'hcms-footer-sidebar-1' ), 111 | 'sidebarTwo' => $this->get_sidebar( 'hcms-footer-sidebar-2' ), 112 | ], 113 | ]; 114 | 115 | } else { 116 | 117 | // If the posts not found. 118 | $error->add( 406, __( 'Data not found', 'rest-api-endpoints' ) ); 119 | 120 | return $error; 121 | 122 | } 123 | 124 | return new WP_REST_Response( $response ); 125 | 126 | } 127 | 128 | /** 129 | * Get Custom logo URL. 130 | * 131 | * @param string $key Key. 132 | * 133 | * @return string Image. 134 | */ 135 | public function get_custom_logo_url( $key ) { 136 | 137 | $custom_logo_id = get_theme_mod( $key ); 138 | $image = wp_get_attachment_image_src( $custom_logo_id, 'full' ); 139 | return $image[0]; 140 | } 141 | 142 | /** 143 | * Get social icons 144 | * 145 | * @return array $social_icons 146 | */ 147 | public function get_social_icons() { 148 | 149 | $social_icons = []; 150 | $social_icons_name = [ 'facebook', 'twitter', 'instagram', 'youtube' ]; 151 | 152 | foreach ( $social_icons_name as $social_icon_name ) { 153 | 154 | $social_link = get_theme_mod( sprintf( 'rae_%s_link', $social_icon_name ) ); 155 | 156 | if ( $social_link ) { 157 | array_push( 158 | $social_icons, 159 | [ 160 | 'iconName' => esc_attr( $social_icon_name ), 161 | 'iconUrl' => esc_url( $social_link ), 162 | ] 163 | ); 164 | } 165 | } 166 | 167 | return $social_icons; 168 | 169 | } 170 | 171 | /** 172 | * Get copyright text 173 | * 174 | * @return mixed 175 | */ 176 | public function get_copyright_text() { 177 | return get_theme_mod( 'rae_footer_text' ); 178 | } 179 | 180 | /** 181 | * Get nav menu items by location. 182 | * 183 | * @param string $location The menu location id. 184 | * @param array $args Arguments. 185 | * 186 | * @return array $menu_data Menu items array of Objects. 187 | */ 188 | public function get_nav_menu_items( $location, $args = [] ) { 189 | 190 | if ( empty( $location ) ) { 191 | return ''; 192 | } 193 | 194 | // Get all locations. 195 | $locations = get_nav_menu_locations(); 196 | 197 | // Get object id by location. 198 | $object = wp_get_nav_menu_object( $locations[ $location ] ); 199 | 200 | // Get menu items by menu name. 201 | $menu_data = wp_get_nav_menu_items( $object->name, $args ); 202 | $menu_items = []; 203 | 204 | if ( ! empty( $menu_data ) ) { 205 | 206 | // Menus ( Loop through the menu, and push all the parent menu items first ). 207 | foreach ( $menu_data as $item ) { 208 | if ( empty( $item->menu_item_parent ) ) { 209 | $menu_item = []; 210 | $menu_item['ID'] = $item->ID; 211 | $menu_item['title'] = $item->title; 212 | $menu_item['url'] = $item->url; 213 | $menu_item['children'] = []; 214 | 215 | // We are also getting the page slug and the page id that this menu is linked to. 216 | $menu_item['pageSlug'] = get_post_field( 'post_name', $item->object_id ); 217 | $menu_item['pageID'] = intval( $item->object_id ); 218 | 219 | array_push( $menu_items, $menu_item ); 220 | } 221 | } 222 | 223 | // Submenus: ( Loop through the menu again, and push all the child menu items ). 224 | foreach ( $menu_data as $item ) { 225 | 226 | // If the menu has a parent, it means its a child menu. 227 | if ( $item->menu_item_parent ) { 228 | 229 | // Create a child menu array. 230 | $submenu_item = []; 231 | $submenu_item['ID'] = $item->ID; 232 | $submenu_item['title'] = $item->title; 233 | $submenu_item['url'] = $item->url; 234 | 235 | // We are also getting the page slug and the page id that this menu is linked to. 236 | $submenu_item['pageSlug'] = get_post_field( 'post_name', $item->object_id ); 237 | $submenu_item['pageID'] = intval( $item->object_id ); 238 | 239 | // Loop through the menu items and find the parent whose child this is. 240 | foreach ( $menu_items as $key => $parent_item ) { 241 | 242 | // if the parent id of this child menu, is same as the parent menu id. 243 | if ( intval( $item->menu_item_parent ) === $parent_item['ID'] ) { 244 | 245 | // push the child menu into its parent menu children property. 246 | array_push( $menu_items[ $key ]['children'], $submenu_item ); 247 | 248 | } 249 | } 250 | } 251 | } 252 | } 253 | 254 | $menu_items = ! empty( $menu_items ) ? $menu_items : ''; 255 | 256 | // Return menu post objects. 257 | return $menu_items; 258 | 259 | } 260 | 261 | /** 262 | * Returns the content of all the sidebars with given sidebar id. 263 | * 264 | * @param string $sidebar_id Sidebar id. 265 | * 266 | * @return false|string 267 | */ 268 | public function get_sidebar( $sidebar_id ) { 269 | ob_start(); 270 | 271 | dynamic_sidebar( $sidebar_id ); 272 | $output = ob_get_contents(); 273 | 274 | ob_end_clean(); 275 | 276 | return $output; 277 | } 278 | 279 | } 280 | -------------------------------------------------------------------------------- /inc/classes/api/class-home-page.php: -------------------------------------------------------------------------------- 1 | plugin_options = get_option( 'hcms_plugin_options' ); 29 | $this->setup_hooks(); 30 | 31 | } 32 | 33 | /** 34 | * To setup action/filter. 35 | * 36 | * @return void 37 | */ 38 | protected function setup_hooks() { 39 | 40 | $this->route = '/home'; 41 | 42 | /** 43 | * Action 44 | */ 45 | add_action( 'rest_api_init', [ $this, 'rest_posts_endpoints' ] ); 46 | 47 | } 48 | 49 | /** 50 | * Register posts endpoints. 51 | */ 52 | public function rest_posts_endpoints() { 53 | 54 | /** 55 | * Handle Posts Request: GET Request 56 | * 57 | * This api gets the custom home page data for the site. 58 | * The data will include: 59 | * 1. Hero section data ( Title, description, button name ) 60 | * 2. Search section data ( Search placeholder text, three lates taxonomies, with given taxonomy name passed in query params of URL, defaults to 'category' ) 61 | * 3. Featured post data ( heading, 3 featured posts selected from plugin settings page ) 62 | * 4. Latest posts ( Heading and 3 latest posts, with given post type passed in query params of URL, defaults to 'post' ) 63 | * 64 | * The 'post_type' here is a string e.g. 'post', The 'taxonomy' here is a string e.g. 'category' 65 | * 66 | * Example: http://example.com/wp-json/rae/v2/home?post_type=post&taxonomy=category 67 | */ 68 | register_rest_route( 69 | 'rae/v1', 70 | $this->route, 71 | [ 72 | 'methods' => 'GET', 73 | 'callback' => [ $this, 'rest_endpoint_handler' ], 74 | 'permission_callback' => '__return_true', 75 | ] 76 | ); 77 | } 78 | 79 | /** 80 | * Get posts call back. 81 | * 82 | * Returns the menu items array of object on success 83 | * 84 | * @param WP_REST_Request $request request object. 85 | * 86 | * @return WP_Error|WP_REST_Response response object. 87 | */ 88 | public function rest_endpoint_handler( WP_REST_Request $request ) { 89 | 90 | $response = []; 91 | $parameters = $request->get_params(); 92 | $post_type = ! empty( $parameters['post_type'] ) ? sanitize_text_field( $parameters['post_type'] ) : 'post'; 93 | $taxonomy = ! empty( $parameters['taxonomy'] ) ? sanitize_text_field( $parameters['taxonomy'] ) : 'category'; 94 | 95 | // Error Handling. 96 | $error = new WP_Error(); 97 | 98 | $hero_section_data = $this->get_hero_section(); 99 | $search_section_data = $this->get_search_section( $taxonomy ); 100 | $featured_posts = $this->get_featured_posts(); 101 | $latest_posts = $this->get_latest_posts( $post_type ); 102 | 103 | // If any menus found. 104 | if ( ! empty( $hero_section_data ) || ! empty( $search_section_data ) || ! empty( $featured_posts ) || ! empty( $latest_posts ) ) { 105 | 106 | $data = array( 107 | 'wordpress_id' => 220, // Use an id required for the GraphQL query. 108 | 'heroSection' => $hero_section_data, 109 | 'searchSection' => $search_section_data, 110 | 'featuredPostsSection' => $featured_posts, 111 | 'latestPostsSection' => $latest_posts, 112 | ); 113 | return new WP_REST_Response( $data, 200 ); 114 | 115 | } else { 116 | 117 | // If the posts not found. 118 | $error->add( 406, __( 'Data not found', 'rest-api-endpoints' ) ); 119 | 120 | return $error; 121 | 122 | } 123 | 124 | } 125 | 126 | /** 127 | * Get Hero Section data. 128 | * 129 | * @return array $hero_section_data Hero Section data 130 | */ 131 | public function get_hero_section() { 132 | 133 | if ( empty( $this->plugin_options ) ) { 134 | return []; 135 | } 136 | 137 | $hero_section_data = [ 138 | 'heroTitle' => $this->plugin_options['hero_title'], 139 | 'heroDescription' => $this->plugin_options['hero_description'], 140 | 'heroBtnTxt' => $this->plugin_options['hero_btn_text'], 141 | 'heroImgURL' => $this->plugin_options['hero_back_img'], 142 | ]; 143 | 144 | return $hero_section_data; 145 | } 146 | 147 | /** 148 | * Get Search Section data. 149 | * 150 | * @param string $taxonomy Taxonomy. 151 | * 152 | * @return array $search_section_data Hero Section data. 153 | */ 154 | public function get_search_section( $taxonomy ) { 155 | 156 | // Get latest three categories. 157 | $terms = get_terms( 158 | [ 159 | 'taxonomy' => $taxonomy, 160 | 'hide_empty' => false, 161 | 'number' => 3, 162 | 'parent' => '0', 163 | ] 164 | ); 165 | 166 | $terms_with_attach = $this->get_terms_with_attach( $terms ); 167 | 168 | $search_section_data = [ 169 | 'searchPlaceholderTxt' => $this->plugin_options['search_placeholder_text'], 170 | 'searchBackURL' => $this->plugin_options['search_back_img'], 171 | 'terms' => $terms_with_attach, 172 | ]; 173 | 174 | return $search_section_data; 175 | } 176 | 177 | /** 178 | * Get terms with attachment image 179 | * 180 | * @param array $terms Terms. 181 | */ 182 | public function get_terms_with_attach( $terms ) { 183 | 184 | $terms_with_attach = []; 185 | 186 | if ( ! empty( $terms ) ) { 187 | foreach ( $terms as $term ) { 188 | 189 | $attachment_id_data = get_term_meta( $term->term_id, 'category-image-id' ); 190 | $attachment_id = $attachment_id_data[0]; 191 | 192 | $term_data = [ 193 | 'termId' => $term->term_id, 194 | 'name' => $term->name, 195 | 'slug' => $term->slug, 196 | 'taxonomy' => $term->taxonomy, 197 | 'image' => [ 198 | 'img_sizes' => wp_get_attachment_image_sizes( $attachment_id ), 199 | 'img_src' => wp_get_attachment_image_src( $attachment_id, 'full' ), 200 | 'img_srcset' => wp_get_attachment_image_srcset( $attachment_id ), 201 | ], 202 | ]; 203 | 204 | array_push( $terms_with_attach, $term_data ); 205 | } 206 | } 207 | 208 | return $terms_with_attach; 209 | } 210 | 211 | /** 212 | * Get featured Posts. 213 | * 214 | * @return array $featured_posts Featured Posts. 215 | */ 216 | public function get_featured_posts() { 217 | 218 | if ( empty( $this->plugin_options ) ) { 219 | return []; 220 | } 221 | 222 | $featured_post_ids = [ 223 | intval( $this->plugin_options['first_featured_post_id'] ), 224 | intval( $this->plugin_options['second_featured_post_id'] ), 225 | intval( $this->plugin_options['third_featured_post_id'] ), 226 | ]; 227 | 228 | $featured_posts = []; 229 | 230 | if ( ! empty( $featured_post_ids ) && is_array( $featured_post_ids ) ) { 231 | foreach ( $featured_post_ids as $post_ID ) { 232 | 233 | $author_id = get_post_field( 'post_author', $post_ID ); 234 | $attachment_id = get_post_thumbnail_id( $post_ID ); 235 | 236 | $post_data = []; 237 | $post_data['id'] = $post_ID; 238 | $post_data['title'] = get_the_title( $post_ID ); 239 | $post_data['excerpt'] = get_the_excerpt( $post_ID ); 240 | $post_data['slug'] = get_post_field( 'post_name', $post_ID ); 241 | $post_data['date'] = get_the_date( '', $post_ID ); 242 | $post_data['attachment_image'] = [ 243 | 'img_sizes' => wp_get_attachment_image_sizes( $attachment_id ), 244 | 'img_src' => wp_get_attachment_image_src( $attachment_id, 'full' ), 245 | 'img_srcset' => wp_get_attachment_image_srcset( $attachment_id ), 246 | ]; 247 | $post_data['meta'] = [ 248 | 'author_id' => $author_id, 249 | 'author_name' => get_the_author_meta( 'display_name', $author_id ), 250 | ]; 251 | 252 | array_push( $featured_posts, $post_data ); 253 | 254 | } 255 | } 256 | 257 | return [ 258 | 'featuredPostHeading' => $this->plugin_options['featured_post_heading'], 259 | 'featuredPosts' => $featured_posts, 260 | ]; 261 | 262 | } 263 | 264 | /** 265 | * Get latest posts 266 | * 267 | * @param string $post_type Post Type. 268 | * 269 | * @return array latest posts 270 | */ 271 | public function get_latest_posts( $post_type ) { 272 | 273 | $args = [ 274 | 'post_type' => $post_type, 275 | 'post_status' => 'publish', 276 | 'posts_per_page' => 3, // Get three posts. 277 | 'fields' => 'ids', 278 | 'orderby' => 'date', 279 | 'update_post_meta_cache' => false, 280 | 'update_post_term_cache' => false, 281 | 282 | ]; 283 | 284 | $result = new WP_Query( $args ); 285 | 286 | $latest_post_ids = $result->get_posts(); 287 | 288 | $latest_posts = []; 289 | 290 | if ( ! empty( $latest_post_ids ) && is_array( $latest_post_ids ) ) { 291 | foreach ( $latest_post_ids as $post_ID ) { 292 | 293 | $attachment_id = get_post_thumbnail_id( $post_ID ); 294 | 295 | $post_data = []; 296 | $post_data['id'] = $post_ID; 297 | $post_data['title'] = get_the_title( $post_ID ); 298 | $post_data['excerpt'] = get_the_excerpt( $post_ID ); 299 | $post_data['attachment_image'] = [ 300 | 'img_sizes' => wp_get_attachment_image_sizes( $attachment_id ), 301 | 'img_src' => wp_get_attachment_image_src( $attachment_id, 'full' ), 302 | 'img_srcset' => wp_get_attachment_image_srcset( $attachment_id ), 303 | ]; 304 | 305 | array_push( $latest_posts, $post_data ); 306 | 307 | } 308 | } 309 | 310 | return [ 311 | 'latestPostHeading' => ! empty( $this->plugin_options['latest_post_heading'] ) ? $this->plugin_options['latest_post_heading'] : '', 312 | 'latestPosts' => $latest_posts, 313 | ]; 314 | } 315 | 316 | } 317 | -------------------------------------------------------------------------------- /inc/classes/api/class-post-by-tax.php: -------------------------------------------------------------------------------- 1 | plugin_options = get_option( 'hcms_plugin_options' ); 29 | $this->setup_hooks(); 30 | 31 | } 32 | 33 | /** 34 | * To setup action/filter. 35 | * 36 | * @return void 37 | */ 38 | protected function setup_hooks() { 39 | 40 | $this->route = '/posts-by-tax'; 41 | 42 | /** 43 | * Action 44 | */ 45 | add_action( 'rest_api_init', [ $this, 'rest_posts_endpoints' ] ); 46 | 47 | } 48 | 49 | /** 50 | * Register posts endpoints. 51 | */ 52 | public function rest_posts_endpoints() { 53 | 54 | /** 55 | * Handle Posts Request: GET Request 56 | * 57 | * This api gets the custom home page data for the site. 58 | * The data will include: 59 | * Latest posts ( Latest posts, with given post type passed in query params of URL, defaults to 'post', and given taxonomy ) 60 | * 61 | * The 'post_type' here is a string e.g. 'post', The 'taxonomy' here is a string e.g. 'category' 62 | * 63 | * Example: http://example.com/wp-json/rae/v1/posts-by-tax?post_type=post&taxonomy=category&slug=xyz 64 | */ 65 | register_rest_route( 66 | 'rae/v1', 67 | $this->route, 68 | [ 69 | 'method' => 'GET', 70 | 'callback' => [ $this, 'rest_endpoint_handler' ], 71 | 'permission_callback' => '__return_true', 72 | ] 73 | ); 74 | } 75 | 76 | /** 77 | * Get posts call back. 78 | * 79 | * Returns the menu items array of object on success 80 | * 81 | * @param WP_REST_Request $request request object. 82 | * 83 | * @return WP_Error|WP_REST_Response response object. 84 | */ 85 | public function rest_endpoint_handler( WP_REST_Request $request ) { 86 | 87 | $response = []; 88 | $parameters = $request->get_params(); 89 | $post_type = ! empty( $parameters['post_type'] ) ? sanitize_text_field( $parameters['post_type'] ) : 'post'; 90 | $taxonomy = ! empty( $parameters['taxonomy'] ) ? sanitize_text_field( $parameters['taxonomy'] ) : 'category'; 91 | $slug = ! empty( $parameters['slug'] ) ? sanitize_text_field( $parameters['slug'] ) : ''; 92 | 93 | // Error Handling. 94 | $error = new WP_Error(); 95 | 96 | $latest_posts = $this->get_latest_posts( $post_type, $taxonomy, $slug ); 97 | 98 | // If any menus found. 99 | if ( ! empty( $hero_section_data ) || ! empty( $search_section_data ) || ! empty( $featured_posts ) || ! empty( $latest_posts ) ) { 100 | 101 | $response['status'] = 200; 102 | $response['data'] = [ 103 | 'posts' => $latest_posts, 104 | ]; 105 | 106 | } else { 107 | 108 | // If the posts not found. 109 | $error->add( 406, __( 'Data not found', 'rest-api-endpoints' ) ); 110 | 111 | return $error; 112 | 113 | } 114 | 115 | return new WP_REST_Response( $response ); 116 | 117 | } 118 | 119 | /** 120 | * Get latest posts 121 | * 122 | * @param string $post_type Post Type. 123 | * @param string $taxonomy Taxnomy. 124 | * @param string $slug Slug. 125 | * 126 | * @return array latest posts 127 | */ 128 | public function get_latest_posts( $post_type, $taxonomy, $slug ) { 129 | 130 | // Ignoring phps for taxonomy query as its required here. 131 | $args = [ 132 | 'post_type' => $post_type, 133 | 'post_status' => 'publish', 134 | 'posts_per_page' => 10, // Get three posts. 135 | 'fields' => 'ids', 136 | 'orderby' => 'date', 137 | 'update_post_meta_cache' => false, 138 | 'update_post_term_cache' => false, 139 | 'tax_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery 140 | [ 141 | 'taxonomy' => $taxonomy, 142 | 'field' => 'slug', 143 | 'terms' => $slug, 144 | ], 145 | ], 146 | 147 | ]; 148 | 149 | $result = new WP_Query( $args ); 150 | 151 | $latest_post_ids = $result->get_posts(); 152 | 153 | $latest_posts = []; 154 | 155 | if ( ! empty( $latest_post_ids ) && is_array( $latest_post_ids ) ) { 156 | foreach ( $latest_post_ids as $post_ID ) { 157 | 158 | $author_id = get_post_field( 'post_author', $post_ID ); 159 | $attachment_id = get_post_thumbnail_id( $post_ID ); 160 | 161 | $post_data = []; 162 | $post_data['id'] = $post_ID; 163 | $post_data['title'] = get_the_title( $post_ID ); 164 | $post_data['excerpt'] = get_the_excerpt( $post_ID ); 165 | $post_data['slug'] = get_post_field( 'post_name', $post_ID ); 166 | $post_data['content'] = get_the_content( null, false, $post_ID ); 167 | $post_data['date'] = get_the_date( '', $post_ID ); 168 | $post_data['attachment_image'] = [ 169 | 'img_sizes' => wp_get_attachment_image_sizes( $attachment_id ), 170 | 'img_src' => wp_get_attachment_image_src( $attachment_id, 'full' ), 171 | 'img_srcset' => wp_get_attachment_image_srcset( $attachment_id ), 172 | ]; 173 | $post_data['meta'] = [ 174 | 'author_id' => $author_id, 175 | 'author_name' => get_the_author_meta( 'display_name', $author_id ), 176 | ]; 177 | 178 | array_push( $latest_posts, $post_data ); 179 | 180 | } 181 | } 182 | 183 | return $latest_posts; 184 | 185 | } 186 | 187 | } 188 | -------------------------------------------------------------------------------- /inc/classes/api/class-wc-cart.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 43 | } 44 | 45 | /** 46 | * To setup action/filter. 47 | * 48 | * @return void 49 | */ 50 | protected function setup_hooks() { 51 | 52 | /** 53 | * Action 54 | */ 55 | add_action( 'init', [ $this, 'customize_rest_cors' ] ); 56 | add_action( 'rest_api_init', [ $this, 'rest_posts_endpoints' ] ); 57 | add_filter( 'rest_pre_dispatch', [ $this, 'check_rest_response' ], 10, 3 ); 58 | 59 | } 60 | 61 | /** 62 | * Customize Rest Cores. 63 | */ 64 | public function customize_rest_cors() { 65 | remove_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' ); 66 | add_filter( 'rest_pre_serve_request', [ $this, 'set_pre_serve_request_headers' ] ); 67 | } 68 | 69 | public function set_pre_serve_request_headers( $value ) { 70 | 71 | // Get Allowed Origin Header Value. 72 | $hcms_plugin_options = get_option( 'hcms_plugin_options' ); 73 | $allowed_origin = ! empty( $hcms_plugin_options['frontend_site_url'] ) ? $hcms_plugin_options['frontend_site_url'] : '*'; 74 | $access_control_allow_origin = sprintf( 'Access-Control-Allow-Origin: %s', $allowed_origin ); 75 | 76 | header( $access_control_allow_origin ); 77 | header( 'Access-Control-Allow-Headers: X-WC-Session, X-Headless-CMS, Content-Type, Authorization' ); 78 | header( 'Access-Control-Expose-Headers: X-WC-Session, X-Headless-CMS, X-WC-Cart-Totals, X-WC-Cart-TotalItems, X-WP-Total, X-WP-TotalPages', false ); 79 | } 80 | 81 | /** 82 | * Dispath rest response 83 | * 84 | * @return mixed 85 | * @since 1.0.0 86 | * 87 | */ 88 | public function check_rest_response( $response, $object, $request ) { 89 | if ( $this->wc_next_validate_boolean( $request->get_header( 'X-Headless-CMS' ) ) ) { 90 | 91 | // WC session cookie setter and getter. 92 | $cookie = apply_filters( 'woocommerce_cookie', 'wp_woocommerce_session_' . COOKIEHASH ); 93 | 94 | if ( $request->get_header( 'X-WC-Session' ) ) { 95 | $_COOKIE[ $cookie ] = urldecode( $request->get_header( 'X-WC-Session' ) ); 96 | } else { 97 | do_action( 'woocommerce_set_cart_cookies', true ); 98 | } 99 | 100 | $this->wc_load_cart(); 101 | } 102 | 103 | return $response; 104 | } 105 | 106 | /** 107 | * Validate a boolean variable 108 | * 109 | * @param mixed $var 110 | * 111 | * @return bool 112 | * @since 1.0.0 113 | * 114 | */ 115 | public function wc_next_validate_boolean( $var ) { 116 | return filter_var( $var, FILTER_VALIDATE_BOOLEAN ); 117 | } 118 | 119 | /** 120 | * Register posts endpoints. 121 | */ 122 | public function rest_posts_endpoints() { 123 | $item_schema = $this->get_default_item_schema(); 124 | 125 | register_rest_route( 126 | $this->namespace, 127 | '/' . $this->rest_base, 128 | [ 129 | [ 130 | 'methods' => WP_REST_Server::READABLE, 131 | 'callback' => [ $this, 'get_items' ], 132 | 'permission_callback' => '__return_true', 133 | ], 134 | 135 | [ 136 | 'methods' => WP_REST_Server::CREATABLE, 137 | 'callback' => [ $this, 'create_item' ], 138 | 'permission_callback' => '__return_true', 139 | 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::CREATABLE ), 140 | ], 141 | 142 | [ 143 | 'methods' => WP_REST_Server::DELETABLE, 144 | 'callback' => [ $this, 'clear_cart' ], 145 | 'permission_callback' => '__return_true', 146 | ], 147 | 148 | 'schema' => [ $this, 'get_public_item_schema' ], 149 | ] 150 | ); 151 | 152 | register_rest_route( 153 | $this->namespace, 154 | '/' . $this->rest_base . '/batch', 155 | [ 156 | [ 157 | 'methods' => WP_REST_Server::EDITABLE, 158 | 'callback' => [ $this, 'batch_items' ], 159 | 'permission_callback' => '__return_true', 160 | 'args' => $this->get_endpoint_args_for_item_schema( WP_REST_Server::EDITABLE ), 161 | ], 162 | 'schema' => [ $this, 'get_public_batch_schema' ], 163 | ] 164 | ); 165 | 166 | register_rest_route( 167 | $this->namespace, 168 | '/' . $this->rest_base . '/(?P[\w-]+)', 169 | [ 170 | 'args' => [ 171 | 'key' => [ 172 | 'description' => __( 'The cart item key is what identifies the item in the cart.', 'headless-cms' ), 173 | 'type' => 'string', 174 | 'validate_callback' => [ $this, 'is_valid_cart_item' ], 175 | ], 176 | ], 177 | 178 | [ 179 | 'methods' => WP_REST_Server::EDITABLE, 180 | 'callback' => [ $this, 'update_item' ], 181 | 'permission_callback' => '__return_true', 182 | 'args' => [ 183 | 'quantity' => $item_schema['properties']['quantity'], 184 | ], 185 | ], 186 | 187 | [ 188 | 'methods' => WP_REST_Server::DELETABLE, 189 | 'callback' => [ $this, 'delete_item' ], 190 | 'permission_callback' => '__return_true', 191 | ], 192 | 193 | 'schema' => [ $this, 'get_public_item_schema' ], 194 | ] 195 | ); 196 | 197 | register_rest_route( 198 | $this->namespace, 199 | '/' . $this->rest_base . '/(?P[\w-]+)/restore', 200 | [ 201 | 'args' => [ 202 | 'key' => [ 203 | 'description' => __( 'The cart item key is what identifies the item in the cart.', 'headless-cms' ), 204 | 'type' => 'string', 205 | 'validate_callback' => [ $this, 'is_valid_removed_cart_item' ], 206 | ], 207 | ], 208 | 209 | [ 210 | 'methods' => WP_REST_Server::EDITABLE, 211 | 'callback' => [ $this, 'restore_item' ], 212 | 'permission_callback' => '__return_true', 213 | ], 214 | ] 215 | ); 216 | } 217 | 218 | /** 219 | * Get the Cart schema, conforming to JSON Schema. 220 | * 221 | * @return array 222 | * @since 1.0.0 223 | * 224 | */ 225 | public function get_default_item_schema() { 226 | // @todo: Add more properties matches the /cart GET endpoint 227 | $schema = [ 228 | '$schema' => 'http://json-schema.org/draft-04/schema#', 229 | 'title' => 'cart', 230 | 'type' => 'object', 231 | 'properties' => [ 232 | 'key' => [ 233 | 'description' => __( 'Cart item key.', 'headless-cms' ), 234 | 'type' => 'string', 235 | 'context' => [ 'view', 'edit' ], 236 | 'readonly' => true, 237 | ], 238 | 'product_id' => [ 239 | 'description' => __( 'ID of the product to add to the cart.', 'headless-cms' ), 240 | 'type' => 'integer', 241 | 'context' => [ 'view', 'edit' ], 242 | 'default' => 0, 243 | 'arg_options' => [ 244 | 'validate_callback' => [ $this, 'is_valid_product' ], 245 | ], 246 | ], 247 | 'quantity' => [ 248 | 'description' => __( 'Quantity of the item to add.', 'headless-cms' ), 249 | 'type' => 'integer', 250 | 'context' => [ 'view', 'edit' ], 251 | 'default' => 1, 252 | 'arg_options' => [ 253 | 'validate_callback' => [ $this, 'is_valid_quantity' ], 254 | ], 255 | ], 256 | 'variation_id' => [ 257 | 'description' => __( 'ID of the variation being added to the cart.', 'headless-cms' ), 258 | 'type' => 'integer', 259 | 'context' => [ 'view', 'edit' ], 260 | 'default' => 0, 261 | 'arg_options' => [ 262 | 'validate_callback' => [ $this, 'is_valid_product' ], 263 | ], 264 | ], 265 | 'variation' => [ 266 | 'description' => __( 'Variation attribute values.', 'headless-cms' ), 267 | 'type' => 'object', 268 | 'context' => [ 'view', 'edit' ], 269 | 'default' => [], 270 | ], 271 | 'cart_item_data' => [ 272 | 'description' => __( 'Extra cart item data we want to pass into the item.', 'headless-cms' ), 273 | 'type' => 'object', 274 | 'context' => [ 'view', 'edit' ], 275 | 'default' => [], 276 | ], 277 | ], 278 | ]; 279 | 280 | return $this->add_additional_fields_schema( $schema ); 281 | } 282 | 283 | /** 284 | * Retrieves the item's schema for display / public consumption purposes. 285 | * 286 | * @return array Public item schema data. 287 | * @since 4.7.0 288 | * 289 | */ 290 | public function get_public_item_schema() { 291 | 292 | $schema = $this->get_item_schema(); 293 | 294 | if ( ! empty( $schema['properties'] ) ) { 295 | foreach ( $schema['properties'] as &$property ) { 296 | unset( $property['arg_options'] ); 297 | } 298 | } 299 | 300 | return $schema; 301 | } 302 | 303 | /** 304 | * Get cart items 305 | * 306 | * @param \WP_REST_Request $request 307 | * 308 | * @return \WP_REST_Response 309 | * @since 1.0.0 310 | * 311 | */ 312 | public function get_items( $request ) { 313 | $data = []; 314 | include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; 315 | include_once WC_ABSPATH . 'includes/class-wc-cart.php'; 316 | 317 | if ( is_null( WC()->cart ) ) { 318 | wc_load_cart(); 319 | } 320 | $cart = WC()->cart->get_cart(); 321 | 322 | foreach ( $cart as $item_key => &$cart_item ) { 323 | $data[] = $this->prepare_cart_item_for_response( $cart_item, $request ); 324 | } 325 | 326 | $response = rest_ensure_response( $data ); 327 | $response = $this->add_headers( $response ); 328 | 329 | return $response; 330 | } 331 | 332 | /** 333 | * Retrieves an array of endpoint arguments from the item schema for the controller. 334 | * 335 | * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are 336 | * checked for required values and may fall-back to a given default, this is not done 337 | * on `EDITABLE` requests. Default WP_REST_Server::CREATABLE. 338 | * 339 | * @return array Endpoint arguments. 340 | * @since 4.7.0 341 | * 342 | */ 343 | public function get_endpoint_args_for_item_schema( $method = WP_REST_Server::CREATABLE ) { 344 | return rest_get_endpoint_args_for_schema( $this->get_item_schema(), $method ); 345 | } 346 | 347 | /** 348 | * Retrieves the item's schema, conforming to JSON Schema. 349 | * 350 | * @return array Item schema data. 351 | * @since 4.7.0 352 | * 353 | */ 354 | public function get_item_schema() { 355 | return $this->add_additional_fields_schema( [] ); 356 | } 357 | 358 | /** 359 | * Adds the schema from additional fields to a schema array. 360 | * 361 | * The type of object is inferred from the passed schema. 362 | * 363 | * @param array $schema Schema array. 364 | * 365 | * @return array Modified Schema array. 366 | * @since 4.7.0 367 | * 368 | */ 369 | protected function add_additional_fields_schema( $schema ) { 370 | if ( empty( $schema['title'] ) ) { 371 | return $schema; 372 | } 373 | 374 | // Can't use $this->get_object_type otherwise we cause an inf loop. 375 | $object_type = $schema['title']; 376 | 377 | $additional_fields = $this->get_additional_fields( $object_type ); 378 | 379 | foreach ( $additional_fields as $field_name => $field_options ) { 380 | if ( ! $field_options['schema'] ) { 381 | continue; 382 | } 383 | 384 | $schema['properties'][ $field_name ] = $field_options['schema']; 385 | } 386 | 387 | return $schema; 388 | } 389 | 390 | /** 391 | * Retrieves all of the registered additional fields for a given object-type. 392 | * 393 | * @param string $object_type Optional. The object type. 394 | * 395 | * @return array Registered additional fields (if any), empty array if none or if the object type could 396 | * not be inferred. 397 | * @since 4.7.0 398 | * 399 | */ 400 | protected function get_additional_fields( $object_type = null ) { 401 | 402 | if ( ! $object_type ) { 403 | $object_type = $this->get_object_type(); 404 | } 405 | 406 | if ( ! $object_type ) { 407 | return []; 408 | } 409 | 410 | global $wp_rest_additional_fields; 411 | 412 | if ( ! $wp_rest_additional_fields || ! isset( $wp_rest_additional_fields[ $object_type ] ) ) { 413 | return []; 414 | } 415 | 416 | return $wp_rest_additional_fields[ $object_type ]; 417 | } 418 | 419 | /** 420 | * Load WC Cart functionalities in REST environment 421 | * 422 | * @return void 423 | * @since 1.0.0 424 | * 425 | */ 426 | public function wc_load_cart() { 427 | include_once WC_ABSPATH . 'includes/wc-notice-functions.php'; 428 | 429 | if ( ! wc()->cart ) { 430 | include_once WC_ABSPATH . 'includes/wc-cart-functions.php'; 431 | wc_load_cart(); 432 | wc()->cart->get_cart(); 433 | } 434 | } 435 | 436 | /** 437 | * Add item to cart 438 | * 439 | * @param \WP_REST_Request $request 440 | * 441 | * @return \WP_REST_Response 442 | * @since 1.0.0 443 | * 444 | */ 445 | public function create_item( $request ) { 446 | $product_id = $request['product_id']; 447 | $quantity = ! empty( $request['quantity'] ) ? $request['quantity'] : 1; 448 | $variation_id = ! empty( $request['variation_id'] ) ? $request['variation_id'] : 0; 449 | 450 | $product = wc_get_product( $variation_id ? $variation_id : $product_id ); 451 | 452 | if ( empty( $product ) ) { 453 | return new WP_Error( 'rest_invalid_product', __( 'Product not exists', 'headless-cms' ) ); 454 | } 455 | 456 | if ( $product instanceof WC_Product_Variation ) { 457 | $product_id = $product->get_parent_id(); 458 | $variation = $product->get_variation_attributes(); 459 | } 460 | 461 | // Force quantity to 1 if sold individually and check for existing item in cart. 462 | if ( $product->is_sold_individually() ) { 463 | $quantity = 1; 464 | 465 | $cart_contents = wc()->cart->cart_contents; 466 | 467 | $found_in_cart = apply_filters( 'woocommerce_add_to_cart_sold_individually_found_in_cart', $cart_item_key && $cart_contents[ $cart_item_key ]['quantity'] > 0, $product_id, $variation_id, $cart_item_data, $cart_id ); 468 | 469 | if ( $found_in_cart ) { 470 | /* translators: %s: product name */ 471 | return new WP_Error( 'wc_next_rest_product_sold_individually', sprintf( __( 'You cannot add another "%s" to your cart.', 'headless-cms' ), $product->get_name() ), [ 'status' => 500 ] ); 472 | } 473 | } 474 | 475 | // Product is purchasable check. 476 | if ( ! $product->is_purchasable() ) { 477 | return new WP_Error( 'wc_next_rest_cannot_be_purchased', __( 'Sorry, this product cannot be purchased.', 'headless-cms' ), [ 'status' => 500 ] ); 478 | } 479 | 480 | // Stock check - only check if we're managing stock and backorders are not allowed. 481 | if ( ! $product->is_in_stock() ) { 482 | return new WP_Error( 'wc_next_rest_product_out_of_stock', sprintf( __( 'You cannot add "%s" to the cart because the product is out of stock.', 'headless-cms' ), $product->get_name() ), [ 'status' => 500 ] ); 483 | } 484 | if ( ! $product->has_enough_stock( $quantity ) ) { 485 | return new WP_Error( 'wc_next_rest_not_enough_in_stock', sprintf( __( 'You cannot add that amount of "%1$s" to the cart because there is not enough stock (%2$s remaining).', 'headless-cms' ), $product->get_name(), wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ) ), [ 'status' => 500 ] ); 486 | } 487 | 488 | // Stock check - this time accounting for whats already in-cart. 489 | if ( $product->managing_stock() ) { 490 | $products_qty_in_cart = wc()->cart->get_cart_item_quantities(); 491 | 492 | if ( isset( $products_qty_in_cart[ $product->get_stock_managed_by_id() ] ) && ! $product->has_enough_stock( $products_qty_in_cart[ $product->get_stock_managed_by_id() ] + $quantity ) ) { 493 | return new WP_Error( 494 | 'wc_next_rest_not_enough_stock_remaining', 495 | sprintf( 496 | __( 'You cannot add that amount to the cart — we have %1$s in stock and you already have %2$s in your cart.', 'headless-cms' ), 497 | wc_format_stock_quantity_for_display( $product->get_stock_quantity(), $product ), 498 | wc_format_stock_quantity_for_display( $products_qty_in_cart[ $product->get_stock_managed_by_id() ], $product ) 499 | ), 500 | [ 'status' => 500 ] 501 | ); 502 | } 503 | } 504 | 505 | // Add item to cart. 506 | $item_key = wc()->cart->add_to_cart( $product_id, $quantity, $variation_id ); 507 | 508 | // Return response to added item to cart or return error. 509 | if ( $item_key ) { 510 | $cart_item = wc()->cart->get_cart_item( $item_key ); 511 | 512 | do_action( 'wc_next_rest_add_to_cart', $item_key, $cart_item ); 513 | 514 | if ( is_array( $cart_item ) ) { 515 | $data = $this->prepare_cart_item_for_response( $cart_item, $request ); 516 | $response = rest_ensure_response( $data ); 517 | $response = $this->add_headers( $response ); 518 | 519 | return $response; 520 | } 521 | } 522 | 523 | return new WP_Error( 'wc_next_rest_cannot_add_to_cart', sprintf( __( 'You cannot add "%s" to your cart.', 'headless-cms' ), $product->get_name() ), [ 'status' => 500 ] ); 524 | } 525 | 526 | /** 527 | * Prepare cart item 528 | * 529 | * @param array $cart_item 530 | * @param \WP_REST_Request $request 531 | * 532 | * @return array 533 | * @since 1.0.0 534 | * 535 | */ 536 | protected function prepare_cart_item_for_response( $cart_item, $request ) { 537 | $product = $cart_item['data']; 538 | 539 | $product_id = $product->get_id(); 540 | 541 | if ( $product instanceof WC_Product_Variation ) { 542 | $product_id = $product->get_parent_id(); 543 | } 544 | 545 | $cart_item['data'] = $product->get_data(); 546 | $cart_item['data']['images'] = $this->get_images( $product ); 547 | $cart_item['currency'] = html_entity_decode( get_woocommerce_currency_symbol() ); 548 | 549 | return $cart_item; 550 | } 551 | 552 | /** 553 | * Get the images for a product or product variation. 554 | * 555 | * @param WC_Product|WC_Product_Variation $product Product instance. 556 | * 557 | * @return array 558 | * @since 1.0.0 559 | * 560 | */ 561 | protected function get_images( $product ) { 562 | $images = []; 563 | $attachment_ids = []; 564 | 565 | // Add featured image. 566 | if ( $product->get_image_id() ) { 567 | $attachment_ids[] = $product->get_image_id(); 568 | } 569 | 570 | // Add gallery images. 571 | $attachment_ids = array_merge( $attachment_ids, $product->get_gallery_image_ids() ); 572 | 573 | // Build image data. 574 | foreach ( $attachment_ids as $position => $attachment_id ) { 575 | $attachment_post = get_post( $attachment_id ); 576 | if ( is_null( $attachment_post ) ) { 577 | continue; 578 | } 579 | 580 | $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); 581 | if ( ! is_array( $attachment ) ) { 582 | continue; 583 | } 584 | 585 | $images[] = [ 586 | 'id' => (int) $attachment_id, 587 | 'src' => current( $attachment ), 588 | 'name' => get_the_title( $attachment_id ), 589 | 'alt' => get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ), 590 | ]; 591 | } 592 | 593 | // Set a placeholder image if the product has no images set. 594 | if ( empty( $images ) ) { 595 | $images[] = [ 596 | 'id' => 0, 597 | 'src' => wc_placeholder_img_src(), 598 | 'name' => __( 'Placeholder', 'headless-cms' ), 599 | 'alt' => __( 'Placeholder', 'headless-cms' ), 600 | ]; 601 | } 602 | 603 | return $images; 604 | } 605 | 606 | /** 607 | * Add response header 608 | * 609 | * @param \WP_REST_Response $response 610 | * 611 | * @since 1.0.0 612 | * 613 | */ 614 | protected function add_headers( $response ) { 615 | wc()->cart->calculate_totals(); 616 | 617 | $response->header( 'X-WC-Cart-TotalItems', wc()->cart->get_cart_contents_count() ); 618 | $response->header( 'X-WC-Cart-Totals', json_encode( wc()->cart->get_totals() ) ); 619 | 620 | $wc_session_id = null; 621 | $headers = headers_list(); 622 | 623 | foreach ( $headers as $header ) { 624 | if ( 0 === strpos( $header, 'Set-Cookie: wp_woocommerce_session_' ) ) { 625 | preg_match_all( '/Set-Cookie: wp_woocommerce_session_(.*?)=(.*?);/', $header, $matches ); 626 | 627 | if ( ! empty( $matches[2][0] ) ) { 628 | $wc_session_id = $matches[2][0]; 629 | } 630 | } 631 | } 632 | 633 | $response->header( 'X-WC-Session', $wc_session_id ); 634 | 635 | return $response; 636 | } 637 | 638 | /** 639 | * Validate product 640 | * 641 | * @param mixed $value 642 | * @param \WP_REST_Request $request 643 | * @param string $param 644 | * 645 | * @return bool 646 | * @since 1.0.0 647 | * 648 | */ 649 | public function is_valid_product( $value, $request, $param ) { 650 | if ( ! is_integer( $value ) ) { 651 | return new WP_Error( 'rest_invalid_product_id', sprintf( __( 'Invalid %s.', 'headless-cms' ), $param ) ); 652 | } 653 | 654 | if ( empty( absint( $request['product_id'] ) ) && empty( absint( $request['variation_id'] ) ) ) { 655 | return new WP_Error( 'rest_invalid_data', __( 'product_id or variation_id is required.', 'headless-cms' ) ); 656 | } 657 | 658 | $product_id = absint( $value ); 659 | 660 | if ( $product_id <= 0 ) { 661 | return true; 662 | } 663 | 664 | $product = wc_get_product( $product_id ); 665 | 666 | if ( $product instanceof WC_Product && 'trash' !== $product->get_status() ) { 667 | return true; 668 | } 669 | 670 | return new WP_Error( 'rest_invalid_product', sprintf( __( '%s does not exist.', 'headless-cms' ), $param ) ); 671 | } 672 | 673 | /** 674 | * Validate product quantity 675 | * 676 | * @param mixed $value 677 | * @param \WP_REST_Request $request 678 | * @param string $param 679 | * 680 | * @return bool|\WP_Error 681 | * @since 1.0.0 682 | * 683 | */ 684 | public function is_valid_quantity( $value, $request, $param ) { 685 | if ( ! is_integer( $value ) ) { 686 | return new WP_Error( 'rest_invalid_quantity', __( 'quantity is not numeric.', 'headless-cms' ) ); 687 | } 688 | 689 | $value = absint( $value ); 690 | 691 | if ( $value < 0 ) { 692 | return new WP_Error( 'rest_invalid_quantity', __( 'quntity must be equal or greater than 0.', 'headless-cms' ) ); 693 | } 694 | 695 | return true; 696 | } 697 | 698 | /** 699 | * Validate a cart item 700 | * 701 | * @param string $key 702 | * @param \WP_REST_Request $request 703 | * @param string $param 704 | * 705 | * @return bool 706 | * @since 1.0.0 707 | * 708 | */ 709 | public function is_valid_cart_item( $key, $request, $param ) { 710 | if ( wc()->cart->is_empty() ) { 711 | return new WP_Error( 'wc_next_rest_empty_cart', __( "You don't have any item in your cart.", 'headless-cms' ) ); 712 | } 713 | 714 | if ( wc()->cart->get_cart_item( $key ) ) { 715 | return true; 716 | } 717 | 718 | return new WP_Error( 'wc_next_rest_invalid_cart_item_key', __( 'Invalid cart item key.', 'headless-cms' ) ); 719 | } 720 | 721 | /** 722 | * Validate a trashed cart item 723 | * 724 | * @param string $key 725 | * @param \WP_REST_Request $request 726 | * @param string $param 727 | * 728 | * @return bool 729 | * @since 1.0.0 730 | * 731 | */ 732 | public function is_valid_removed_cart_item( $key, $request, $param ) { 733 | $removed_items = wc()->cart->get_removed_cart_contents(); 734 | 735 | if ( isset( $removed_items[ $key ] ) ) { 736 | return true; 737 | } 738 | 739 | return new WP_Error( 'wc_next_rest_invalid_trashed_item_key', __( 'Cart item not found in removed items.', 'headless-cms' ) ); 740 | } 741 | 742 | /** 743 | * Clear cart 744 | * 745 | * @return \WP_REST_Response|\WP_Error 746 | * @since 1.0.0 747 | * 748 | */ 749 | public function clear_cart() { 750 | wc()->cart->empty_cart(); 751 | wc()->session->set( 'cart', [] ); 752 | 753 | if ( ! wc()->cart->is_empty() ) { 754 | return new WP_Error( 'wc_next_rest_clear_cart_failed', __( 'Clearing the cart failed!', 'headless-cms' ), [ 'status' => 500 ] ); 755 | } else { 756 | $response = rest_ensure_response( [] ); 757 | $response = $this->add_headers( $response ); 758 | 759 | return $response; 760 | } 761 | } 762 | 763 | /** 764 | * Update a cart item 765 | * 766 | * @param \WP_REST_Request $request 767 | * 768 | * @return \WP_REST_Response|\WP_Error 769 | * @since 1.0.0 770 | * 771 | */ 772 | public function update_item( $request ) { 773 | $cart_item_key = $request['key']; 774 | $quantity = $request['quantity']; 775 | 776 | $current_data = wc()->cart->get_cart_item( $cart_item_key ); 777 | 778 | // Checks if the item has enough stock before updating 779 | $has_enough_stock = $this->has_enough_stock( $current_data, $quantity ); 780 | 781 | if ( is_wp_error( $has_enough_stock ) ) { 782 | return $has_enough_stock; 783 | } 784 | 785 | if ( wc()->cart->set_quantity( $cart_item_key, $quantity ) ) { 786 | $new_data = wc()->cart->get_cart_item( $cart_item_key ); 787 | 788 | $product_id = ! isset( $new_data['product_id'] ) ? 0 : absint( $new_data['product_id'] ); 789 | $variation_id = ! isset( $new_data['variation_id'] ) ? 0 : absint( $new_data['variation_id'] ); 790 | 791 | $product_data = wc_get_product( $variation_id ? $variation_id : $product_id ); 792 | 793 | if ( isset( $new_data['quantity'] ) && $quantity != $new_data['quantity'] ) { 794 | do_action( 'wc_next_rest_item_quantity_changed', $cart_item_key, $new_data ); 795 | } 796 | 797 | $data = ! empty( $new_data ) ? $this->prepare_cart_item_for_response( $new_data, $request ) : []; 798 | $response = rest_ensure_response( $data ); 799 | $response = $this->add_headers( $response ); 800 | 801 | // Return response based on product quantity increment. 802 | if ( $quantity > $current_data['quantity'] ) { 803 | $status = 'increased'; 804 | } elseif ( $quantity < $current_data['quantity'] ) { 805 | $status = 'decreased'; 806 | } else { 807 | $status = 'unchanged'; 808 | } 809 | 810 | $quantity_status = json_encode( [ 811 | 'status' => $status, 812 | 'previous_quantity' => $current_data['quantity'], 813 | 'new_quantity' => $quantity, 814 | ] ); 815 | 816 | $response->header( 'X-WC-Cart-ItemQuantity', $quantity_status ); 817 | 818 | return $response; 819 | 820 | } else { 821 | return new WP_Error( 'wc_next_rest_can_not_update_item', __( 'Unable to update item quantity in cart.', 'headless-cms' ), [ 'status' => 500 ] ); 822 | } 823 | } 824 | 825 | /** 826 | * Checks if the product in the cart has enough stock 827 | * before updating the quantity. 828 | * 829 | * @param array $current_data 830 | * @param string $quantity 831 | * 832 | * @return bool|WP_Error 833 | * @since 1.0.0 834 | * 835 | */ 836 | protected function has_enough_stock( $current_data = [], $quantity = 1 ) { 837 | $product_id = ! isset( $current_data['product_id'] ) ? 0 : absint( $current_data['product_id'] ); 838 | $variation_id = ! isset( $current_data['variation_id'] ) ? 0 : absint( $current_data['variation_id'] ); 839 | $current_product = wc_get_product( $variation_id ? $variation_id : $product_id ); 840 | 841 | $quantity = absint( $quantity ); 842 | 843 | if ( ! $current_product->has_enough_stock( $quantity ) ) { 844 | return new WP_Error( 845 | 'wc_next_rest_not_enough_in_stock', 846 | sprintf( 847 | __( 'You cannot add that amount of "%1$s" to the cart because there is not enough stock (%2$s remaining).', 'headless-cms' ), 848 | $current_product->get_name(), 849 | wc_format_stock_quantity_for_display( $current_product->get_stock_quantity(), $current_product ) 850 | ), 851 | [ 'status' => 500 ] 852 | ); 853 | } 854 | 855 | return true; 856 | } 857 | 858 | /** 859 | * Delete/Remove a cart item 860 | * 861 | * @param \WP_REST_Request $request 862 | * 863 | * @return \WP_REST_Response 864 | * @since 1.0.0 865 | * 866 | */ 867 | public function delete_item( $request ) { 868 | $cart_item = wc()->cart->get_cart_item( $request['key'] ); 869 | 870 | if ( ! wc()->cart->remove_cart_item( $request['key'] ) ) { 871 | return new WP_Error( 'wc_cart_rest_can_not_remove_item', __( 'Unable to remove item from cart.', 'headless-cms' ), [ 'status' => 500 ] ); 872 | } 873 | 874 | $data = $this->prepare_cart_item_for_response( $cart_item, $request ); 875 | $response = rest_ensure_response( $data ); 876 | $response = $this->add_headers( $response ); 877 | 878 | return $response; 879 | } 880 | 881 | /** 882 | * Restore a cart item 883 | * 884 | * @param \WP_REST_Request $request 885 | * 886 | * @return \WP_REST_Response 887 | * @since 1.0.0 888 | * 889 | */ 890 | public function restore_item( $request ) { 891 | if ( ! wc()->cart->restore_cart_item( $request['key'] ) ) { 892 | return new WP_Error( 'wc_cart_rest_can_not_restore_item', __( 'Unable to restore cart item.', 'headless-cms' ), [ 'status' => 500 ] ); 893 | } 894 | 895 | $cart_item = wc()->cart->get_cart_item( $request['key'] ); 896 | $data = $this->prepare_cart_item_for_response( $cart_item, $request ); 897 | $response = rest_ensure_response( $data ); 898 | $response = $this->add_headers( $response ); 899 | 900 | return $response; 901 | } 902 | 903 | /** 904 | * Get normalized rest base. 905 | * 906 | * @return string 907 | */ 908 | protected function get_normalized_rest_base() { 909 | return preg_replace( '/\(.*\)\//i', '', $this->rest_base ); 910 | } 911 | 912 | /** 913 | * Check batch limit. 914 | * 915 | * ATTENTION: Intentionally keep original code from WooCommerce 916 | * 917 | * @param array $items Request items. 918 | * 919 | * @return bool|WP_Error 920 | * @since 1.0.0 921 | * 922 | */ 923 | protected function check_batch_limit( $items ) { 924 | $limit = apply_filters( 'woocommerce_rest_batch_items_limit', 100, $this->get_normalized_rest_base() ); 925 | $total = 0; 926 | 927 | if ( ! empty( $items['create'] ) ) { 928 | $total += count( $items['create'] ); 929 | } 930 | 931 | if ( ! empty( $items['update'] ) ) { 932 | $total += count( $items['update'] ); 933 | } 934 | 935 | if ( ! empty( $items['delete'] ) ) { 936 | $total += count( $items['delete'] ); 937 | } 938 | 939 | if ( $total > $limit ) { 940 | /* translators: %s: items limit */ 941 | return new WP_Error( 'woocommerce_rest_request_entity_too_large', sprintf( __( 'Unable to accept more than %s items for this request.', 'headless-cms' ), $limit ), [ 'status' => 413 ] ); 942 | } 943 | 944 | return true; 945 | } 946 | 947 | /** 948 | * Bulk create, update and delete items. 949 | * 950 | * @param WP_REST_Request $request Full details about the request. 951 | * 952 | * @return array Of WP_Error or WP_REST_Response. 953 | * @since 1.0.0 954 | * 955 | */ 956 | public function batch_items( $request ) { 957 | /** 958 | * REST Server 959 | * 960 | * @var WP_REST_Server $wp_rest_server 961 | */ 962 | global $wp_rest_server; 963 | 964 | // Get the request params. 965 | $items = array_filter( $request->get_params() ); 966 | $response = []; 967 | 968 | // Check batch limit. 969 | $limit = $this->check_batch_limit( $items ); 970 | if ( is_wp_error( $limit ) ) { 971 | return $limit; 972 | } 973 | 974 | if ( ! empty( $items['create'] ) ) { 975 | foreach ( $items['create'] as $item ) { 976 | $_item = new WP_REST_Request( 'POST' ); 977 | 978 | // Default parameters. 979 | $defaults = []; 980 | $schema = $this->get_public_item_schema(); 981 | foreach ( $schema['properties'] as $arg => $options ) { 982 | if ( isset( $options['default'] ) ) { 983 | $defaults[ $arg ] = $options['default']; 984 | } 985 | } 986 | $_item->set_default_params( $defaults ); 987 | 988 | // Set request parameters. 989 | $_item->set_body_params( $item ); 990 | $_response = $this->create_item( $_item ); 991 | 992 | if ( is_wp_error( $_response ) ) { 993 | $response['create'][] = [ 994 | 'id' => 0, 995 | 'error' => [ 996 | 'code' => $_response->get_error_code(), 997 | 'message' => $_response->get_error_message(), 998 | 'data' => $_response->get_error_data(), 999 | ], 1000 | ]; 1001 | } else { 1002 | $response['create'][] = $wp_rest_server->response_to_data( $_response, '' ); 1003 | } 1004 | } 1005 | } 1006 | 1007 | if ( ! empty( $items['update'] ) ) { 1008 | foreach ( $items['update'] as $item ) { 1009 | $_item = new WP_REST_Request( 'PUT' ); 1010 | $_item->set_body_params( $item ); 1011 | $_response = $this->update_item( $_item ); 1012 | 1013 | if ( is_wp_error( $_response ) ) { 1014 | $response['update'][] = [ 1015 | 'id' => $item['id'], 1016 | 'error' => [ 1017 | 'code' => $_response->get_error_code(), 1018 | 'message' => $_response->get_error_message(), 1019 | 'data' => $_response->get_error_data(), 1020 | ], 1021 | ]; 1022 | } else { 1023 | $response['update'][] = $wp_rest_server->response_to_data( $_response, '' ); 1024 | } 1025 | } 1026 | } 1027 | 1028 | if ( ! empty( $items['delete'] ) ) { 1029 | foreach ( $items['delete'] as $id ) { 1030 | $id = (int) $id; 1031 | 1032 | if ( 0 === $id ) { 1033 | continue; 1034 | } 1035 | 1036 | $_item = new WP_REST_Request( 'DELETE' ); 1037 | $_item->set_query_params( 1038 | [ 1039 | 'id' => $id, 1040 | 'force' => true, 1041 | ] 1042 | ); 1043 | $_response = $this->delete_item( $_item ); 1044 | 1045 | if ( is_wp_error( $_response ) ) { 1046 | $response['delete'][] = [ 1047 | 'id' => $id, 1048 | 'error' => [ 1049 | 'code' => $_response->get_error_code(), 1050 | 'message' => $_response->get_error_message(), 1051 | 'data' => $_response->get_error_data(), 1052 | ], 1053 | ]; 1054 | } else { 1055 | $response['delete'][] = $wp_rest_server->response_to_data( $_response, '' ); 1056 | } 1057 | } 1058 | } 1059 | 1060 | return $response; 1061 | } 1062 | 1063 | } 1064 | -------------------------------------------------------------------------------- /inc/classes/api/class-wc-countries.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 41 | } 42 | 43 | /** 44 | * To setup action/filter. 45 | * 46 | * @return void 47 | */ 48 | protected function setup_hooks() { 49 | 50 | /** 51 | * Action 52 | */ 53 | add_action( 'rest_api_init', [ $this, 'register_rest_api_endpoints' ] ); 54 | 55 | } 56 | 57 | /** 58 | * Register endpoints. 59 | */ 60 | public function register_rest_api_endpoints() { 61 | 62 | // e.g. http://example.com/wp-json/rae/v1/wc/countries 63 | register_rest_route( 64 | $this->namespace, 65 | '/' . $this->rest_base, 66 | [ 67 | [ 68 | 'methods' => WP_REST_Server::READABLE, 69 | 'callback' => [ $this, 'get_countries' ], 70 | 'permission_callback' => '__return_true', 71 | ], 72 | ] 73 | ); 74 | } 75 | 76 | /** 77 | * Get countries 78 | * 79 | * @param \WP_REST_Request $request 80 | * 81 | * @return \WP_REST_Response 82 | * 83 | * @since 1.0.0 84 | * 85 | */ 86 | public function get_countries( \WP_REST_Request $request ): \WP_REST_Response { 87 | 88 | // All countries for billing. 89 | $all_countries = class_exists( 'WooCommerce' ) ? WC()->countries : []; 90 | $billing_countries = ! empty( $all_countries->countries ) ? $all_countries->countries : []; 91 | $billing_countries = $this->get_formatted_countries( $billing_countries ); 92 | 93 | // All countries with states for shipping. 94 | $shipping_countries = class_exists( 'WooCommerce' ) ? WC()->countries->get_shipping_countries() : [];; 95 | $shipping_countries = ! empty( $shipping_countries ) ? $shipping_countries : []; 96 | $shipping_countries = $this->get_formatted_countries( $shipping_countries ); 97 | 98 | /** 99 | * Here you need to return data that matches the shape of the "WooCountries" type. You could get 100 | * the data from the WP Database, an external API, or static values. 101 | * For example in this case we are getting it from WordPress database. 102 | */ 103 | $data = [ 104 | 'billingCountries' => $billing_countries, 105 | 'shippingCountries' => $shipping_countries, 106 | ]; 107 | 108 | return rest_ensure_response( $data ); 109 | } 110 | 111 | /** 112 | * Get Formatted Countries. 113 | * 114 | * @param $countries 115 | * 116 | * @return array 117 | */ 118 | public function get_formatted_countries( $countries ) { 119 | 120 | $formatted_countries = []; 121 | 122 | if ( empty( $countries ) && ! is_array( $countries ) ) { 123 | return $formatted_countries; 124 | } 125 | 126 | foreach ( $countries as $countryCode => $countryName ) { 127 | array_push( $formatted_countries, [ 128 | 'countryCode' => $countryCode, 129 | 'countryName' => $countryName, 130 | ] ); 131 | } 132 | 133 | return $formatted_countries; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /inc/classes/api/class-wc-states.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 41 | } 42 | 43 | /** 44 | * To setup action/filter. 45 | * 46 | * @return void 47 | */ 48 | protected function setup_hooks() { 49 | 50 | /** 51 | * Action 52 | */ 53 | add_action( 'rest_api_init', [ $this, 'register_rest_api_endpoints' ] ); 54 | 55 | } 56 | 57 | /** 58 | * Register endpoints. 59 | */ 60 | public function register_rest_api_endpoints() { 61 | 62 | // e.g. http://example.com/wp-json/rae/v1/wc/states?countryCode=IN 63 | register_rest_route( 64 | $this->namespace, 65 | '/' . $this->rest_base, 66 | [ 67 | [ 68 | 'methods' => WP_REST_Server::READABLE, 69 | 'callback' => [ $this, 'get_states' ], 70 | 'permission_callback' => '__return_true', 71 | ], 72 | ] 73 | ); 74 | } 75 | 76 | /** 77 | * Get States by 78 | * 79 | * @param \WP_REST_Request $request 80 | * 81 | * @return \WP_REST_Response 82 | */ 83 | public function get_states( \WP_REST_Request $request ): \WP_REST_Response { 84 | 85 | if ( ! class_exists( 'WooCommerce' ) ) { 86 | return rest_ensure_response( [] ); 87 | } 88 | 89 | $parameters = $request->get_params(); 90 | $countryCode = ! empty( $parameters['countryCode'] ) ? sanitize_text_field( $parameters['countryCode'] ) : ''; 91 | 92 | $states = ! empty( $countryCode ) ? WC()->countries->get_states( strtoupper( $countryCode ) ) : []; 93 | $states = $this->get_formatted_states( $states ); 94 | 95 | /** 96 | * Here you need to return data that matches the shape of the "WooStates" type. You could get 97 | * the data from the WP Database, an external API, or static values. 98 | * For example in this case we are getting it from WordPress database. 99 | */ 100 | $data = [ 101 | 'states' => $states, 102 | ]; 103 | 104 | return rest_ensure_response( $data ); 105 | } 106 | 107 | /** 108 | * Get Formatted States. 109 | * 110 | * @param array $states 111 | * 112 | * @return array 113 | */ 114 | public function get_formatted_states( array $states = [] ): array { 115 | 116 | $formatted_states = []; 117 | 118 | if ( empty( $states ) && ! is_array( $states ) ) { 119 | return $formatted_states; 120 | } 121 | 122 | foreach ( $states as $stateCode => $stateName ) { 123 | array_push( $formatted_states, [ 124 | 'stateCode' => $stateCode, 125 | 'stateName' => $stateName, 126 | ] ); 127 | } 128 | 129 | return $formatted_states; 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /inc/classes/class-assets.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Action 35 | */ 36 | add_action( 'admin_enqueue_scripts', [ $this, 'admin_enqueue_scripts' ] ); 37 | add_action( 'enqueue_block_assets', [ $this, 'enqueue_editor_assets' ] ); 38 | 39 | } 40 | 41 | /** 42 | * To enqueue scripts and styles. in admin. 43 | * 44 | * @param string $hook_suffix Admin page name. 45 | * 46 | * @return void 47 | */ 48 | public function admin_enqueue_scripts( $hook_suffix ) { 49 | 50 | if ( 'toplevel_page_hcms-settings-menu-page' === $hook_suffix ) { 51 | wp_register_script( 'hcms-plugins-settings-js', HEADLESS_CMS_BUILD_URI . '/js/settings.js', [ 'jquery' ], filemtime( HEADLESS_CMS_BUILD_DIR . '/js/settings.js' ), true ); 52 | wp_register_style( 'hcms-plugins-settings-css', HEADLESS_CMS_BUILD_URI . '/css/settings.css', [], filemtime( HEADLESS_CMS_BUILD_DIR . '/css/settings.css' ), false ); 53 | 54 | wp_enqueue_style( 'hcms-plugins-settings-css' ); 55 | wp_enqueue_media(); 56 | wp_enqueue_script( 'hcms-plugins-settings-js' ); 57 | wp_enqueue_script( 'media-uploader' ); 58 | } 59 | 60 | if ( 'term.php' === $hook_suffix ) { 61 | wp_register_script( 'hcms-plugins-category-js', HEADLESS_CMS_BUILD_URI . '/js/category.js', [ 'jquery' ], filemtime( HEADLESS_CMS_BUILD_DIR . '/js/category.js' ), true ); 62 | wp_register_style( 'hcms-plugins-category-css', HEADLESS_CMS_BUILD_URI . '/css/category.css', [], filemtime( HEADLESS_CMS_BUILD_DIR . '/css/category.css' ), false ); 63 | 64 | wp_enqueue_style( 'hcms-plugins-category-css' ); 65 | wp_enqueue_media(); 66 | wp_enqueue_script( 'hcms-plugins-category-js' ); 67 | wp_enqueue_script( 'media-uploader' ); 68 | } 69 | 70 | } 71 | 72 | /** 73 | * Enqueue editor scripts. 74 | */ 75 | public function enqueue_editor_assets() { 76 | 77 | $plugin_settings = get_option('hcms_plugin_options'); 78 | $is_custom_preview_link_active = is_array($plugin_settings) && !empty($plugin_settings['activate_preview']) ? $plugin_settings['activate_preview'] : false; 79 | $frontend_site_url = is_array($plugin_settings) && !empty($plugin_settings['frontend_site_url']) ? $plugin_settings['frontend_site_url'] : ''; 80 | 81 | // Theme Editor JS. 82 | if ( is_admin() ) { 83 | wp_enqueue_script( 84 | 'hcms-editor-js', 85 | HEADLESS_CMS_BUILD_URI . '/js/editor.js', 86 | [], 87 | '1.1', 88 | true 89 | ); 90 | wp_localize_script( 'hcms-editor-js', 'frontendConfig', [ 91 | 'isPreviewLinkActive' => $is_custom_preview_link_active, 92 | 'frontendSiteUrl' => $frontend_site_url 93 | ] ); 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /inc/classes/class-category.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 26 | } 27 | 28 | /** 29 | * To setup action/filter. 30 | * 31 | * @return void 32 | */ 33 | protected function setup_hooks() { 34 | 35 | /** 36 | * Action 37 | */ 38 | add_action( 'category_add_form_fields', [ $this, 'add_category_image' ], 10, 2 ); 39 | add_action( 'created_category', [ $this, 'save_category_image' ], 10, 2 ); 40 | add_action( 'category_edit_form_fields', [ $this, 'update_category_image' ], 10, 2 ); 41 | add_action( 'edited_category', [ $this, 'updated_category_image' ], 10, 2 ); 42 | 43 | } 44 | 45 | /** 46 | * Add form fields 47 | * 48 | * @param string $taxonomy Taxonomy. 49 | */ 50 | public function add_category_image( $taxonomy ) { 51 | include_once HEADLESS_CMS_TEMPLATE_PATH . 'category-img-form.php'; 52 | } 53 | 54 | /** 55 | * Save the form fields 56 | * 57 | * @param integer $term_id term ID. 58 | */ 59 | public function save_category_image( $term_id ) { 60 | if ( ! empty( $_POST['category-image-id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification 61 | $image = sanitize_text_field( $_POST['category-image-id'] ); 62 | $meta_key = 'category-image-id'; 63 | 64 | add_term_meta( $term_id, $meta_key, $image, true ); 65 | } 66 | } 67 | 68 | /** 69 | * Edit the form fields 70 | * 71 | * @param object $term Term. 72 | */ 73 | public function update_category_image( $term ) { ?> 74 | 75 | 76 | 77 | 78 | 79 | term_id, 'category-image-id', true ); 81 | $add_image_class = ! empty( $image_id ) ? 'hcms_hide' : ''; 82 | $remove_image_class = empty( $image_id ) ? 'hcms_hide' : ''; 83 | ?> 84 | 85 |
86 | 87 | 88 | 89 |
90 |
91 | 92 | 93 |
94 | 95 | 96 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Action 35 | */ 36 | add_action( 'customize_register', [ $this, 'customize_register' ] ); 37 | 38 | } 39 | 40 | /** 41 | * Customize register. 42 | * 43 | * @param \WP_Customize_Manager $wp_customize Theme Customizer object. 44 | * 45 | * @action customize_register 46 | */ 47 | public function customize_register( \WP_Customize_Manager $wp_customize ) { 48 | 49 | $this->social_icon_section( $wp_customize ); 50 | $this->footer_section( $wp_customize ); 51 | 52 | } 53 | 54 | /** 55 | * Add Footer section settings. 56 | * 57 | * @param object $wp_customize WP_Customize. 58 | */ 59 | public function footer_section( $wp_customize ) { 60 | 61 | $wp_customize->add_section( 62 | 'rae_footer', 63 | [ 64 | 'title' => esc_html__( 'Footer', 'rest-api-endpoints' ), 65 | 'description' => esc_html__( 'Footer', 'rest-api-endpoints' ), 66 | ] 67 | ); 68 | 69 | $setting_id = 'rae_footer_text'; 70 | 71 | $wp_customize->add_setting( 72 | $setting_id, 73 | [ 74 | 'default' => '', 75 | 'capability' => 'edit_theme_options', 76 | 'sanitize_callback' => 'esc_html', 77 | ] 78 | ); 79 | 80 | $wp_customize->add_control( 81 | $setting_id, 82 | [ 83 | 'label' => esc_html__( 'Copyright text', 'rest-api-endpoints' ), 84 | 'section' => 'rae_footer', 85 | 'settings' => $setting_id, 86 | 'type' => 'text', 87 | ] 88 | ); 89 | } 90 | 91 | /** 92 | * Add social icon section. 93 | * 94 | * @param object $wp_customize WP_Customize. 95 | */ 96 | public function social_icon_section( $wp_customize ) { 97 | 98 | // Social Icons. 99 | $social_icons = [ 'facebook', 'twitter', 'instagram', 'youtube' ]; 100 | 101 | $wp_customize->add_section( 102 | 'rae_social_links', 103 | [ 104 | 'title' => esc_html__( 'Social Links', 'rest-api-endpoints' ), 105 | 'description' => esc_html__( 'Social links', 'rest-api-endpoints' ), 106 | ] 107 | ); 108 | 109 | foreach ( $social_icons as $social_icon ) { 110 | 111 | $setting_id = sprintf( 'rae_%s_link', $social_icon ); 112 | 113 | $wp_customize->add_setting( 114 | $setting_id, 115 | [ 116 | 'default' => '', 117 | 'capability' => 'edit_theme_options', 118 | 'sanitize_callback' => 'esc_url', 119 | ] 120 | ); 121 | 122 | $wp_customize->add_control( 123 | $setting_id, 124 | [ 125 | 'label' => esc_html( $social_icon ), 126 | 'section' => 'rae_social_links', 127 | 'settings' => $setting_id, 128 | 'type' => 'text', 129 | ] 130 | ); 131 | } 132 | } 133 | 134 | 135 | } 136 | -------------------------------------------------------------------------------- /inc/classes/class-plugin.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 26 | } 27 | 28 | /** 29 | * To setup action/filter. 30 | * 31 | * @return void 32 | */ 33 | protected function setup_hooks() 34 | { 35 | 36 | /** 37 | * Action 38 | */ 39 | add_filter('preview_post_link', [$this, 'set_frontend_post_preview_link'], 1, 2); 40 | 41 | } 42 | 43 | /** 44 | * Sets the customized post preview link for frontend application. 45 | * 46 | * @param string $link The WordPress preview link. 47 | * @param object $post Post Object 48 | */ 49 | public function set_frontend_post_preview_link($link, $post) 50 | { 51 | $plugin_settings = get_option('hcms_plugin_options'); 52 | $is_custom_preview_link_active = is_array($plugin_settings) && !empty($plugin_settings['activate_preview']) ? $plugin_settings['activate_preview'] : false; 53 | $frontend_site_url = is_array($plugin_settings) && !empty($plugin_settings['frontend_site_url']) ? $plugin_settings['frontend_site_url'] : ''; 54 | 55 | if (!$is_custom_preview_link_active) { 56 | return $link; 57 | } 58 | 59 | // Expected frontend preview URL /api/preview/?postType=page&postId=30 60 | $root_url = WP_DEBUG === true ? 'http://localhost:3000' : $frontend_site_url; 61 | return sprintf('%1$sapi/preview/?postType=%2$s&postId=%3$d', esc_url(trailingslashit( $root_url )), get_post_type($post), $post->ID); 62 | 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /inc/classes/class-settings.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 26 | } 27 | 28 | /** 29 | * To setup action/filter. 30 | * 31 | * @return void 32 | */ 33 | protected function setup_hooks() { 34 | 35 | /** 36 | * Action 37 | */ 38 | add_action( 'admin_menu', [ $this, 'add_settings_page' ] ); 39 | 40 | // Get plugin settings. 41 | $plugin_settings = get_option( 'hcms_plugin_options' ); 42 | $allow_anonymous_comments = is_array( $plugin_settings ) && ! empty( $plugin_settings['allow_anonymous_comments'] ) ? $plugin_settings['allow_anonymous_comments'] : false; 43 | 44 | // If $allow_anonymous_comments is checked, then allow users to post comments without logging-in via REST API. 45 | if ( ! empty( $allow_anonymous_comments ) ) { 46 | add_filter( 'rest_allow_anonymous_comments', '__return_true' ); 47 | } 48 | 49 | } 50 | 51 | /** 52 | * Adds settings page for the plugin in the dashboard. 53 | */ 54 | public function add_settings_page() { 55 | 56 | $menu_plugin_title = __( 'HCMS Settings', 'headless-cms' ); 57 | 58 | // Create new top-level menu. 59 | add_menu_page( 60 | __( 61 | 'HCMS Plugin Settings', 62 | 'headless-cms' 63 | ), 64 | $menu_plugin_title, 65 | 'administrator', 66 | 'hcms-settings-menu-page', 67 | [ $this, 'plugin_settings_page_content' ], 68 | 'dashicons-admin-generic' 69 | ); 70 | 71 | // Call register settings function. 72 | add_action( 'admin_init', [ $this, 'register_plugin_settings' ] ); 73 | } 74 | 75 | /** 76 | * Register our settings. 77 | */ 78 | public function register_plugin_settings() { 79 | register_setting( 'hcms-plugin-settings-group', 'hcms_plugin_options' ); 80 | } 81 | 82 | /** 83 | * Settings Page Content for Orion Plugin. 84 | */ 85 | public function plugin_settings_page_content() { 86 | 87 | // Check user capabilities. 88 | if ( ! current_user_can( 'manage_options' ) ) { 89 | return; 90 | } 91 | 92 | /** 93 | * Add error/update messages. 94 | * Check if the user have submitted the settings. 95 | * WordPress will add the "settings-updated" $_GET parameter to the url. 96 | */ 97 | if ( isset( $_GET['settings-updated'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification 98 | 99 | // Add settings saved message with the class of "updated". 100 | add_settings_error( 'hcms_app_messages', 'hcms_app_message', __( 'Settings Saved', 'headless-cms' ), 'updated' ); 101 | 102 | } 103 | 104 | // Show error/update messages. 105 | settings_errors( 'hcms_app_messages' ); 106 | 107 | include_once HEADLESS_CMS_TEMPLATE_PATH . 'settings-form-template.php'; 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /inc/classes/mutations/class-add-wishlist.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Action 35 | */ 36 | 37 | // Register Wishlist Mutation Types. 38 | add_action( 'graphql_register_types', [ $this, 'add_wishlist_mutation' ] ); 39 | 40 | } 41 | 42 | /** 43 | * 44 | * inputFields: expects an array of Fields to be used for inputting values to the mutation 45 | * 46 | * outputFields: expects an array of fields that can be asked for in response to the mutation 47 | * the resolve function is optional, but can be useful if the mutateAndPayload doesn't return an array 48 | * with the same key(s) as the outputFields 49 | * 50 | * mutateAndGetPayload: expects a function, and the function gets passed the $input, $context, and $info 51 | * the function should return enough info for the outputFields to resolve with 52 | * 53 | * @throws \Exception 54 | */ 55 | /** 56 | * Register field. 57 | */ 58 | public function add_wishlist_mutation() { 59 | register_graphql_mutation( 60 | 'addToWishlist', 61 | [ 62 | 'inputFields' => [ 63 | 'productId' => [ 64 | 'type' => 'Integer', 65 | 'description' => __( 'Product id', 'headless-cms' ), 66 | ], 67 | ], 68 | 69 | 'outputFields' => [ 70 | 'added' => [ 71 | 'type' => 'Boolean', 72 | 'description' => __( 'True if the product is removed, false otherwise', 'headless-cms' ), 73 | ], 74 | 'productId' => [ 75 | 'type' => 'Integer', 76 | 'description' => __( 'The Product id that was added', 'headless-cms' ), 77 | ], 78 | 'wishlistProductIds' => [ 79 | 'type' => [ 'list_of' => 'Integer' ], 80 | 'description' => __( 'The Product ids in the wishlist', 'headless-cms' ), 81 | ], 82 | 'error' => [ 83 | 'type' => 'String', 84 | 'description' => __( 'Description of the error', 'headless-cms' ), 85 | ], 86 | ], 87 | 88 | 'mutateAndGetPayload' => function ( $input, $context, $info ) { 89 | 90 | $response = [ 91 | 'added' => false, 92 | 'productId' => ! empty( $input['productId'] ) ? $input['productId'] : 0, 93 | 'wishlistProductIds' => [], 94 | 'error' => '', 95 | ]; 96 | 97 | $user_id = get_current_user_id(); 98 | 99 | if ( ! $user_id ) { 100 | $response['error'] = __( 'Authentication failed', 'headless-cms' ); 101 | 102 | return $response; 103 | } 104 | 105 | if ( empty( $input['productId'] ) ) { 106 | $response['error'] = __( 'Please enter a valid product id', 'headless-cms' ); 107 | 108 | return $response; 109 | } 110 | 111 | return $this->save_products_to_wishlist( $input['productId'], $user_id, $response ); 112 | }, 113 | ] 114 | ); 115 | } 116 | 117 | /** 118 | * Save products to wishlist 119 | * 120 | * @param int $product_id Product id. 121 | * @param int $user_id User id. 122 | * @param array $response Response. 123 | * 124 | * @return array $response Response. 125 | */ 126 | public function save_products_to_wishlist( int $product_id, int $user_id, array $response ) { 127 | 128 | // Check if the product id is valid else return error. 129 | $product = wc_get_product( $product_id ); 130 | if ( empty( $product ) ) { 131 | $response['error'] = __( 'Product does not exist', 'headless-cms' ); 132 | return $response; 133 | } 134 | 135 | // Get saved products of current user. 136 | $saved_products = (array) get_user_meta( $user_id, 'wishlist_saved_products', true ); 137 | $response['wishlistProductIds'] = array_filter( $saved_products ); 138 | 139 | // Check if the product already exists. 140 | if ( in_array( $product_id, $saved_products, true ) ) { 141 | if ( array_search( $product_id, $saved_products, true ) ) { 142 | $response['error'] = __( 'Product already exist', 'headless-cms' ); 143 | return $response; 144 | } 145 | } else { 146 | $saved_products[] = $product_id; 147 | } 148 | 149 | // Save product to current user. 150 | $save_product_to_user = update_user_meta( $user_id, 'wishlist_saved_products', $saved_products ); 151 | 152 | if ( !$save_product_to_user ) { 153 | $response['error'] = __( 'Something went wrong in adding the product to wishlist', 'headless-cms' ); 154 | 155 | return $response; 156 | } 157 | 158 | $response['added'] = true; 159 | $response['productId'] = $product_id; 160 | $response['wishlistProductIds'] = array_filter( $saved_products ); 161 | return $response; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /inc/classes/mutations/class-delete-wishlist.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Action 35 | */ 36 | 37 | // Register Delete wishlist Mutation Types. 38 | add_action( 'graphql_register_types', [ $this, 'delete_wishlist_mutation' ] ); 39 | 40 | } 41 | 42 | /** 43 | * 44 | * inputFields: expects an array of Fields to be used for inputting values to the mutation 45 | * 46 | * outputFields: expects an array of fields that can be asked for in response to the mutation 47 | * the resolve function is optional, but can be useful if the mutateAndPayload doesn't return an array 48 | * with the same key(s) as the outputFields 49 | * 50 | * mutateAndGetPayload: expects a function, and the function gets passed the $input, $context, and $info 51 | * the function should return enough info for the outputFields to resolve with 52 | * 53 | * @throws \Exception 54 | */ 55 | public function delete_wishlist_mutation() { 56 | register_graphql_mutation( 'removeFromWishlist', [ 57 | 'inputFields' => [ 58 | 'productId' => [ 59 | 'type' => 'Integer', 60 | 'description' => __( 'Product id', 'headless-cms' ), 61 | ], 62 | ], 63 | 64 | 'outputFields' => [ 65 | 'removed' => [ 66 | 'type' => 'Boolean', 67 | 'description' => __( 'True if the product is removed, false otherwise', 'headless-cms' ), 68 | ], 69 | 'productId' => [ 70 | 'type' => 'Integer', 71 | 'description' => __( 'The Product id that was deleted', 'headless-cms' ), 72 | ], 73 | 'wishlistProductIds' => [ 74 | 'type' => [ 'list_of' => 'Integer' ], 75 | 'description' => __( 'The Product ids in the wishlist', 'headless-cms' ), 76 | ], 77 | 'error' => [ 78 | 'type' => 'String', 79 | 'description' => __( 'Description of the error', 'headless-cms' ), 80 | ], 81 | ], 82 | 83 | 'mutateAndGetPayload' => function ( $input, $context, $info ) { 84 | 85 | $response = [ 86 | 'removed' => false, 87 | 'productId' => ! empty( $input['productId'] ) ? intval($input['productId']) : 0, 88 | 'error' => '', 89 | ]; 90 | 91 | if ( empty( $input['productId'] ) ) { 92 | $response['error'] = __( 'Please enter a valid product id', 'headless-cms' ); 93 | 94 | return $response; 95 | } 96 | 97 | $user_id = get_current_user_id(); 98 | 99 | if ( ! $user_id ) { 100 | $response['error'] = __( 'Request is not authenticated', 'headless-cms' ); 101 | 102 | return $response; 103 | } 104 | 105 | return $this->remove_item( $input['productId'], $user_id, $response ); 106 | }, 107 | ] ); 108 | } 109 | 110 | /** 111 | * Remove item from wishlist 112 | * 113 | * @since 1.0.0 114 | * 115 | */ 116 | public function remove_item( $product_id, $user_id, $response ) { 117 | 118 | $saved_products = (array) get_user_meta( $user_id, 'wishlist_saved_products', true ); 119 | $key = array_search( $product_id, $saved_products ); 120 | 121 | if ( ! $key ) { 122 | $response['error'] = __( 'Product does not exist in the wishlist', 'headless-cms' ); 123 | 124 | return $response; 125 | } 126 | 127 | unset( $saved_products[ $key ] ); 128 | $removed_product_from_wishlist = update_user_meta( $user_id, 'wishlist_saved_products', $saved_products ); 129 | 130 | if ( !$removed_product_from_wishlist ) { 131 | $response['error'] = __( 'Something went wrong in removing the product', 'headless-cms' ); 132 | 133 | return $response; 134 | } else { 135 | $response['removed'] = true; 136 | $response['productId'] = intval( $product_id ); 137 | $response['wishlistProductIds'] = array_filter( $saved_products ); 138 | 139 | return $response; 140 | } 141 | 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /inc/classes/queries/class-get-wishlist.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Action 35 | */ 36 | 37 | // Register Wishlist Field. 38 | add_action( 'graphql_register_types', [ $this, 'register_get_wishlist_fields' ] ); 39 | 40 | } 41 | 42 | /** 43 | * Register field. 44 | */ 45 | function register_get_wishlist_fields() { 46 | 47 | if ( !class_exists('WooCommerce') ) { 48 | return; 49 | } 50 | 51 | register_graphql_object_type( 'WishlistProductImage', [ 52 | 'fields' => [ 53 | 'attachmentId' => [ 'type' => 'Integer' ], 54 | 'src' => [ 'type' => 'String' ], 55 | 'alt' => [ 'type' => 'String' ], 56 | ], 57 | ] ); 58 | 59 | register_graphql_object_type( 'WishlistProduct', [ 60 | 'fields' => [ 61 | 'databaseId' => [ 'type' => 'Integer' ], 62 | 'name' => [ 'type' => 'String' ], 63 | 'slug' => [ 'type' => 'String' ], 64 | 'typename' => [ 'type' => 'String' ], 65 | 'priceHtml' => [ 'type' => 'String' ], 66 | 'image' => [ 'type' => 'WishlistProductImage' ], 67 | 'buttonText' => [ 'type' => 'String' ], 68 | 'productUrl' => [ 'type' => 'String' ], 69 | 'stockStatus' => [ 'type' => 'String' ], 70 | 'stockQuantity' => [ 'type' => 'Integer' ], 71 | ], 72 | ] ); 73 | 74 | register_graphql_object_type( 'WishlistProducts', [ 75 | 'description' => __( 'States Type', 'headless-cms' ), 76 | 'fields' => [ 77 | 'productIds' => [ 78 | 'type' => [ 79 | 'list_of' => 'Integer', 80 | ], 81 | ], 82 | 'products' => [ 'type' => [ 'list_of' => 'WishlistProduct' ] ], 83 | 'error' => [ 'type' => 'String' ], 84 | ], 85 | ] ); 86 | 87 | register_graphql_field( 88 | 'RootQuery', 89 | 'getWishList', 90 | [ 91 | 'description' => __( 'States', 'headless-cms' ), 92 | 'type' => 'WishlistProducts', 93 | 'resolve' => function ( $source, $args, $context, $info ) { 94 | $wishlist_products = [ 95 | 'productIds' => [], 96 | 'products' => [], 97 | ]; 98 | 99 | $user_id = get_current_user_id(); 100 | 101 | if ( ! $user_id ) { 102 | $wishlist_products['error'] = __( 'Request is not authenticated', 'headless-cms' ); 103 | 104 | return $wishlist_products; 105 | } 106 | 107 | $saved_product_ids = (array) get_user_meta( $user_id, 'wishlist_saved_products', true ); 108 | $wish_list_products = $this->prepare_wishlist_items_for_response( $saved_product_ids ); 109 | 110 | /** 111 | * Here you need to return data that matches the shape of the "WishlistProduct" type. You could get 112 | * the data from the WP Database, an external API, or static values. 113 | * For example in this case we are getting it from WordPress database. 114 | */ 115 | $wishlist_products['productIds'] = array_filter( $saved_product_ids ); 116 | $wishlist_products['products'] = $wish_list_products; 117 | 118 | return $wishlist_products; 119 | 120 | }, 121 | ] 122 | ); 123 | } 124 | 125 | /** 126 | * Get the wishlist products with required data. 127 | * 128 | * @param array $product_ids Product Ids 129 | * 130 | * @return array $wishlist_products Wishlist products. 131 | */ 132 | public function prepare_wishlist_items_for_response( array $product_ids ) { 133 | 134 | $type_list = [ 135 | 'simple' => 'SimpleProduct', 136 | 'variable' => 'VariableProduct', 137 | 'external' => 'ExternalProduct', 138 | 'group' => 'GroupProduct', 139 | ]; 140 | 141 | $wishlist_products = []; 142 | $args = [ 143 | 'include' => $product_ids, 144 | ]; 145 | $products = wc_get_products( $args ); 146 | 147 | if ( empty( $products ) || ! is_array( $products ) ) { 148 | return $wishlist_products; 149 | } 150 | 151 | foreach ( $products as $product ) { 152 | $product_data = []; 153 | $data = $product->get_data(); 154 | $stock_status = ! empty( $data['stock_status'] ) ? $data['stock_status'] : ''; 155 | $stock_status = 'instock' === $stock_status ? 'IN_STOCK' : $stock_status; 156 | $typename = $product->get_type(); 157 | $typename = ! empty( $typename ) ? $type_list[$typename] : ''; 158 | 159 | $product_data['databaseId'] = ! empty( $data['id'] ) ? $data['id'] : 0; 160 | $product_data['name'] = ! empty( $data['name'] ) ? $data['name'] : ''; 161 | $product_data['slug'] = ! empty( $data['slug'] ) ? $data['slug'] : ''; 162 | $product_data['typename'] = $typename; 163 | $product_data['priceHtml'] = $product->get_price_html(); 164 | $product_data['image'] = $this->get_image( $product, $data['name'] ); 165 | $product_data['buttonText'] = ! empty( $data['button_text'] ) ? $data['button_text'] : ''; 166 | $product_data['productUrl'] = ! empty( $data['product_url'] ) ? $data['product_url'] : ''; 167 | $product_data['stockStatus'] = $stock_status; 168 | $product_data['stockQuantity'] = intval( $data['stock_quantity'] ); 169 | 170 | // Push each product into the wishlist products array. 171 | $wishlist_products[] = $product_data; 172 | } 173 | 174 | return $wishlist_products; 175 | } 176 | 177 | 178 | /** 179 | * Get the featured image for a product 180 | * 181 | * @param object $product Product 182 | * @param string $product_name Product name 183 | * 184 | * @return array Featured image. 185 | */ 186 | protected function get_image( object $product, string $product_name ) { 187 | $attachment_id = $product->get_image_id() ? $product->get_image_id() : 0; 188 | 189 | // Set a placeholder image if the product has no images set. 190 | if ( empty( $attachment_id ) ) { 191 | return [ 192 | 'attachmentId' => 0, 193 | 'src' => wc_placeholder_img_src(), 194 | 'alt' => $product_name, 195 | ]; 196 | } 197 | 198 | $attachment = wp_get_attachment_image_src( $attachment_id, 'full' ); 199 | $altText = get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); 200 | 201 | return [ 202 | 'attachmentId' => (int) $attachment_id, 203 | 'src' => current( $attachment ), 204 | 'alt' => ! empty( $altText ) ? $altText : $product_name, 205 | ]; 206 | } 207 | 208 | } 209 | -------------------------------------------------------------------------------- /inc/classes/queries/class-header-footer-schema.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Action 35 | */ 36 | 37 | // Register Header Type and Field. 38 | add_action( 'graphql_register_types', [ $this, 'register_header_type' ] ); 39 | add_action( 'graphql_register_types', [ $this, 'register_header_field' ] ); 40 | 41 | // Register Social Links Type. 42 | add_action( 'graphql_register_types', [ $this, 'register_social_links_type' ] ); 43 | 44 | // Register Footer Type and Field. 45 | add_action( 'graphql_register_types', [ $this, 'register_footer_type' ] ); 46 | add_action( 'graphql_register_types', [ $this, 'register_footer_field' ] ); 47 | 48 | } 49 | 50 | /** 51 | * Register header type. 52 | */ 53 | public function register_header_type() { 54 | register_graphql_object_type( 55 | 'HCMSHeader', 56 | [ 57 | 'description' => __( 'Header Type', 'headless-cms' ), 58 | 'fields' => [ 59 | 'siteLogoUrl' => [ 60 | 'type' => 'String', 61 | 'description' => __( 'Site logo URL', 'headless-cms' ), 62 | ], 63 | 'siteTitle' => [ 64 | 'type' => 'String', 65 | 'description' => __( 'Site title', 'headless-cms' ), 66 | ], 67 | 'siteTagLine' => [ 68 | 'type' => 'String', 69 | 'description' => __( 'Site tagline', 'headless-cms' ), 70 | ], 71 | 'favicon' => [ 72 | 'type' => 'String', 73 | 'description' => __( 'favicon', 'headless-cms' ), 74 | ], 75 | ], 76 | ] 77 | ); 78 | } 79 | 80 | /** 81 | * Register header field 82 | */ 83 | public function register_header_field() { 84 | 85 | register_graphql_field( 86 | 'RootQuery', 87 | 'getHeader', 88 | [ 89 | 'description' => __( 'Get header', 'headless-cms' ), 90 | 'type' => 'HCMSHeader', 91 | 'resolve' => function () { 92 | 93 | /** 94 | * Here you need to return data that matches the shape of the "HCMSHeader" type. You could get 95 | * the data from the WP Database, an external API, or static values. 96 | * For example in this case we are getting it from WordPress database. 97 | */ 98 | return [ 99 | 'siteLogoUrl' => $this->get_logo_url( 'custom_logo' ), 100 | 'siteTitle' => get_bloginfo( 'title' ), 101 | 'siteTagLine' => get_bloginfo( 'description' ), 102 | 'favicon' => get_site_icon_url(), 103 | ]; 104 | 105 | }, 106 | ] 107 | ); 108 | 109 | } 110 | 111 | /** 112 | * Register footer type. 113 | */ 114 | public function register_footer_type() { 115 | register_graphql_object_type( 116 | 'HCMSFooter', 117 | [ 118 | 'description' => __( 'Header Type', 'headless-cms' ), 119 | 'fields' => [ 120 | 'copyrightText' => [ 121 | 'type' => 'String', 122 | 'description' => __( 'Copyright text', 'headless-cms' ), 123 | ], 124 | 'socialLinks' => [ 125 | 'type' => [ 'list_of' => 'HCMSSocialLinks' ], 126 | 'description' => __( 'Social links', 'headless-cms' ), 127 | ], 128 | 'sidebarOne' => [ 129 | 'type' => 'String', 130 | 'description' => __( 'sidebarOne', 'headless-cms' ), 131 | ], 132 | 'sidebarTwo' => [ 133 | 'type' => 'String', 134 | 'description' => __( 'sidebarTwo', 'headless-cms' ), 135 | ], 136 | ], 137 | ] 138 | ); 139 | } 140 | 141 | /** 142 | * Register footer field 143 | */ 144 | public function register_footer_field() { 145 | 146 | register_graphql_field( 147 | 'RootQuery', 148 | 'getFooter', 149 | [ 150 | 'description' => __( 'Get footer', 'headless-cms' ), 151 | 'type' => 'HCMSFooter', 152 | 'resolve' => function () { 153 | 154 | /** 155 | * Here you need to return data that matches the shape of the "HCMSFooter" type. You could get 156 | * the data from the WP Database, an external API, or static values. 157 | * For example in this case we are getting it from WordPress database. 158 | */ 159 | return [ 160 | 'copyrightText' => $this->get_copyright_text(), 161 | 'socialLinks' => $this->get_social_icons(), 162 | 'sidebarOne' => $this->get_sidebar( 'hcms-footer-sidebar-1' ), 163 | 'sidebarTwo' => $this->get_sidebar( 'hcms-footer-sidebar-2' ), 164 | ]; 165 | 166 | }, 167 | ] 168 | ); 169 | 170 | } 171 | 172 | /** 173 | * Register social links field 174 | */ 175 | public function register_social_links_type() { 176 | register_graphql_object_type( 177 | 'HCMSSocialLinks', 178 | [ 179 | 'description' => __( 'Social Links Type', 'headless-cms' ), 180 | 'fields' => [ 181 | 'iconName' => [ 182 | 'type' => 'String', 183 | 'description' => __( 'Icon name', 'headless-cms' ), 184 | ], 185 | 'iconUrl' => [ 186 | 'type' => 'String', 187 | 'description' => __( 'Icon url', 'headless-cms' ), 188 | ], 189 | ], 190 | ] 191 | ); 192 | } 193 | 194 | /** 195 | * Get logo URL. 196 | * 197 | * @param string $key Key. 198 | * 199 | * @return string Image. 200 | */ 201 | public function get_logo_url( $key ) { 202 | 203 | $custom_logo_id = get_theme_mod( $key ); 204 | $image = wp_get_attachment_image_src( $custom_logo_id, 'full' ); 205 | 206 | return $image[0]; 207 | } 208 | 209 | /** 210 | * Get social icons 211 | * 212 | * @return array $social_icons 213 | */ 214 | public function get_social_icons() { 215 | 216 | $social_icons = []; 217 | $social_icons_name = [ 'facebook', 'twitter', 'instagram', 'youtube' ]; 218 | 219 | foreach ( $social_icons_name as $social_icon_name ) { 220 | 221 | $social_link = get_theme_mod( sprintf( 'rae_%s_link', $social_icon_name ) ); 222 | 223 | if ( $social_link ) { 224 | array_push( 225 | $social_icons, 226 | [ 227 | 'iconName' => esc_attr( $social_icon_name ), 228 | 'iconUrl' => esc_url( $social_link ), 229 | ] 230 | ); 231 | } 232 | } 233 | 234 | return $social_icons; 235 | 236 | } 237 | 238 | /** 239 | * Get copyright text 240 | * 241 | * @return mixed 242 | */ 243 | public function get_copyright_text() { 244 | 245 | $copy_right_text = get_theme_mod( 'rae_footer_text' ); 246 | 247 | return $copy_right_text ? $copy_right_text : ''; 248 | } 249 | 250 | /** 251 | * Returns the content of all the sidebars with given sidebar id. 252 | * 253 | * @param string $sidebar_id Sidebar id. 254 | * 255 | * @return false|string 256 | */ 257 | public function get_sidebar( $sidebar_id ) { 258 | ob_start(); 259 | 260 | dynamic_sidebar( $sidebar_id ); 261 | $output = ob_get_contents(); 262 | 263 | ob_end_clean(); 264 | 265 | return $output; 266 | } 267 | 268 | } 269 | -------------------------------------------------------------------------------- /inc/classes/queries/class-post-schema.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 26 | } 27 | 28 | /** 29 | * To setup action/filter. 30 | * 31 | * @return void 32 | */ 33 | protected function setup_hooks() { 34 | 35 | /** 36 | * Actions 37 | */ 38 | add_action( 'graphql_register_types', [ $this, 'register_graphql_fields' ] ); 39 | 40 | } 41 | 42 | /** 43 | * Register field. 44 | */ 45 | public function register_graphql_fields() { 46 | 47 | // coAuthors 48 | register_graphql_field( 49 | 'Post', 50 | 'coAuthors', 51 | [ 52 | 'type' => 'String', 53 | 'description' => __( 'Co Authors', 'headless-cms' ), 54 | 'resolve' => function ($post) { 55 | return function_exists( 'get_coauthors' ) ? wp_json_encode( get_coauthors( $post->ID ) ) : ''; 56 | }, 57 | ] 58 | ); 59 | 60 | // Register bodyClasses type for Posts. 61 | register_graphql_field( 62 | 'Post', 63 | 'bodyClasses', 64 | [ 65 | 'type' => 'String', 66 | 'description' => __( 'bodyClasses', 'headless-cms' ), 67 | 'resolve' => function ($post) { 68 | return $this->get_body_classes($post); 69 | }, 70 | ] 71 | ); 72 | 73 | // Register bodyClasses type for Page. 74 | register_graphql_field( 75 | 'Page', 76 | 'bodyClasses', 77 | [ 78 | 'type' => 'String', 79 | 'description' => __( 'bodyClasses', 'headless-cms' ), 80 | 'resolve' => function ($post) { 81 | return $this->get_body_classes($post); 82 | }, 83 | ] 84 | ); 85 | } 86 | 87 | /** 88 | * Get body classes including elementor classes. 89 | * 90 | * @param Object $post Post. 91 | * 92 | * @return string Body classes. 93 | */ 94 | public function get_body_classes( $post ) { 95 | $body_classes = array_merge( [], get_body_class() ); 96 | $body_classes = implode( ' ', $body_classes ); 97 | $elementor_kit_post = get_page_by_title('Default Kit', OBJECT, 'elementor_library'); 98 | $elementor_kit_post_id = ! empty( $elementor_kit_post ) ? $elementor_kit_post->ID : ''; 99 | $body_classes = $body_classes . sprintf( ' elementor-default elementor-kit-%1$s elementor-page elementor-page-%2$s', $elementor_kit_post_id, $post->ID ); 100 | return $body_classes; 101 | } 102 | 103 | } 104 | 105 | -------------------------------------------------------------------------------- /inc/classes/queries/class-product.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Action 35 | */ 36 | // Register Product Fields. 37 | add_action( 'graphql_register_types', [ $this, 'register_product_fields' ] ); 38 | } 39 | 40 | /** 41 | * Register field. 42 | */ 43 | function register_product_fields() { 44 | if ( !class_exists('WooCommerce') ) { 45 | return; 46 | } 47 | register_graphql_field( 48 | 'Product', 49 | 'productCurrency', 50 | [ 51 | 'description' => __( 'Product Currency', 'headless-cms' ), 52 | 'type' => 'String', 53 | 'resolve' => function () { 54 | return get_woocommerce_currency(); 55 | }, 56 | ] 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /inc/classes/queries/class-register-countries.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Action 35 | */ 36 | 37 | // Register Countries Field. 38 | add_action( 'graphql_register_types', [ $this, 'register_countries_fields' ] ); 39 | 40 | } 41 | 42 | /** 43 | * Register field. 44 | */ 45 | public function register_countries_fields() { 46 | 47 | register_graphql_object_type( 'WooCountry', [ 48 | 'fields' => [ 49 | 'countryCode' => [ 'type' => 'String' ], 50 | 'countryName' => [ 'type' => 'String' ], 51 | ] 52 | ] ); 53 | 54 | register_graphql_object_type( 'WooCountries', [ 55 | 'description' => __( 'Countries Type', 'headless-cms' ), 56 | 'fields' => [ 57 | 'billingCountries' => [ 58 | 'type' => [ 59 | 'list_of' => 'WooCountry' 60 | ] 61 | ], 62 | 'shippingCountries' => [ 63 | 'type' => [ 64 | 'list_of' => 'WooCountry' 65 | ] 66 | ], 67 | ], 68 | ] ); 69 | 70 | register_graphql_field( 71 | 'RootQuery', 72 | 'wooCountries', 73 | [ 74 | 'description' => __( 'Countries', 'headless-cms' ), 75 | 'type' => 'WooCountries', 76 | 'resolve' => function () { 77 | 78 | // All countries for billing. 79 | $all_countries = class_exists( 'WooCommerce' ) ? WC()->countries : []; 80 | $billing_countries = ! empty( $all_countries->countries ) ? $all_countries->countries : []; 81 | $billing_countries = $this->get_formatted_countries( $billing_countries ); 82 | 83 | // All countries with states for shipping. 84 | $shipping_countries = class_exists( 'WooCommerce' ) ? WC()->countries->get_shipping_countries() : [];; 85 | $shipping_countries = ! empty( $shipping_countries ) ? $shipping_countries : []; 86 | $shipping_countries = $this->get_formatted_countries( $shipping_countries ); 87 | 88 | /** 89 | * Here you need to return data that matches the shape of the "WooCountries" type. You could get 90 | * the data from the WP Database, an external API, or static values. 91 | * For example in this case we are getting it from WordPress database. 92 | */ 93 | return [ 94 | 'billingCountries' => $billing_countries, 95 | 'shippingCountries' => $shipping_countries, 96 | ]; 97 | 98 | }, 99 | ] 100 | ); 101 | } 102 | 103 | public function get_formatted_countries( $countries ) { 104 | 105 | $formatted_countries = []; 106 | 107 | if ( empty( $countries ) && !is_array( $countries ) ) { 108 | return $formatted_countries; 109 | } 110 | 111 | foreach ( $countries as $countryCode => $countryName ) { 112 | array_push( $formatted_countries, [ 113 | 'countryCode' => $countryCode, 114 | 'countryName' => $countryName, 115 | ] ); 116 | } 117 | 118 | return $formatted_countries; 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /inc/classes/queries/class-register-shipping.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Action 35 | */ 36 | 37 | // Register Shipping Zones fields. 38 | add_action( 'graphql_register_types', [ $this, 'register_shipping_zones_fields' ] ); 39 | 40 | } 41 | 42 | /** 43 | * Register field. 44 | */ 45 | function register_shipping_zones_fields() { 46 | 47 | register_graphql_object_type( 'ShippingInfo', [ 48 | 'description' => __( 'Shipping Zones Type', 'headless-cms' ), 49 | 'fields' => [ 50 | 'shippingZones' => [ 'type' => 'String' ], 51 | 'storePostCode' => [ 'type' => 'Integer' ], 52 | ] 53 | ] ); 54 | 55 | register_graphql_field( 56 | 'RootQuery', 57 | 'shippingInfo', 58 | [ 59 | 'description' => __( 'Shipping Zones', 'headless-cms' ), 60 | 'type' => 'ShippingInfo', 61 | 'resolve' => function () { 62 | 63 | $zone_locations = $this->get_zone_locations(); 64 | $store_post_code = class_exists('WooCommerce') ? WC()->countries->get_base_postcode() : 0; 65 | 66 | /** 67 | * Here you need to return data that matches the shape of the "ShippingInfo" type. You could get 68 | * the data from the WP Database, an external API, or static values. 69 | * For example in this case we are getting it from WordPress database. 70 | */ 71 | return [ 72 | 'shippingZones' => wp_json_encode($zone_locations), 73 | 'storePostCode' => intval( $store_post_code ), 74 | ]; 75 | 76 | }, 77 | ] 78 | ); 79 | } 80 | 81 | public function get_store_address() { 82 | $store_address = ''; 83 | if( class_exists( 'WC_Countries' ) ) { 84 | $store_address = get_option( 'woocommerce_store_address' ); 85 | } 86 | 87 | return $store_address; 88 | } 89 | 90 | /** 91 | * Get Zone locations 92 | * 93 | * @return array $zone_locations Zone locations. 94 | */ 95 | public function get_zone_locations() { 96 | $zone_locations = []; 97 | 98 | if( class_exists( 'WC_Shipping_Zones' ) ) { 99 | $all_zones = \WC_Shipping_Zones::get_zones(); 100 | if ( ! empty( $all_zones ) && is_array( $all_zones ) ) { 101 | foreach ((array) $all_zones as $key => $the_zone ) { 102 | 103 | $zone_info = [ 104 | 'zone_name' => $the_zone['zone_name'], 105 | 'country_names' => $the_zone['formatted_zone_location'], 106 | 'zone_location_codes' => $the_zone['zone_locations'], 107 | 'shipping_methods' => $this->get_shipping_methods( $the_zone['shipping_methods'] ), 108 | ]; 109 | array_push($zone_locations, $zone_info); 110 | 111 | } 112 | } 113 | } 114 | 115 | return $zone_locations; 116 | } 117 | 118 | /** 119 | * Get Shipping methods. 120 | * 121 | * @param array $shipping_methods_data Shipping method data. 122 | * 123 | * @return array Shipping methods. 124 | */ 125 | public function get_shipping_methods($shipping_methods_data) { 126 | 127 | $shipping_methods = []; 128 | 129 | if ( empty( $shipping_methods_data ) || !is_array( $shipping_methods_data ) ) { 130 | return $shipping_methods; 131 | } 132 | 133 | foreach ((array) $shipping_methods_data as $key => $shipping_method ) { 134 | array_push($shipping_methods, [ 135 | 'method_title' => ! empty($shipping_method->instance_settings['title']) ? $shipping_method->instance_settings['title'] : '', 136 | ]); 137 | } 138 | 139 | return $shipping_methods; 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /inc/classes/queries/class-register-states.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Action 35 | */ 36 | 37 | // Register States Field. 38 | add_action( 'graphql_register_types', [ $this, 'register_states_fields' ] ); 39 | 40 | } 41 | 42 | /** 43 | * Register field. 44 | */ 45 | function register_states_fields() { 46 | 47 | register_graphql_object_type( 'WooState', [ 48 | 'fields' => [ 49 | 'stateCode' => [ 'type' => 'String' ], 50 | 'stateName' => [ 'type' => 'String' ], 51 | ], 52 | ] ); 53 | 54 | register_graphql_object_type( 'WooStates', [ 55 | 'description' => __( 'States Type', 'headless-cms' ), 56 | 'fields' => [ 57 | 'states' => [ 58 | 'type' => [ 59 | 'list_of' => 'WooState' 60 | ] 61 | ], 62 | ] 63 | ] ); 64 | 65 | register_graphql_field( 66 | 'RootQuery', 67 | 'wooStates', 68 | [ 69 | 'description' => __( 'States', 'headless-cms' ), 70 | 'type' => 'WooStates', 71 | 'args' => [ 72 | 'countryCode' => [ 73 | 'type' => 'String', 74 | ], 75 | ], 76 | 'resolve' => function ( $source, $args, $context, $info ) { 77 | $states = []; 78 | 79 | if ( ! class_exists( 'WooCommerce' ) ) { 80 | return $states; 81 | } 82 | 83 | $states = isset( $args['countryCode'] ) && ! empty( $args['countryCode'] ) ? WC()->countries->get_states( strtoupper($args['countryCode']) ) : []; 84 | $states = $this->get_formatted_states( $states ); 85 | 86 | /** 87 | * Here you need to return data that matches the shape of the "WooStates" type. You could get 88 | * the data from the WP Database, an external API, or static values. 89 | * For example in this case we are getting it from WordPress database. 90 | */ 91 | return [ 92 | 'states' => $states, 93 | ]; 94 | 95 | }, 96 | ] 97 | ); 98 | } 99 | 100 | public function get_formatted_states( $states ) { 101 | 102 | $formatted_states = []; 103 | 104 | if ( empty( $states ) && !is_array( $states ) ) { 105 | return $formatted_states; 106 | } 107 | 108 | foreach ( $states as $stateCode => $stateName ) { 109 | array_push( $formatted_states, [ 110 | 'stateCode' => $stateCode, 111 | 'stateName' => $stateName, 112 | ] ); 113 | } 114 | 115 | return $formatted_states; 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /inc/classes/queries/class-seo.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 26 | } 27 | 28 | /** 29 | * To setup action/filter. 30 | * 31 | * @return void 32 | */ 33 | protected function setup_hooks() { 34 | 35 | /** 36 | * Actions 37 | */ 38 | add_action( 'graphql_register_types', [ $this, 'register_graphql_fields' ] ); 39 | 40 | } 41 | 42 | /** 43 | * Register field. 44 | */ 45 | public function register_graphql_fields() { 46 | 47 | // Register type 'schemaDetails'. 48 | register_graphql_field( 49 | 'PostTypeSEO', 50 | 'schemaDetails', 51 | [ 52 | 'type' => 'String', 53 | 'description' => esc_html__( 'Yoast SEO Schema', 'headless-cms' ), 54 | 'resolve' => function( $root, $args, $context, $info ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable 55 | 56 | $post_id = get_the_ID(); // Current post or page id. 57 | $post_type = get_post_type( $post_id ); // Current post type. 58 | $yoast_meta = \YoastSEO()->meta; 59 | 60 | if ( is_home() || is_front_page() ) { 61 | // Get schema for home page. 62 | $output = $yoast_meta->for_home_page()->schema; 63 | } elseif ( 'post' === $post_type ) { 64 | // Get schema for post. Only post type 'post'. 65 | $output = $yoast_meta->for_post( $post_id )->schema; 66 | } else { 67 | // Get schema for all other page or post. 68 | $output = $yoast_meta->for_current_page()->schema; 69 | } 70 | 71 | if ( ! empty( $output ) ) { 72 | $output = wp_json_encode( $output, JSON_UNESCAPED_SLASHES ); 73 | $output = $this->replace_backend_url( $output ); 74 | } 75 | 76 | return $output; 77 | }, 78 | ] 79 | ); 80 | } 81 | 82 | /** 83 | * Function to replace backend URL with frontend. 84 | * 85 | * @param string $output String to replace backend URL. 86 | * 87 | * @return string 88 | */ 89 | public function replace_backend_url( $output ) { 90 | 91 | $plugin_options = get_option('hcms_plugin_options'); 92 | $frontend_url = is_array( $plugin_options ) && ! empty( $plugin_options['frontend_site_url'] ) ? esc_url( $plugin_options['frontend_site_url'] ) : ''; 93 | 94 | if ( ! empty( $frontend_url ) ) { 95 | $frontend_url = untrailingslashit( $frontend_url ); 96 | $output = str_replace( home_url(), $frontend_url, $output ); 97 | } 98 | 99 | return $output; 100 | 101 | } 102 | 103 | } 104 | 105 | -------------------------------------------------------------------------------- /inc/classes/queries/class-sticky-post.php: -------------------------------------------------------------------------------- 1 | setup_hooks(); 24 | } 25 | 26 | /** 27 | * To setup action/filter. 28 | * 29 | * @return void 30 | */ 31 | protected function setup_hooks() { 32 | 33 | /** 34 | * Actions 35 | */ 36 | add_action( 'graphql_register_types', [ $this, 'add_sticky_where_argument' ] ); 37 | 38 | /** 39 | * Filters 40 | */ 41 | add_filter( 'graphql_post_object_connection_query_args', [ $this, 'add_sticky_argument_condition' ], 10, 5 ); 42 | 43 | } 44 | 45 | /** 46 | * Function to register WPGraphql field. 47 | * 48 | * @param TypeRegistry $type_registry Instance of the TypeRegistry 49 | * 50 | * @return void 51 | */ 52 | public function add_sticky_where_argument( $type_registry ) { 53 | 54 | register_graphql_field( 55 | 'RootQueryToPostConnectionWhereArgs', 56 | 'onlySticky', 57 | [ 58 | 'type' => 'boolean', 59 | 'description' => esc_html__( 'The ID of the post object to filter by', 'headless-cms' ), 60 | ] 61 | ); 62 | 63 | } 64 | 65 | /** 66 | * Function to add custom argument condition in WP_Query args. 67 | * 68 | * @param array $query_args Query arguments. 69 | * @param mixed $source The source that's passed down the GraphQL queries 70 | * @param array $args The inputArgs on the field 71 | * @param AppContext $context The AppContext passed down the GraphQL tree 72 | * @param ResolveInfo $info The ResolveInfo passed down the GraphQL tree 73 | * 74 | * @return array 75 | */ 76 | public function add_sticky_argument_condition( $query_args, $source, $args, $context, $info ) { 77 | 78 | // Add condition if onlySticky argument is set. 79 | if ( 80 | ! empty( $args['where'] ) && 81 | isset( $args['where']['onlySticky'] ) 82 | ) { 83 | 84 | if ( true === $args['where']['onlySticky'] ) { 85 | $query_args['post__in'] = get_option( 'sticky_posts', [] ); 86 | } else { 87 | $query_args['post__not_in'] = get_option( 'sticky_posts', [] ); 88 | } 89 | } 90 | 91 | return $query_args; 92 | 93 | } 94 | 95 | 96 | } 97 | -------------------------------------------------------------------------------- /inc/helpers/autoloader.php: -------------------------------------------------------------------------------- 1 | esc_html__( 'HCMS Header Menu', 'headless-cms' ), 44 | 'hcms-menu-footer' => esc_html__( 'HCMS Footer Menu', 'headless-cms' ), 45 | ] 46 | ); 47 | } 48 | add_action( 'init', 'hcms_custom_new_menu' ); 49 | 50 | /** 51 | * Register Sidebar 52 | */ 53 | 54 | /** 55 | * Register widget areas. 56 | * 57 | * @link https://developer.wordpress.org/themes/functionality/sidebars/#registering-a-sidebar 58 | */ 59 | function hcms_sidebar_registration() { 60 | 61 | // Arguments used in all register_sidebar() calls. 62 | $shared_args = [ 63 | 'before_title' => '

', 64 | 'after_title' => '

', 65 | 'before_widget' => '
', 66 | 'after_widget' => '
', 67 | ]; 68 | 69 | // Footer #1. 70 | register_sidebar( 71 | array_merge( 72 | $shared_args, 73 | [ 74 | 'name' => __( 'HCMS Footer #1', 'headless-cms' ), 75 | 'id' => 'hcms-footer-sidebar-1', 76 | 'description' => __( 'Widgets in this area will be displayed in the first column in the footer.', 'headless-cms' ), 77 | ] 78 | ) 79 | ); 80 | 81 | // Footer #2. 82 | register_sidebar( 83 | array_merge( 84 | $shared_args, 85 | [ 86 | 'name' => __( 'HCMS Footer #2', 'headless-cms' ), 87 | 'id' => 'hcms-footer-sidebar-2', 88 | 'description' => __( 'Widgets in this area will be displayed in the second column in the footer.', 'headless-cms' ), 89 | ] 90 | ) 91 | ); 92 | 93 | } 94 | 95 | add_action( 'widgets_init', 'hcms_sidebar_registration' ); 96 | 97 | /** 98 | * Add theme supports 99 | */ 100 | function hcms_theme_support() { 101 | 102 | if ( function_exists( 'add_theme_support' ) ) { 103 | /* 104 | * Enable support for Post Thumbnails on posts and pages. 105 | * 106 | * @link https://developer.wordpress.org/themes/functionality/featured-images-post-thumbnails/ 107 | */ 108 | add_theme_support( 'post-thumbnails' ); 109 | 110 | // Set post thumbnail size. 111 | set_post_thumbnail_size( 1200, 9999 ); 112 | 113 | // Add support for full and wide align images. 114 | add_theme_support( 'align-wide' ); 115 | } 116 | 117 | } 118 | 119 | add_action( 'after_setup_theme', 'hcms_theme_support' ); 120 | 121 | /** 122 | * Back to React Theme's home page. 123 | */ 124 | function hcms_back_to_home_button() { 125 | 126 | $frontend_site_url = ! empty( $option_val_array['frontend_site_url'] ) ? $option_val_array['frontend_site_url'] : 'https://gatsby-woocommerce-theme.netlify.app'; 127 | 128 | printf( 129 | '%2$s', 130 | esc_url( $frontend_site_url ), 131 | __('Back to Home', 'headless-cms') 132 | ); 133 | 134 | } 135 | 136 | add_action( 'woocommerce_order_details_after_order_table', 'hcms_back_to_home_button', 10 ); 137 | 138 | add_filter( 'graphql_jwt_auth_secret_key', function() { 139 | $plugin_options = get_option( 'hcms_plugin_options' ); 140 | if ( ! is_array($plugin_options) && empty( $plugin_options['jwt_secret'] ) ) { 141 | return ''; 142 | } 143 | 144 | return $plugin_options['jwt_secret']; 145 | }); 146 | -------------------------------------------------------------------------------- /inc/traits/trait-singleton.php: -------------------------------------------------------------------------------- 1 | value pair for each `classname => instance` in self::$_instance 70 | * for each sub-class. 71 | */ 72 | $called_class = get_called_class(); 73 | 74 | if ( ! isset( $instance[ $called_class ] ) ) { 75 | 76 | $instance[ $called_class ] = new $called_class(); 77 | 78 | /** 79 | * Dependent items can use the headless_cms_features_singleton_init_{$called_class} hook to execute code 80 | */ 81 | do_action( sprintf( 'headless_cms_features_singleton_init_%s', $called_class ) ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores 82 | 83 | } 84 | 85 | return $instance[ $called_class ]; 86 | 87 | } 88 | 89 | } // End trait 90 | -------------------------------------------------------------------------------- /phpcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WordPress and VIP Go Coding Standards 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | */node_modules/* 24 | */vendor/* 25 | .github/ 26 | 27 | 28 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Headless CMS === 2 | Contributors: gsayed786 3 | Tags: headless-cms, decoupled, graphql 4 | Requires at least: 4.6 5 | Tested up to: 5.4.2 6 | Stable tag: 4.9.2 7 | Requires PHP: 5.2.4 8 | License: GPLv2 or later 9 | License URI: https://www.gnu.org/licenses/gpl-2.0.html 10 | 11 | A WordPress plugin that adds features to use WordPress as a headless CMS with any front-end environment using REST API. 12 | 13 | == Description == 14 | 15 | A WordPress plugin that adds following features to use WordPress as a headless CMS with any front-end environment using REST API 16 | This plugin provides multiple features and you can use the one's that is relevant to your front-end application. You don't necessarily need to use all. 17 | 18 | == Features == 19 | 20 | 1. Custom REST API Endpoints. 21 | 2. Social links in customizer. 22 | 3. Image uploads for categories. 23 | 4. Custom header and footer menus. 24 | 5. Custom Widgets. 25 | 6. Custom Header and Footer GraphQL fields when using [wp-graphql](https://github.com/wp-graphql/wp-graphql) plugin 26 | 27 | == Feature Details == 28 | 29 | ## Features 30 | * Adds option to add social links in customizer 31 | * Registers two custom menus for header ( menu location = hcms-menu-header ) and for footer ( menu location = hcms-menu-footer ) 32 | * Registers the following sidebars 33 | 1. HCMS Footer #1 with sidebar id 'hcms-sidebar-1' 34 | 2. HCMS Footer #2 with sidebar id 'hcms-sidebar-2' 35 | 36 | == Available Custom REST API endpoints == 37 | 1. Get single post ( GET request ): `http://example.com/wp-json/rae/v1/post?post_id=1` 38 | 39 | 2. Get posts by page no: ( GET Request ) : `http://example.com/wp-json/rae/v1/posts?page_no=1` 40 | 41 | 3. Get header and footer date: ( GET Request ) 42 | * Get the header data ( site title, site description , site logo URL, menu items ) and footer data ( footer menu items, social icons ) 43 | * `http://example.com/wp-json/rae/v1/header-footer?header_location_id=hcms-menu-header&footer_location_id=hcms-menu-footer`` 44 | 45 | 4. Get posts by page no: ( GET Request ) 46 | * Get the posts by taxonomy 47 | * `http://example.com/wp-json/rae/v1/posts-by-tax?post_type=post&taxonomy=category&slug=xyz` 48 | 49 | == More Features == 50 | 1. Registers the sections for socials icons in the customizer 51 | 52 | * Social icons urls for 'facebook', 'twitter', 'instagram', 'youtube' 53 | 54 | 2. Image upload features for categories 55 | 56 | * Provides Image upload features for categories. 57 | 58 | 3. Plugin Settings Page 59 | 60 | * Settings for getting data for a custom page like Hero section, Search section, Featured post section, latest posts heading. 61 | 62 | == Installation and Use == 63 | 64 | This section describes how to install the plugin and get it working. 65 | 66 | 1. Upload the plugin files to the `/wp-content/plugins/plugin-name` directory, or install the plugin through the WordPress plugins screen directly. 67 | 2. Activate the plugin through the 'Plugins' screen in WordPress 68 | 3. Your can add social icons from customizer 69 | 4. You can set up custom header and footer menus. 70 | 5. You can add image to categories. 71 | 72 | == Demo of the Frontend applications that can be used with this plugin == 73 | 74 | Please check the demo of an example React front-end application, where this plugin can be used. 75 | 76 | [2020-07-02] Demo. 77 | 78 | [youtube https://youtu.be/nYXL1KKjKrc] 79 | 80 | = Its not working. 81 | 82 | Step 1. Check if your Plugin is activated. 83 | Step 2. Deactivate all plugins and reactivate headless-cms. 84 | 85 | == Screenshots == 86 | 87 | 1-Plugin Settings. screenshot-1.png 88 | 2-GraphQL Fields. screenshot-2.png 89 | 3-Category Image Upload. screenshot-3.png 90 | 4-Custom Header Menu. screenshot-4.png 91 | 5-Custom Footer Menu. screenshot-5.png 92 | -------------------------------------------------------------------------------- /templates/category-img-form.php: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 | 13 |
14 |

15 | 16 | 17 |

18 |
19 | -------------------------------------------------------------------------------- /templates/comments-section.php: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |
17 | 18 |

19 | 20 | 21 | 22 | /> 23 |

24 |
25 | 26 |
27 |
28 | -------------------------------------------------------------------------------- /templates/featured-post-section.php: -------------------------------------------------------------------------------- 1 | 'post', 19 | 'post_status' => 'publish', 20 | 'orderby' => 'date', 21 | 'update_post_meta_cache' => false, 22 | 'update_post_term_cache' => false, 23 | 24 | ]; 25 | 26 | $latest_posts_data = new WP_Query( $args ); 27 | $latest_posts = ! empty( $latest_posts_data->posts ) ? $latest_posts_data->posts : []; 28 | ?> 29 | 30 | 31 |
32 | 33 |

34 | 35 | 36 | 37 | 38 | 39 |

40 | 41 | 42 | 43 | 59 |
60 | 61 | 62 | 63 | 79 |
80 | 81 | 82 | 83 | 99 |
100 |
101 | 102 |
103 | -------------------------------------------------------------------------------- /templates/frontend-site-details-section.php: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | 17 |
18 | 19 |

20 |

http://localhost:3000

21 |

https://example.com

22 | 23 | 24 | 25 | 26 |
27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /templates/hero-section.php: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 | 22 |
23 | 24 |

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |

40 | left site logo 41 | 42 | 43 | 44 |
45 | 46 |
47 |
48 | -------------------------------------------------------------------------------- /templates/latest-posts-section.php: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | 17 |
18 | 19 |

20 | 21 | 22 | 23 | 24 |
25 | -------------------------------------------------------------------------------- /templates/post-preview-section.php: -------------------------------------------------------------------------------- 1 | k|7iFfGuH+0#oPTLXiG@8r-'; 14 | ?> 15 | 16 |
17 | 18 |
19 | 20 |

21 | 22 | 23 | 24 | /> 25 |
26 | 27 | 28 |
29 | 30 |

31 |

32 | 33 | https://api.wordpress.org/secret-key/1.1/salt/ 34 |

35 |

wQ2gp%lGB(T0~!eV?FJg3M+tdd-RddLFa2rH_ Lou>k|7iFfGuH+0#oPTLXiG@8r-

36 | 37 | 38 | 39 | 40 |
41 | 42 |
43 |
44 | -------------------------------------------------------------------------------- /templates/search-section.php: -------------------------------------------------------------------------------- 1 | 18 | 19 | 20 |
21 | 22 |

23 | 24 | 25 | 26 | 27 | 28 | 29 |

30 | right site logo 31 | 32 | 33 | 34 |
35 | 36 |
37 |
38 | -------------------------------------------------------------------------------- /templates/settings-form-template.php: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 12 |
13 |

14 |

15 |
16 | 17 |
18 | $option_val_array, 27 | ] 28 | ); 29 | 30 | headless_cms_get_template_part( 31 | 'templates/post-preview-section', 32 | [ 33 | 'option_val_array' => $option_val_array, 34 | ] 35 | ); 36 | 37 | headless_cms_get_template_part( 38 | 'templates/hero-section', 39 | [ 40 | 'option_val_array' => $option_val_array, 41 | ] 42 | ); 43 | 44 | headless_cms_get_template_part( 45 | 'templates/search-section', 46 | [ 47 | 'option_val_array' => $option_val_array, 48 | ] 49 | ); 50 | 51 | headless_cms_get_template_part( 52 | 'templates/featured-post-section', 53 | [ 54 | 'option_val_array' => $option_val_array, 55 | ] 56 | ); 57 | 58 | headless_cms_get_template_part( 59 | 'templates/latest-posts-section', 60 | [ 61 | 'option_val_array' => $option_val_array, 62 | ] 63 | ); 64 | 65 | headless_cms_get_template_part( 66 | 'templates/comments-section', 67 | [ 68 | 'option_val_array' => $option_val_array, 69 | ] 70 | ); 71 | 72 | ?> 73 | 74 | 75 |
76 |
77 |
78 | --------------------------------------------------------------------------------