├── favicon.png ├── screenshot.png ├── footer.php ├── header.php ├── .gitignore ├── .prettierrc ├── blocks ├── credit │ ├── block.css │ └── block.php └── README.md ├── style.css ├── index.php ├── functions ├── theme-config.php ├── deprecated.php ├── svg.php ├── gutenberg-functions.php ├── plugin-manifest.php ├── developer-role.php ├── widgets.php ├── proxy.php ├── class-cookiemanager.php ├── class-cookiemanagersettings.php ├── wp-functions.php └── acf-functions.php ├── functions.php ├── phpcs.xml.dist ├── css ├── admin.css └── login.css ├── acf ├── developer-settings.json ├── image-meta.json ├── proxy-settings.json └── site-options.json ├── images └── logo-funkhaus.svg ├── js └── admin.js ├── README.md └── LICENSE /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkhaus/fuxt-backend/HEAD/favicon.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/funkhaus/fuxt-backend/HEAD/screenshot.png -------------------------------------------------------------------------------- /footer.php: -------------------------------------------------------------------------------- 1 | 10 | Block. Be sure to select "Show in GraphQL". You don't need to se the GraphQL types manually. The default works. 9 | 1. Verify that new component is available in the editor (note you may need to clear site cache) 10 | 1. Create backend PHP template and styles 11 | 1. in /blocks/ dir add new dir for block (eg "/block/slideshow") 12 | 1. add template to `/block/slideshow/block.php` 13 | 1. add styles to `/block/slideshow/block.css` 14 | 1. Import Component to the WpContent.vue file for lazy loading 15 | 1. Build frontend Vue template in `/components/wp-block` 16 | -------------------------------------------------------------------------------- /functions/theme-config.php: -------------------------------------------------------------------------------- 1 | 17 |
Enter the primary front end URL.
" 14 | ) 15 | }, 16 | showAttachmentIds: function() { 17 | // Show the attachment IDs on hover of attachment grid blocks 18 | jQuery(document).on( 19 | 'mouseenter', 20 | '.media-modal .attachment, .media-frame .attachment', 21 | function() { 22 | var id = jQuery(this).data('id') 23 | if (id) { 24 | jQuery(this).attr('title', 'Attachment ID: ' + id) 25 | } 26 | } 27 | ) 28 | }, 29 | shiftClickNestedPages: function() { 30 | // Enable shift-clicking on NestedPages admin lists 31 | var lastChecked = null 32 | jQuery('#wpbody-content').on( 33 | 'click', 34 | ".nestedpages .np-bulk-checkbox input[type='checkbox']", 35 | function(e) { 36 | // Abort if first click 37 | if (!lastChecked) { 38 | lastChecked = this 39 | return 40 | } 41 | 42 | // Handle shift clicking and auto selecting all following checkboxes 43 | var $chkboxes = jQuery( 44 | ".nestedpages .np-bulk-checkbox input[type='checkbox']" 45 | ) 46 | if (e.shiftKey) { 47 | var start = $chkboxes.index(this) 48 | var end = $chkboxes.index(lastChecked) 49 | $chkboxes 50 | .slice(Math.min(start, end), Math.max(start, end) + 1) 51 | .prop('checked', lastChecked.checked) 52 | } 53 | 54 | lastChecked = this; 55 | } 56 | ) 57 | }, 58 | removeUnusedBlocks: function() { 59 | // This functions removes some blocks from the Gutenberg editor. 60 | // SEE: https://wordpress.stackexchange.com/questions/379612/how-to-remove-the-core-embed-blocks-in-wordpress-5-6 61 | 62 | if (typeof wp.domReady == "undefined") { 63 | return; 64 | } 65 | 66 | wp.domReady(function() { 67 | let allowedEmbedBlocks = ['vimeo', 'youtube'] 68 | if (fuxtAdmin.isGutenbergActive()) 69 | wp.blocks 70 | .getBlockVariations('core/embed') 71 | .forEach(function(blockVariation) { 72 | if ( 73 | -1 === 74 | allowedEmbedBlocks.indexOf(blockVariation.name) 75 | ) { 76 | wp.blocks.unregisterBlockVariation( 77 | 'core/embed', 78 | blockVariation.name 79 | ) 80 | } 81 | }) 82 | }) 83 | }, 84 | isGutenbergActive: function() { 85 | return typeof wp !== 'undefined' && typeof wp.blocks !== 'undefined'; 86 | } 87 | } 88 | jQuery(document).ready(function() { 89 | fuxtAdmin.showAttachmentIds() 90 | fuxtAdmin.shiftClickNestedPages() 91 | fuxtAdmin.removeUnusedBlocks() 92 | }) 93 | jQuery(window).load(function() { 94 | if (jQuery('body').hasClass('options-general-php')) { 95 | fuxtAdmin.enabledHomeUrlEdit() 96 | } 97 | }) 98 | -------------------------------------------------------------------------------- /acf/image-meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "key": "group_5cccac8d93cd4", 3 | "title": "Image Meta", 4 | "fields": [ 5 | { 6 | "key": "field_5cccac9d781a1", 7 | "label": "Video URL", 8 | "name": "video_url", 9 | "type": "url", 10 | "instructions": "", 11 | "required": 0, 12 | "conditional_logic": 0, 13 | "wrapper": { 14 | "width": "", 15 | "class": "", 16 | "id": "" 17 | }, 18 | "show_in_graphql": 1, 19 | "default_value": "", 20 | "placeholder": "" 21 | }, 22 | { 23 | "key": "field_5cccacb8781a2", 24 | "label": "Primary Color", 25 | "name": "primary_color", 26 | "type": "color_picker", 27 | "instructions": "", 28 | "required": 0, 29 | "conditional_logic": 0, 30 | "wrapper": { 31 | "width": "", 32 | "class": "", 33 | "id": "" 34 | }, 35 | "show_in_graphql": 1, 36 | "default_value": "" 37 | }, 38 | { 39 | "key": "field_5d8eabc56cc17", 40 | "label": "Focal Point X", 41 | "name": "focal_point_x", 42 | "type": "number", 43 | "instructions": "Horizontal coordinate", 44 | "required": 0, 45 | "conditional_logic": 0, 46 | "wrapper": { 47 | "width": "", 48 | "class": "focal-point", 49 | "id": "focal-point-x" 50 | }, 51 | "show_in_graphql": 1, 52 | "default_value": "", 53 | "placeholder": "", 54 | "prepend": "%", 55 | "append": "", 56 | "min": 0, 57 | "max": 100, 58 | "step": "" 59 | }, 60 | { 61 | "key": "field_5d8eac216cc19", 62 | "label": "Focal Point Y", 63 | "name": "focal_point_y", 64 | "type": "number", 65 | "instructions": "Vertical coordinate", 66 | "required": 0, 67 | "conditional_logic": 0, 68 | "wrapper": { 69 | "width": "", 70 | "class": "focal-point", 71 | "id": "focal-point-y" 72 | }, 73 | "show_in_graphql": 1, 74 | "default_value": "", 75 | "placeholder": "", 76 | "prepend": "%", 77 | "append": "", 78 | "min": 0, 79 | "max": 100, 80 | "step": "" 81 | }, 82 | { 83 | "key": "field_609539db9248d", 84 | "label": "Blurhash", 85 | "name": "blurhash", 86 | "type": "text", 87 | "instructions": "", 88 | "required": 0, 89 | "conditional_logic": 0, 90 | "wrapper": { 91 | "width": "", 92 | "class": "", 93 | "id": "" 94 | }, 95 | "show_in_graphql": 1, 96 | "default_value": "", 97 | "placeholder": "", 98 | "prepend": "", 99 | "append": "", 100 | "maxlength": "" 101 | } 102 | ], 103 | "location": [ 104 | [ 105 | { 106 | "param": "attachment", 107 | "operator": "==", 108 | "value": "image" 109 | } 110 | ] 111 | ], 112 | "menu_order": 0, 113 | "position": "normal", 114 | "style": "default", 115 | "label_placement": "top", 116 | "instruction_placement": "label", 117 | "hide_on_screen": "", 118 | "active": true, 119 | "description": "", 120 | "show_in_graphql": 1, 121 | "graphql_field_name": "imageMeta" 122 | } 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fuxt-backend 2 | 3 | A low-code/no-code theme that turns WordPress into a true GraphQL powered Headless CMS. Optimized for the [fuxt frontend boilerplate](https://github.com/funkhaus/fuxt). 4 | 5 | Built by [Funkhaus](http://funkhaus.us/). 6 | 7 | ## Install 8 | 9 | 1. Where are you hosting WordPress? We recommend [Flywheel](https://share.getf.ly/n02x5z). 10 | 1. Install theme 11 | 1. Install required plugins (as prompted) 12 | 1. If you want to use ACF, there are some default fields we recommend, you can find the file to import from the theme directory `/acf/` or [here](https://github.com/funkhaus/fuxt-backend/tree/master/acf). 13 | 1. Re-save Permalinks 14 | 1. If using ACF, it is strongly recommended to import the ACF fields in the `.json` file from `/acf/` directory. 15 | 16 | ## Setting up WordPress Coding Standards in VS Code 17 | 18 | 1. Install [PHP CodeSniffer (PHPCS)](https://github.com/squizlabs/PHP_CodeSniffer#installation) 19 | ``` 20 | composer global require "squizlabs/php_codesniffer=*" 21 | ``` 22 | PHPCS is a development tool that detects violations of coding standard and automatically corrects them. Do note that PHP 5.4 or greater is required. 23 | 2. Download [WPCS](https://github.com/WordPress/WordPress-Coding-Standards) 24 | ``` 25 | git clone -b master https://github.com/WordPress/WordPress-Coding-Standards.git wpcs 26 | ``` 27 | `cd` to your desired directory and run the above command in your terminal. This will download WPCS into a folder called `wpcs`. 28 | 3. Add WPCS to PHPCS 29 | ``` 30 | /path/to/composer/vendor/bin/phpcs --config-set installed_paths /path/to/WPCS, /path/to/another-standards 31 | ``` 32 | Now that we have WPCS and PHPCS in our system, we can run the above command and the latter know where our coding standard is located. 33 | 34 | If you are getting the `command not found: phpcs message`, ensure that your path to PHPCS is correct. Since we installed it globally, the path should be something like `/users/your_user_name/.composer/vendor/bin/phpcs` 35 | 36 | Also, do note that the `installed_paths` command overwrites any previously set installed_paths. If you have existing coding standards, please remember to include their paths together with whatever coding standards you are adding separated by a comma. 37 | 4. Check to ensure WPCS is added. 38 | ``` 39 | /path/to/composer/vendor/bin/phpcs -i 40 | ``` 41 | If WPCS is added correctly, we should see the following output: 42 | ``` 43 | The installed coding standards are PEAR, Zend, PSR2, MySource, Squiz, PSR1, PSR12, WordPress, WordPress-Extra, WordPress-Docs and WordPress-Core 44 | ``` 45 | 5. Configureing VSCode 46 | 1. Install **phpcs** and **phpcbf** extensions in VSCode. 47 | The **phpcs** extension enables linting for all PHP files in our editor while **phpcbf** will try to beautify and fix our code according to the chosen coding standard. 48 | 2. Configure **settings.json** 49 | Once those two are installed, open up the editor settings under **Code > Preferences > Settings**. Toggle to the JSON view and add the following values: 50 | Save the settings.json and restart VSCode. 51 | ``` 52 | "phpcs.enable": true, 53 | "phpcs.executablePath": "/path/to/composer/vendor/bin/phpcs", 54 | "phpcs.standard": "WordPress" 55 | "phpcbf.enable": true, 56 | "phpcbf.documentFormattingProvider": true, 57 | "phpcbf.onsave": true, 58 | "phpcbf.executablePath": "/path/to/composer/vendor/bin/phpcbf", 59 | "phpcbf.standard": "WordPress", 60 | ``` 61 | 6. Test! 62 | If everything is configured correctly, VSCode should start linting your PHP. Since we configured `phpcbf.onsave: true`, VSCode should auto fix some issues when we save the file. 63 | Or we can format the file manually by `right click > Format Document`. 64 | 7. Additional resources for VSCode + PHPCS + WPCS configuration 65 | https://www.edmundcwm.com/setting-up-wordpress-coding-standards-in-vs-code/ 66 | https://github.com/tommcfarlin/phpcs-wpcs-vscode 67 | 68 | 69 | ## More 70 | 71 | Please see the documentation for [Fuxt](https://github.com/funkhaus/fuxt) to better understand what this theme can do! 72 | 73 | ## TODO 74 | 75 | - Document the plugins that get auto-installed 76 | -------------------------------------------------------------------------------- /acf/proxy-settings.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "group_6466cc3ca925a", 4 | "title": "Proxy Settings", 5 | "fields": [ 6 | { 7 | "key": "field_6466cc3df4887", 8 | "label": "Providers", 9 | "name": "providers", 10 | "aria-label": "", 11 | "type": "repeater", 12 | "instructions": "Enter details for the services you wish to proxy.", 13 | "required": 0, 14 | "conditional_logic": 0, 15 | "wrapper": { 16 | "width": "", 17 | "class": "", 18 | "id": "" 19 | }, 20 | "show_in_graphql": 1, 21 | "layout": "row", 22 | "pagination": 0, 23 | "min": 0, 24 | "max": 0, 25 | "collapsed": "", 26 | "button_label": "Add Provider", 27 | "rows_per_page": 20, 28 | "sub_fields": [ 29 | { 30 | "key": "field_6466cc64f4888", 31 | "label": "Name", 32 | "name": "name", 33 | "aria-label": "", 34 | "type": "text", 35 | "instructions": "The name of the provider. Used in frontend code for reference.", 36 | "required": 0, 37 | "conditional_logic": 0, 38 | "wrapper": { 39 | "width": "", 40 | "class": "", 41 | "id": "" 42 | }, 43 | "show_in_graphql": 1, 44 | "default_value": "", 45 | "maxlength": "", 46 | "placeholder": "", 47 | "prepend": "", 48 | "append": "", 49 | "parent_repeater": "field_6466cc3df4887" 50 | }, 51 | { 52 | "key": "field_6466cc6af4889", 53 | "label": "Base URL", 54 | "name": "base_url", 55 | "aria-label": "", 56 | "type": "url", 57 | "instructions": "The base URL of the API you are trying to proxy. Tip: leave off the trailing slash.", 58 | "required": 0, 59 | "conditional_logic": 0, 60 | "wrapper": { 61 | "width": "", 62 | "class": "", 63 | "id": "" 64 | }, 65 | "show_in_graphql": 1, 66 | "default_value": "", 67 | "placeholder": "", 68 | "parent_repeater": "field_6466cc3df4887" 69 | }, 70 | { 71 | "key": "field_6466cc74f488a", 72 | "label": "Authorization header", 73 | "name": "authorization_header", 74 | "aria-label": "", 75 | "type": "text", 76 | "instructions": "The HTTP Authorization header to add to the request. Often this will be of the format `Bearer xxxxxxxxx`", 77 | "required": 0, 78 | "conditional_logic": 0, 79 | "wrapper": { 80 | "width": "", 81 | "class": "", 82 | "id": "" 83 | }, 84 | "show_in_graphql": 1, 85 | "default_value": "", 86 | "maxlength": "", 87 | "placeholder": "", 88 | "prepend": "", 89 | "append": "", 90 | "parent_repeater": "field_6466cc3df4887" 91 | } 92 | ] 93 | }, 94 | { 95 | "key": "field_6466ccb3f488b", 96 | "label": "Domain restricted", 97 | "name": "domain_restricted", 98 | "aria-label": "", 99 | "type": "true_false", 100 | "instructions": "When true, only frontend requests from the Site and WordPress URLs will be allowed. When false, requests from all domains will be allowed. This is a security issue, so only have this false during when testing from localhost.", 101 | "required": 0, 102 | "conditional_logic": 0, 103 | "wrapper": { 104 | "width": "", 105 | "class": "", 106 | "id": "" 107 | }, 108 | "show_in_graphql": 1, 109 | "message": "Restrict access to the Site Address and WordPress Address", 110 | "default_value": 1, 111 | "ui_on_text": "", 112 | "ui_off_text": "", 113 | "ui": 1 114 | } 115 | ], 116 | "location": [ 117 | [ 118 | { 119 | "param": "options_page", 120 | "operator": "==", 121 | "value": "proxy-settings" 122 | } 123 | ] 124 | ], 125 | "menu_order": 0, 126 | "position": "normal", 127 | "style": "default", 128 | "label_placement": "top", 129 | "instruction_placement": "label", 130 | "hide_on_screen": "", 131 | "active": true, 132 | "description": "", 133 | "show_in_rest": 0, 134 | "show_in_graphql": 0, 135 | "graphql_field_name": "proxySettings", 136 | "map_graphql_types_from_location_rules": 0, 137 | "graphql_types": "" 138 | } 139 | ] -------------------------------------------------------------------------------- /acf/site-options.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "key": "group_5e4da96a17fe9", 4 | "title": "Site Options", 5 | "fields": [ 6 | { 7 | "key": "field_5e4dbc3d5b0a8", 8 | "label": "Google Analytics", 9 | "name": "google_analytics", 10 | "type": "repeater", 11 | "instructions": "Enter Google Analytics tracking codes. Uses the gtag.js tracking method.", 12 | "required": 0, 13 | "conditional_logic": 0, 14 | "wrapper": { 15 | "width": "", 16 | "class": "", 17 | "id": "" 18 | }, 19 | "show_in_graphql": 1, 20 | "collapsed": "", 21 | "min": 0, 22 | "max": 0, 23 | "layout": "table", 24 | "button_label": "Add Tracking Code", 25 | "sub_fields": [ 26 | { 27 | "key": "field_5e4dbc465b0a9", 28 | "label": "Code", 29 | "name": "code", 30 | "type": "text", 31 | "instructions": "", 32 | "required": 0, 33 | "conditional_logic": 0, 34 | "wrapper": { 35 | "width": "", 36 | "class": "", 37 | "id": "" 38 | }, 39 | "show_in_graphql": 1, 40 | "default_value": "", 41 | "placeholder": "UA-12345678-1", 42 | "prepend": "", 43 | "append": "", 44 | "maxlength": "" 45 | } 46 | ] 47 | }, 48 | { 49 | "key": "field_5e4da982da90c", 50 | "label": "Social media", 51 | "name": "social_media", 52 | "type": "repeater", 53 | "instructions": "", 54 | "required": 0, 55 | "conditional_logic": 0, 56 | "wrapper": { 57 | "width": "", 58 | "class": "", 59 | "id": "" 60 | }, 61 | "show_in_graphql": 1, 62 | "collapsed": "", 63 | "min": 0, 64 | "max": 0, 65 | "layout": "table", 66 | "button_label": "Add Social Media", 67 | "sub_fields": [ 68 | { 69 | "key": "field_5e4daaed308b6", 70 | "label": "Platform", 71 | "name": "platform", 72 | "type": "text", 73 | "instructions": "", 74 | "required": 0, 75 | "conditional_logic": 0, 76 | "wrapper": { 77 | "width": "", 78 | "class": "", 79 | "id": "" 80 | }, 81 | "show_in_graphql": 1, 82 | "default_value": "", 83 | "placeholder": "Instagram", 84 | "prepend": "", 85 | "append": "", 86 | "maxlength": "" 87 | }, 88 | { 89 | "key": "field_5e4dc3ebb6ba8", 90 | "label": "Url", 91 | "name": "url", 92 | "type": "url", 93 | "instructions": "", 94 | "required": 0, 95 | "conditional_logic": 0, 96 | "wrapper": { 97 | "width": "", 98 | "class": "", 99 | "id": "" 100 | }, 101 | "show_in_graphql": 1, 102 | "default_value": "", 103 | "placeholder": "https://www.instagram.com/funkhaus/" 104 | } 105 | ] 106 | }, 107 | { 108 | "key": "field_622ab9158ac0d", 109 | "label": "Social shared image", 110 | "name": "social_shared_image", 111 | "type": "image", 112 | "instructions": "This image is used when people share the site on social media, unless a more relevant image is found. The ideal size for this image is 1200px wide by 630px high.", 113 | "required": 0, 114 | "conditional_logic": 0, 115 | "wrapper": { 116 | "width": "", 117 | "class": "", 118 | "id": "" 119 | }, 120 | "show_in_graphql": 1, 121 | "return_format": "id", 122 | "preview_size": "social-preview", 123 | "library": "all", 124 | "min_width": "", 125 | "min_height": "", 126 | "min_size": "", 127 | "max_width": "", 128 | "max_height": "", 129 | "max_size": "", 130 | "mime_types": "" 131 | } 132 | ], 133 | "location": [ 134 | [ 135 | { 136 | "param": "options_page", 137 | "operator": "==", 138 | "value": "site-options" 139 | } 140 | ] 141 | ], 142 | "menu_order": 0, 143 | "position": "normal", 144 | "style": "default", 145 | "label_placement": "top", 146 | "instruction_placement": "label", 147 | "hide_on_screen": "", 148 | "active": true, 149 | "description": "", 150 | "show_in_rest": 0, 151 | "show_in_graphql": 1, 152 | "graphql_field_name": "siteOptionsMeta", 153 | "map_graphql_types_from_location_rules": 0, 154 | "graphql_types": "" 155 | } 156 | ] 157 | -------------------------------------------------------------------------------- /functions/proxy.php: -------------------------------------------------------------------------------- 1 | "POST, GET, DELETE, PATCH, PUT", 17 | "callback" => "fuxt_proxy_do_request", 18 | "permission_callback" => "fuxt_proxy_check_permission", 19 | ], 20 | ]); 21 | 22 | // Change headers to allow the custom headers we need 23 | add_filter( 24 | "rest_pre_serve_request", 25 | "fuxt_proxy_add_custom_cors_headers", 26 | 20, 27 | 4 28 | ); 29 | } 30 | add_action("rest_api_init", "fuxt_proxy_init"); 31 | 32 | /* 33 | * Check's that the request matches allowed Origin and Provider name is in ACF repeater field 34 | * Returns true || WP_Error 35 | */ 36 | function fuxt_proxy_check_permission($request) 37 | { 38 | // Security is be on, so check that Origin is on whitelist 39 | $domain_restricted = get_field("domain_restricted", "option"); 40 | if ( 41 | $domain_restricted && 42 | !fuxt_proxy_request_from_allowed_origin($request) 43 | ) { 44 | return new WP_Error( 45 | "permission", 46 | "Your origin is not allowed to make this Proxy request" 47 | ); 48 | } 49 | 50 | // Passed Origin checks, now check requested asked for an approved Provider 51 | $proxy_name = $request->get_header("Fuxt-Proxy-Name"); 52 | $provider = fuxt_proxy_get_provider("name", $proxy_name); 53 | 54 | // Allow if Provider is found 55 | if ($proxy_name && $provider) { 56 | return true; 57 | } 58 | 59 | return new WP_Error("permission", "Your requested Proxy Name is invalid"); 60 | } 61 | 62 | /* 63 | * Forwards request to allowed API, adds in Bearer token from ACF Proxy Settings field group 64 | * Returns WP_REST_Response || WP_Error 65 | */ 66 | function fuxt_proxy_do_request($request) 67 | { 68 | // Get bearer token from ACF field, include in request below 69 | $proxy_name = $request->get_header("Fuxt-Proxy-Name"); 70 | $proxy_endpoint = $request->get_header("Fuxt-Proxy-Endpoint"); 71 | $provider = fuxt_proxy_get_provider("name", $proxy_name); 72 | 73 | // Get any found tokens from ACF, build out full request URL 74 | $auth_header = $provider["authorization_header"] ?? ""; 75 | $url = $provider["base_url"] . $proxy_endpoint; 76 | 77 | // Encode the body to JSON if supplied 78 | $body = $request->get_json_params(); 79 | if ($body && is_array($body)) { 80 | $body = json_encode($body); 81 | } 82 | 83 | // Setup HTTP Request, pass through as much settings as we can 84 | $args = [ 85 | "headers" => [ 86 | "Authorization" => $auth_header, 87 | "Content-Type" => $request->get_header("Content-Type"), 88 | ], 89 | "method" => $request->get_method(), 90 | "body" => $body ?? "", 91 | ]; 92 | 93 | // Send remote request! Go Proxy Go! 94 | $response = wp_remote_request($url, $args); 95 | 96 | // Retrieve information from the $response 97 | $response_code = wp_remote_retrieve_response_code($response); 98 | $response_message = wp_remote_retrieve_response_message($response); 99 | $response_headers = wp_remote_retrieve_headers($response)->getAll(); 100 | $response_body = wp_remote_retrieve_body($response); 101 | 102 | // Check if response is JSON, if so then decode it so that when we send it later it's not double encoded 103 | if( fuxt_proxy_is_json($response_body) ) { 104 | $response_body = json_decode($response_body); 105 | } 106 | 107 | // Return data or error back to frontend 108 | if (!is_wp_error($response)) { 109 | $response = new WP_REST_Response($response_body, $response_code); 110 | 111 | // Add orginal response's headers 112 | foreach($response_headers as $key => $val) { 113 | $response->set_headers( array($key => $val) ); 114 | } 115 | 116 | // Make sure Flywheel doesn't cache this 117 | $response->set_headers( array("Cache-Control" => "no-cache, no-store, must-revalidate, max-age=0") ); 118 | return $response; 119 | } 120 | 121 | return new WP_Error($response_code, $response_message, $response_body); 122 | } 123 | 124 | /* 125 | * This customizes the CORS headers the server will accept, allowing use of our Proxy custom headers 126 | * Returns true || false 127 | */ 128 | function fuxt_proxy_add_custom_cors_headers($served, $result, $request, $server) 129 | { 130 | // Abort if not a request to the Proxy endpoint 131 | if ($request->get_route() !== "/fuxt/v1/proxy") { 132 | return $served; 133 | } 134 | 135 | // Now add our headers 136 | header( 137 | "Access-Control-Allow-Headers: Fuxt-Proxy-Name, Fuxt-Proxy-Endpoint, Content-Type, Authorization" 138 | ); 139 | 140 | return $served; 141 | } 142 | 143 | /* 144 | * Check that the request is from an allowed Origin. 145 | * Returns true || false 146 | */ 147 | function fuxt_proxy_request_from_allowed_origin($request) 148 | { 149 | $origin = get_http_origin(); 150 | 151 | // Start with site url as allowed origin. 152 | $allowed_origins = [site_url(), home_url()]; 153 | 154 | // Add fuxt home url to allowed origin. 155 | $fuxt_home_url = get_option("fuxt_home_url"); 156 | if ($fuxt_home_url) { 157 | $allowed_origins[] = $fuxt_home_url; 158 | } 159 | 160 | $allowed_origins = apply_filters("fuxt_allowed_origins", $allowed_origins); 161 | 162 | // Current request comes from an Origin that is allowed 163 | if (in_array($origin, $allowed_origins, true)) { 164 | return true; 165 | } 166 | 167 | return false; 168 | } 169 | 170 | /* 171 | * Return the Provider if found, or false. 172 | * Returns Array || false 173 | */ 174 | function fuxt_proxy_get_provider($search_key, $search_value) 175 | { 176 | $proxy_providers = get_field("providers", "option"); 177 | 178 | $columns = array_column($proxy_providers, $search_key); 179 | $found = array_search($search_value, $columns); 180 | 181 | // Return found Provider 182 | if (is_int($found)) { 183 | return $proxy_providers[$found]; 184 | } 185 | 186 | return false; 187 | } 188 | 189 | /* 190 | * Checks if a string is JSON 191 | * Returns Array || false 192 | */ 193 | function fuxt_proxy_is_json($str) { 194 | $json = json_decode($str); 195 | return $json && $str != $json; 196 | } -------------------------------------------------------------------------------- /functions/class-cookiemanager.php: -------------------------------------------------------------------------------- 1 | secure = is_ssl(); 40 | $this->secure_logged_in_cookie = $this->secure && 'https' === wp_parse_url( get_option( 'home' ), PHP_URL_SCHEME ); 41 | $this->send_auth_cookies = true; 42 | 43 | add_filter( 'secure_auth_cookie', array( $this, 'secure_auth_cookie' ), PHP_INT_MAX, 1 ); 44 | add_filter( 'secure_logged_in_cookie', array( $this, 'secure_logged_in_cookie' ), PHP_INT_MAX, 1 ); 45 | add_action( 'set_auth_cookie', array( $this, 'set_auth_cookie' ), 10, 6 ); 46 | add_action( 'set_logged_in_cookie', array( $this, 'set_logged_in_cookie' ), 10, 6 ); 47 | add_filter( 'send_auth_cookies', array( $this, 'send_auth_cookies' ) ); 48 | 49 | add_action( 'clear_auth_cookie', array( $this, 'clear_auth_cookie' ) ); 50 | } 51 | 52 | /** 53 | * Init secure with site secure value. 54 | * 55 | * @param bool $secure Whether the cookie should only be sent over HTTPS. 56 | * 57 | * @return bool 58 | */ 59 | public function secure_auth_cookie( $secure ) { 60 | $this->secure = $secure; 61 | 62 | return $secure; 63 | } 64 | 65 | /** 66 | * Init $secure_logged_in_cookie with site secure value. 67 | * 68 | * @param bool $secure_logged_in_cookie Whether the logged in cookie should only be sent over HTTPS. 69 | * 70 | * @return bool 71 | */ 72 | public function secure_logged_in_cookie( $secure_logged_in_cookie ) { 73 | $this->secure_logged_in_cookie = $secure_logged_in_cookie; 74 | 75 | return $secure_logged_in_cookie; 76 | } 77 | 78 | /** 79 | * Set auth cookie. 80 | * Fires immediately before the authentication cookie is set. 81 | * 82 | * @param string $auth_cookie Authentication cookie value. 83 | * @param int $expire The time the login grace period expires as a UNIX timestamp. 84 | * Default is 12 hours past the cookie's expiration time. 85 | * @param int $expiration The time when the authentication cookie expires as a UNIX timestamp. 86 | * Default is 14 days from now. 87 | * @param int $user_id User ID. 88 | * @param string $scheme Authentication scheme. Values include 'auth' or 'secure_auth'. 89 | * @param string $token User's session token to use for this cookie. 90 | * 91 | * @return void 92 | */ 93 | public function set_auth_cookie( $auth_cookie, $expire, $expiration, $user_id, $scheme, $token ) { 94 | 95 | $same_site = CookieManagerSettings::get_samesite(); 96 | $cookie_domain = CookieManagerSettings::get_domain(); 97 | 98 | $auth_cookie_name = $this->secure ? SECURE_AUTH_COOKIE : AUTH_COOKIE; 99 | 100 | if ( version_compare( PHP_VERSION, '7.3.0' ) >= 0 ) { 101 | setcookie( 102 | $auth_cookie_name, 103 | $auth_cookie, 104 | array( 105 | 'expires' => $expire, 106 | 'path' => PLUGINS_COOKIE_PATH, 107 | 'domain' => $cookie_domain, 108 | 'secure' => $this->secure, 109 | 'httponly' => true, 110 | 'samesite' => $same_site, 111 | ) 112 | ); 113 | 114 | setcookie( 115 | $auth_cookie_name, 116 | $auth_cookie, 117 | array( 118 | 'expires' => $expire, 119 | 'path' => ADMIN_COOKIE_PATH, 120 | 'domain' => $cookie_domain, 121 | 'secure' => $this->secure, 122 | 'httponly' => true, 123 | 'samesite' => $same_site, 124 | ) 125 | ); 126 | } else { 127 | setcookie( $auth_cookie_name, $auth_cookie, $expire, PLUGINS_COOKIE_PATH, $cookie_domain, $this->secure, true ); 128 | setcookie( $auth_cookie_name, $auth_cookie, $expire, ADMIN_COOKIE_PATH, $cookie_domain, $this->secure, true ); 129 | } 130 | } 131 | 132 | /** 133 | * Set logged in cookie. 134 | * Fires immediately before the logged-in authentication cookie is set. 135 | * 136 | * @param string $logged_in_cookie The logged-in cookie value. 137 | * @param int $expire The time the login grace period expires as a UNIX timestamp. 138 | * Default is 12 hours past the cookie's expiration time. 139 | * @param int $expiration The time when the logged-in authentication cookie expires as a UNIX timestamp. 140 | * Default is 14 days from now. 141 | * @param int $user_id User ID. 142 | * @param string $scheme Authentication scheme. Default 'logged_in'. 143 | * @param string $token User's session token to use for this cookie. 144 | */ 145 | public function set_logged_in_cookie( $logged_in_cookie, $expire, $expiration, $user_id, $scheme, $token ) { 146 | 147 | $same_site = CookieManagerSettings::get_samesite(); 148 | $cookie_domain = CookieManagerSettings::get_domain(); 149 | 150 | if ( version_compare( PHP_VERSION, '7.3.0' ) >= 0 ) { 151 | setcookie( 152 | LOGGED_IN_COOKIE, 153 | $logged_in_cookie, 154 | array( 155 | 'expires' => $expire, 156 | 'path' => COOKIEPATH, 157 | 'domain' => $cookie_domain, 158 | 'secure' => $this->secure_logged_in_cookie, 159 | 'httponly' => true, 160 | 'samesite' => $same_site, 161 | ) 162 | ); 163 | 164 | if ( COOKIEPATH != SITECOOKIEPATH ) { 165 | setcookie( 166 | LOGGED_IN_COOKIE, 167 | $logged_in_cookie, 168 | array( 169 | 'expires' => $expire, 170 | 'path' => SITECOOKIEPATH, 171 | 'domain' => $cookie_domain, 172 | 'secure' => $this->secure_logged_in_cookie, 173 | 'httponly' => true, 174 | 'samesite' => $same_site, 175 | ) 176 | ); 177 | } 178 | } else { 179 | setcookie( LOGGED_IN_COOKIE, $logged_in_cookie, $expire, COOKIEPATH, $cookie_domain, $this->secure_logged_in_cookie, true ); 180 | if ( COOKIEPATH != SITECOOKIEPATH ) { 181 | setcookie( LOGGED_IN_COOKIE, $logged_in_cookie, $expire, SITECOOKIEPATH, $cookie_domain, $this->secure_logged_in_cookie, true ); 182 | } 183 | } 184 | 185 | // Set to don't send auth cookie again. 186 | $this->send_auth_cookies = false; 187 | } 188 | 189 | /** 190 | * Disable default auth cookie function on login. 191 | * 192 | * @param bool $send True. 193 | */ 194 | public function send_auth_cookies( $send ) { 195 | return $this->send_auth_cookies; 196 | } 197 | 198 | /** 199 | * Clear auth cookie. 200 | * 201 | * @param string $cookie_domain Cookie domain. 202 | */ 203 | public function clear_auth_cookie( $cookie_domain = '' ) { 204 | $this->send_auth_cookies = false; 205 | 206 | if ( $cookie_domain === '' ) { 207 | $cookie_domain = CookieManagerSettings::get_domain(); 208 | } 209 | 210 | $time = time() - YEAR_IN_SECONDS; 211 | 212 | setcookie( AUTH_COOKIE, ' ', $time, ADMIN_COOKIE_PATH, $cookie_domain ); 213 | setcookie( SECURE_AUTH_COOKIE, ' ', $time, ADMIN_COOKIE_PATH, $cookie_domain ); 214 | setcookie( AUTH_COOKIE, ' ', $time, PLUGINS_COOKIE_PATH, $cookie_domain ); 215 | setcookie( SECURE_AUTH_COOKIE, ' ', $time, PLUGINS_COOKIE_PATH, $cookie_domain ); 216 | setcookie( LOGGED_IN_COOKIE, ' ', $time, COOKIEPATH, $cookie_domain ); 217 | setcookie( LOGGED_IN_COOKIE, ' ', $time, SITECOOKIEPATH, $cookie_domain ); 218 | } 219 | 220 | } 221 | 222 | ( new CookieManager() )->init(); 223 | -------------------------------------------------------------------------------- /functions/class-cookiemanagersettings.php: -------------------------------------------------------------------------------- 1 | 'string', 43 | 'group' => 'general', 44 | 'description' => 'Authentication Cookie SameSite parameter', 45 | 'sanitize_callback' => array( $this, 'sanitize_value' ), 46 | 'show_in_rest' => false, 47 | ) 48 | ); 49 | 50 | // add Field. 51 | add_settings_field( 52 | self::SETTING_FIELD_ID, 53 | 'Authentication Cookie SameSite parameter', 54 | array( $this, 'setting_samesite_callback_function' ), 55 | 'general', 56 | 'default', 57 | array( 58 | 'id' => self::SETTING_FIELD_ID, 59 | 'option_name' => self::SETTING_SAMESITE, 60 | ) 61 | ); 62 | 63 | register_setting( 64 | 'general', 65 | self::SETTING_DOMAIN, 66 | array( 67 | 'type' => 'boolean', 68 | 'group' => 'general', 69 | 'description' => 'Authentication Cookie Domain', 70 | 'show_in_rest' => false, 71 | ) 72 | ); 73 | 74 | // add Field. 75 | add_settings_field( 76 | 'fuxt_cookie_domain-id', 77 | 'Authentication Cookie Domain', 78 | array( $this, 'setting_domain_callback_function' ), 79 | 'general', 80 | 'default', 81 | array( 82 | 'id' => 'fuxt_cookie_domain-id', 83 | 'option_name' => self::SETTING_DOMAIN, 84 | ) 85 | ); 86 | 87 | } 88 | 89 | /** 90 | * Clear old domain cookie and set new domain cookie on cookie setting change. 91 | * 92 | * @param string $old_value Old value. 93 | */ 94 | public function remove_old_domain_cookies( $old_value ) { 95 | $user_id = get_current_user_id(); 96 | $secure = is_ssl(); 97 | 98 | // Clear old domain cookie. 99 | if ( empty( $old_value ) ) { 100 | $old_value = COOKIE_DOMAIN; 101 | } 102 | 103 | do_action( 'clear_auth_cookie', $old_value ); 104 | 105 | // Set new domain cookie. 106 | wp_set_auth_cookie( $user_id, false, $secure ); 107 | } 108 | 109 | /** 110 | * When previous samesite value is Strict, domain was overrided by COOKIE_DOMAIN. 111 | * So need to reset cookie with correct domain value. 112 | * 113 | * @param string $old_same_site Old samesite value. 114 | * @param string $new_same_site New samesite value. 115 | */ 116 | public function same_site_updated( $old_same_site, $new_same_site ) { 117 | $domain = get_option( self::SETTING_DOMAIN, COOKIE_DOMAIN ); 118 | 119 | if ( 'Strict' === $old_same_site && $domain != COOKIE_DOMAIN ) { 120 | $this->remove_old_domain_cookies( COOKIE_DOMAIN ); 121 | } 122 | 123 | if ( 'Strict' === $new_same_site ) { 124 | $this->remove_old_domain_cookies( $domain ); 125 | } 126 | } 127 | 128 | /** 129 | * Sanitizes SameSite value. 130 | * 131 | * @param string $val Value to sanitize. 132 | * 133 | * @return string 134 | */ 135 | public function sanitize_value( $val ) { 136 | 137 | $valid_values = $this->get_valid_values(); 138 | 139 | if ( in_array( $val, $valid_values, true ) ) { 140 | // Do not allow "None" for Non-SSL site. 141 | if ( ! is_ssl() && 'None' === $val ) { 142 | return 'Lax'; 143 | } 144 | 145 | return $val; 146 | } else { 147 | return 'Lax'; // default one. 148 | } 149 | } 150 | 151 | /** 152 | * Renders Selector for our SameSite option field. 153 | * 154 | * @param array $val Data to render. 155 | */ 156 | public function setting_samesite_callback_function( $val ) { 157 | $id = $val['id']; 158 | $option_name = $val['option_name']; 159 | $option_value = get_option( $option_name ); 160 | $valid_values = $this->get_valid_values(); 161 | ?> 162 | 166 | 167 |
168 | Warning: Upgrade to PHP 7.3.0 or above to be able to set SameSite Authentication Cookie,
169 | Current PHP version is:
170 | Otherwise setting will not be applied.
171 |
175 | Authentication Cookie SameSite parameter, Use: 176 |
212 | If SameSite parameter is Strict, this setting is ignored and Default Cookie Domain is used by Default. 213 |
214 |