├── .nvmrc
├── .gitattributes
├── src
├── Exceptions
│ ├── CustomFieldsException.php
│ └── MissingArgumentException.php
├── Integrations
│ ├── ItemsIntegration.php
│ ├── MenuItem.php
│ ├── User.php
│ ├── Taxonomy.php
│ ├── Comment.php
│ ├── BaseIntegration.php
│ ├── OptionsIntegration.php
│ ├── Metabox.php
│ ├── WooCommerceSettings.php
│ ├── OrderMetabox.php
│ ├── SubscriptionMetabox.php
│ ├── ProductVariationOptions.php
│ ├── WcMembershipPlanOptions.php
│ ├── CouponOptions.php
│ ├── ProductOptions.php
│ ├── GutenbergBlock.php
│ └── SiteOptions.php
├── Fields
│ └── DirectFileField.php
├── Api.php
└── Helpers.php
├── readme.txt
├── composer.json
├── custom-fields.php
├── readme.md
├── phpcs.xml
└── CLAUDE.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | v22
2 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | .githooks export-ignore
2 | .github export-ignore
3 | assets export-ignore
4 | docs export-ignore
5 | .gitignore export-ignore
6 | package.json export-ignore
7 | package-lock.json export-ignore
8 | webpack.config.js export-ignore
9 |
--------------------------------------------------------------------------------
/src/Exceptions/CustomFieldsException.php:
--------------------------------------------------------------------------------
1 | get_item_id() );
32 | } elseif ( ! empty( $this->option_name ) ) {
33 | $meta_value = $this->get_option_value( $this->option_name, array() );
34 |
35 | return $meta_value[ $name ] ?? $this->custom_fields->get_default_value( $item );
36 | } else {
37 | return $this->get_option_value( $name, $this->custom_fields->get_default_value( $item ) );
38 | }
39 | }
40 |
41 | /**
42 | * Sets the value of a specified field.
43 | *
44 | * @param string $name The name of the field to set.
45 | * @param mixed $value The value to set for the specified field.
46 | * @param array $item Optional. An array of item data which may include a 'callback_set' callable.
47 | *
48 | * @return mixed The result of setting the specified field.
49 | */
50 | public function set_field( string $name, mixed $value, array $item = array() ): mixed {
51 | if ( ! empty( $item['callback_set'] ) ) {
52 | return call_user_func( $item['callback_set'], $item, $this->get_item_id(), $value );
53 | } elseif ( ! empty( $this->option_name ) ) {
54 | $meta_value = $this->get_option_value( $this->option_name, array() );
55 | $meta_value[ $name ] = $value;
56 |
57 | return $this->set_option_value( $this->option_name, $meta_value );
58 | } else {
59 | return $this->set_option_value( $name, $value );
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/Integrations/MenuItem.php:
--------------------------------------------------------------------------------
1 | id = $args['id'] ?? 'menu_item__' . wp_generate_uuid4();
69 | $this->tabs = $args['tabs'] ?? array();
70 | $this->option_name = $args['meta_key'] ?? '';
71 | $this->items = $args['items'] ?? array();
72 |
73 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
74 | add_action( 'wp_nav_menu_item_custom_fields', array( $this, 'render' ) );
75 | add_action( 'wp_update_nav_menu_item', array( $this, 'save' ), 10, 2 );
76 | add_action( 'current_screen', array( $this, 'maybe_enqueue' ) );
77 | }
78 | }
79 |
80 | /**
81 | * Maybe enqueue scripts and styles based on the current screen.
82 | *
83 | * @param WP_Screen $current_screen The current screen object.
84 | *
85 | * @return void
86 | */
87 | public function maybe_enqueue( WP_Screen $current_screen ): void {
88 | if ( 'nav-menus' === $current_screen->id ) {
89 | $this->enqueue();
90 | }
91 | }
92 |
93 | /**
94 | * Renders the menu item with the specified configurations.
95 | *
96 | * @param string $item_id The ID of the menu item.
97 | *
98 | * @return void
99 | */
100 | public function render( string $item_id ): void {
101 | $this->item_id = $item_id;
102 | $this->enqueue();
103 | $this->print_app(
104 | 'menu-item',
105 | $this->tabs,
106 | array( 'loop' => $item_id ),
107 | );
108 |
109 | $items = $this->normalize_items( $this->items );
110 |
111 | foreach ( $items as $item ) {
112 | $this->print_field(
113 | $item,
114 | array( 'loop' => $item_id ),
115 | );
116 | }
117 | }
118 |
119 | /**
120 | * Saves the menu item with the specified configurations.
121 | *
122 | * @param int $menu_id The ID of the menu.
123 | * @param int $menu_item_db_id The ID of the menu item in the database.
124 | *
125 | * @return void
126 | */
127 | public function save( int $menu_id, int $menu_item_db_id ): void {
128 | $this->item_id = $menu_item_db_id;
129 |
130 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ), $menu_item_db_id );
131 | }
132 |
133 | /**
134 | * Retrieves the value of a specified option, or returns a default value if the option is not set.
135 | *
136 | * @param string $name The name of the option to retrieve.
137 | * @param mixed $default_value The default value to return if the option is not set.
138 | */
139 | public function get_option_value( string $name, mixed $default_value ): mixed {
140 | return get_post_meta( $this->get_item_id(), $name, true ) ?? $default_value;
141 | }
142 |
143 | /**
144 | * Sets the value of a specified option.
145 | *
146 | * @param string $name The name of the option to set.
147 | * @param mixed $value The value to set for the option.
148 | *
149 | * @return bool True on success, false on failure.
150 | */
151 | public function set_option_value( string $name, mixed $value ): bool {
152 | return update_post_meta( $this->get_item_id(), $name, $value );
153 | }
154 |
155 | /**
156 | * Retrieves the ID of the item.
157 | *
158 | * @return int The ID of the item.
159 | */
160 | public function get_item_id(): int {
161 | return $this->item_id;
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | # WPify Custom Fields
2 |
3 | WPify Custom Fields is a powerful, developer-oriented WordPress library for creating custom fields. It provides a comprehensive solution for integrating custom fields into various parts of WordPress and WooCommerce, from post metaboxes to product options, options pages, taxonomies, and much more.
4 |
5 | Built with modern React.js and PHP 8.1+, this library offers maximum flexibility for developers while maintaining a clean, intuitive interface for end-users.
6 |
7 | ## Key Features
8 |
9 | - **Extensive Integration Options**: Add custom fields to 14+ different contexts:
10 | - WordPress Core: Post Metaboxes, Taxonomies, Options Pages, Menu Items, Gutenberg Blocks, User Profiles, Comments
11 | - WooCommerce: Product Options, Product Variations, Order Metaboxes, Settings Pages, Subscriptions, Membership Plans
12 | - Multisite: Site Options, Network Options
13 |
14 | - **50+ Field Types**: Build anything from simple forms to complex interfaces:
15 | - Simple Fields: Text, Textarea, Number, Select, Toggle, Checkbox, Date/Time, Color, etc.
16 | - Relational Fields: Post, Term, Attachment, Links
17 | - Complex Fields: Group, Code Editor, WYSIWYG, Map integration
18 | - Repeater Fields: Multi versions of all field types
19 | - Static Fields: HTML, Button, Title, Hidden
20 |
21 | - **Powerful Conditional Logic**: Dynamically show/hide fields based on complex conditions:
22 | - Multiple comparison operators (equals, contains, greater than, etc.)
23 | - Complex AND/OR logic and nested condition groups
24 | - Advanced path references with dot notation for nested fields
25 |
26 | - **Organized Field Groups**: Create better user experiences:
27 | - Tabbed interface for organizing related fields
28 | - Nested tabs for complex hierarchies
29 | - Collapsible field groups
30 |
31 | - **Developer-Friendly**:
32 | - Strong typing with PHP 8.1+ features
33 | - Clean, standardized API
34 | - Extendable architecture for custom field types
35 | - Well-documented with consistent examples
36 |
37 | ## Requirements
38 |
39 | - PHP 8.1 or later
40 | - WordPress 6.2 or later
41 | - Modern browser (Chrome, Firefox, Safari, Edge)
42 |
43 | ## Installation
44 |
45 | ### Via Composer (Recommended)
46 |
47 | ```bash
48 | composer require wpify/custom-fields
49 | ```
50 |
51 | ### Manual Installation
52 |
53 | 1. Download the latest release from the [Releases page](https://github.com/wpify/custom-fields/releases)
54 | 2. Upload to your `/wp-content/plugins/` directory
55 | 3. Activate through the WordPress admin interface
56 |
57 | ## Quick Example
58 |
59 | ```php
60 | // Create a custom metabox for posts
61 | wpify_custom_fields()->create_metabox(
62 | array(
63 | 'id' => 'demo_metabox',
64 | 'title' => __( 'Demo Metabox', 'textdomain' ),
65 | 'post_type' => 'post',
66 | 'items' => array(
67 | 'text_field' => array(
68 | 'type' => 'text',
69 | 'label' => __( 'Text Field', 'textdomain' ),
70 | 'description' => __( 'This is a simple text field', 'textdomain' ),
71 | 'required' => true,
72 | ),
73 | 'select_field' => array(
74 | 'type' => 'select',
75 | 'label' => __( 'Select Field', 'textdomain' ),
76 | 'options' => array(
77 | 'option1' => __( 'Option 1', 'textdomain' ),
78 | 'option2' => __( 'Option 2', 'textdomain' ),
79 | ),
80 | 'conditions' => array(
81 | array(
82 | 'field' => 'text_field',
83 | 'condition' => '!=',
84 | 'value' => '',
85 | ),
86 | ),
87 | ),
88 | ),
89 | )
90 | );
91 | ```
92 |
93 | ## Why Choose WPify Custom Fields?
94 |
95 | - **Flexible API**: Provides a consistent API across all WordPress and WooCommerce contexts
96 | - **Modern Architecture**: Built with React and modern PHP principles
97 | - **Performance Optimized**: Loads only the resources needed for each context
98 | - **Comprehensive Solution**: No need for multiple plugins to handle different field contexts
99 | - **Future-Proof**: Regularly updated and maintained
100 | - **Extendable**: Create custom field types when needed
101 |
102 | ## Documentation
103 |
104 | For comprehensive documentation, visit:
105 |
106 | - [Main Documentation](docs/index.md)
107 | - [Field Types](docs/field-types.md)
108 | - [Integrations](docs/integrations.md)
109 | - [Conditional Logic](docs/features/conditions.md)
110 | - [Tabs System](docs/features/tabs.md)
111 | - [Extending](docs/features/extending.md)
112 | - [Migration from 3.x to 4.x](docs/migration-3-to-4.md)
113 |
114 | ## Support & Issues
115 |
116 | If you encounter any issues or have questions, please [open an issue](https://github.com/wpify/custom-fields/issues) on our GitHub repository.
117 |
118 | ## License
119 |
120 | WPify Custom Fields is released under the GPL v2 or later license.
121 |
--------------------------------------------------------------------------------
/phpcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | A custom set of rules to check for a WPized WordPress project
5 |
6 |
12 |
13 | .
14 |
15 |
16 | /vendor/*
17 |
18 |
19 | /node_modules/*
20 |
21 |
22 | /build/*
23 |
24 |
25 | *.js
26 |
27 |
28 | *.scss
29 | *.css
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
43 |
44 |
45 |
46 |
57 |
58 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
74 |
75 |
76 |
84 |
85 |
86 | *\.php
87 |
88 |
89 |
90 |
95 |
96 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
130 |
131 |
147 |
155 |
156 |
--------------------------------------------------------------------------------
/src/Integrations/User.php:
--------------------------------------------------------------------------------
1 | items = $args['items'] ?? array();
89 | $this->user_id = $args['user_id'] ?? null;
90 | $this->title = $args['title'] ?? '';
91 | $this->init_priority = $args['init_priority'] ?? 10;
92 | $this->tabs = $args['tabs'] ?? array();
93 | $this->option_name = $args['meta_key'] ?? '';
94 |
95 | if ( isset( $args['display'] ) && is_callable( $args['display'] ) ) {
96 | $this->display = $args['display'];
97 | } else {
98 | $this->display = function () use ( $args ) {
99 | return $args['display'] ?? true;
100 | };
101 | }
102 |
103 | $this->id = $args['id'] ?? 'user__' . sanitize_title( $this->title ) . '__' . wp_generate_uuid4();
104 |
105 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
106 | add_action( 'show_user_profile', array( $this, 'render_edit_form' ) );
107 | add_action( 'edit_user_profile', array( $this, 'render_edit_form' ) );
108 | add_action( 'personal_options_update', array( $this, 'save' ) );
109 | add_action( 'edit_user_profile_update', array( $this, 'save' ) );
110 | add_action( 'init', array( $this, 'register_meta' ), $this->init_priority );
111 | }
112 | }
113 |
114 | /**
115 | * Registers meta fields for user profiles.
116 | *
117 | * Normalizes the items and registers each meta field with specific properties
118 | * such as type, description, default value, and sanitize callback.
119 | *
120 | * @return void
121 | */
122 | public function register_meta(): void {
123 | $items = $this->normalize_items( $this->items );
124 |
125 | foreach ( $items as $item ) {
126 | register_meta(
127 | 'user',
128 | $item['id'],
129 | array(
130 | 'type' => $this->custom_fields->get_wp_type( $item ),
131 | 'description' => $item['label'],
132 | 'single' => true,
133 | 'default' => $this->custom_fields->get_default_value( $item ),
134 | 'sanitize_callback' => $this->custom_fields->sanitize_item_value( $item ),
135 | ),
136 | );
137 | }
138 | }
139 |
140 | /**
141 | * Renders the edit form for a user.
142 | *
143 | * @param WP_User $user The user object whose profile is being edited.
144 | *
145 | * @return void
146 | */
147 | public function render_edit_form( WP_User $user ): void {
148 | $display_callback = $this->display;
149 |
150 | if ( ! $display_callback() ) {
151 | return;
152 | }
153 |
154 | $this->user_id = $user->ID;
155 | $items = $this->normalize_items( $this->items );
156 |
157 | $this->enqueue();
158 |
159 | if ( ! empty( $this->title ) ) {
160 | ?>
161 |
title ); ?>
162 | print_app( 'user', $this->tabs );
165 | ?>
166 |
186 | user_id = $user_id;
198 |
199 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
200 | }
201 |
202 | /**
203 | * Retrieves an option value for a given name. If the option does not exist,
204 | * returns the provided default value.
205 | *
206 | * @param string $name The name of the option to retrieve.
207 | * @param mixed $default_value The value to return if the option is not found.
208 | *
209 | * @return mixed The value of the option if it exists, otherwise the default value.
210 | */
211 | public function get_option_value( string $name, mixed $default_value ): mixed {
212 | return get_user_meta( $this->get_item_id(), $name, true ) ?? $default_value;
213 | }
214 |
215 | /**
216 | * Sets a value for the specified option name.
217 | *
218 | * @param string $name The name of the option to set.
219 | * @param mixed $value The value to set for the option.
220 | *
221 | * @return bool|int True on success, false on failure.
222 | */
223 | public function set_option_value( string $name, mixed $value ): bool|int {
224 | return update_user_meta( $this->get_item_id(), $name, $value );
225 | }
226 |
227 | /**
228 | * Retrieves the item ID.
229 | *
230 | * @return int The ID of the item.
231 | */
232 | public function get_item_id(): int {
233 | return $this->user_id;
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/CLAUDE.md:
--------------------------------------------------------------------------------
1 | # CLAUDE.md
2 |
3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4 |
5 | ## Build/Test Commands
6 | - Start dev server: `npm run start`
7 | - Build for production: `npm run build`
8 | - Analyze bundle: `npm run build:analyze`
9 | - PHP code standards check: `composer run phpcs`
10 | - PHP code beautifier: `composer run phpcbf`
11 |
12 | ## Code Style Guidelines
13 | - PHP: WordPress Coding Standards (WPCS) with customizations in phpcs.xml
14 | - PHP version: 8.1+
15 | - WordPress version: 6.2+
16 | - JS: Use WordPress scripts standards
17 | - Prefix PHP globals with `wpifycf`
18 | - Translation text domain: `wpify-custom-fields`
19 | - React components use PascalCase
20 | - JS helpers use camelCase
21 | - Namespace: `Wpify\CustomFields`
22 | - PHP class files match class name (PSR-4)
23 | - Import paths: Use `@` alias for assets directory in JS
24 | - Error handling: Use custom exceptions in `Exceptions` directory
25 | - Documentation is in PHPDoc format and in docs folder in md format
26 | - When generating PHP code, always use WordPress Coding Standards
27 |
28 | ## Extending Field Types
29 | To create a custom field type, the following components are required:
30 |
31 | 1. **PHP Filters**:
32 | - `wpifycf_sanitize_{type}` - For sanitizing field values
33 | - `wpifycf_wp_type_{type}` - To specify WordPress data type (integer, number, boolean, object, array, string)
34 | - `wpifycf_default_value_{type}` - To define default values
35 |
36 | 2. **JavaScript Components**:
37 | - Create a React component for the field
38 | - Add validation method to the component (`YourComponent.checkValidity`)
39 | - Register the field via `addFilter('wpifycf_field_{type}', 'wpify_custom_fields', () => YourComponent)`
40 |
41 | 3. **Multi-field Types**:
42 | - Custom field types can have multi-versions by prefixing with `multi_`
43 | - Leverage the existing `MultiField` component for implementation
44 | - Use `checkValidityMultiFieldType` helper for validation
45 |
46 | 4. **Field Component Structure**:
47 | - Field components receive props like `id`, `htmlId`, `onChange`, `value`, etc.
48 | - CSS classes should follow pattern: `wpifycf-field-{type}`
49 | - Return JSX with appropriate HTML elements
50 |
51 | ## Documentation Standards
52 | When writing or updating documentation:
53 |
54 | ### PHP Code Examples
55 | - Use tabs for indentation, not spaces
56 | - Follow WordPress Coding Standards for all PHP examples:
57 | - Add spaces inside parentheses for conditions: `if ( ! empty( $var ) )`
58 | - Add spaces after control structure keywords: `if (...) {`
59 | - Add spaces around logical operators: `$a && $b`, `! $condition`
60 | - Add spaces around string concatenation: `$a . ' ' . $b`
61 | - Add spaces for function parameters: `function_name( $param1, $param2 )`
62 | - Use proper array formatting with tabs for indentation:
63 | ```php
64 | array(
65 | 'key1' => 'value1',
66 | 'key2' => 'value2',
67 | )
68 | ```
69 | - Maintain consistent spacing around array arrow operators: `'key' => 'value'`
70 | - Use spaces in associative array access: `$array[ 'key' ]`
71 |
72 | ### Documentation Structure for Field Types
73 | Field type documentation should follow this consistent structure:
74 | 1. **Title and Description** - Clear explanation of the field's purpose
75 | 2. **Field Type Definition** - Example code following WordPress coding standards
76 | 3. **Properties Section**:
77 | - Default field properties
78 | - Specific properties unique to the field type
79 | 4. **Stored Value** - Explanation of how data is stored in the database
80 | 5. **Example Usage** - Real-world examples with WordPress coding standards
81 | 6. **Notes** - Important details about the field's behavior and uses
82 |
83 | ### Documentation Structure for Integrations
84 | Integration documentation should follow this consistent structure:
85 | 1. **Title and Overview** - Clear explanation of the integration's purpose
86 | 2. **Requirements** - Any specific plugins or dependencies required (if applicable)
87 | 3. **Usage Example** - PHP code example following WordPress coding standards
88 | 4. **Parameters Section**:
89 | - Required parameters with descriptions
90 | - Optional parameters with descriptions and default values
91 | 5. **Data Storage** - How and where the integration stores its data
92 | 6. **Retrieving Data** - How to access stored data programmatically
93 | 7. **Advanced Usage** - Examples of tabs, conditional display, etc. (as applicable)
94 |
95 | ### Security in Examples
96 | - Always include proper data escaping in examples:
97 | - `esc_html()` for plain text output
98 | - `esc_attr()` for HTML attributes
99 | - `esc_url()` for URLs
100 | - `wp_kses()` for allowing specific HTML
101 |
102 | ### Consistency
103 | - Maintain consistent terminology across all documentation files
104 | - Use consistent formatting for property descriptions
105 | - Keep parameter documentation format consistent: `name` _(type)_ - description
106 | - When documenting integrations, use consistent parameter naming and structure
107 |
108 | ### Integration-Specific Notes
109 | - For WooCommerce integrations, always mention compatibility with HPOS when applicable
110 | - Product Options integrations should list common tab IDs from WooCommerce
111 | - Order and Subscription integrations should include examples of retrieving meta
112 | - All integration documentation should include examples of tabs and conditional display
113 | - When documenting options pages, always include proper menu/page configuration
114 |
115 | ### File Organization
116 | - Field type documentation goes in `docs/field-types/`
117 | - Integration documentation goes in `docs/integrations/`
118 | - Feature documentation goes in `docs/features/`
119 | - All documentation files should use `.md` extension
120 | - Main index files (integrations.md, field-types.md) should link to all related docs
121 |
122 | ## Conditional Fields
123 | The plugin provides a robust conditional logic system for dynamically showing/hiding fields:
124 |
125 | ### Condition Structure
126 | Each condition requires:
127 | - `field`: The ID of the field to check (can use path references)
128 | - `condition`: The comparison operator
129 | - `value`: The value to compare against
130 |
131 | ### Available Operators
132 | - `==`: Equal (default)
133 | - `!=`: Not equal
134 | - `>`, `>=`, `<`, `<=`: Comparison operators
135 | - `between`: Value is between two numbers, inclusive
136 | - `contains`, `not_contains`: String contains/doesn't contain value
137 | - `in`, `not_in`: Value is/isn't in an array
138 | - `empty`, `not_empty`: Value is/isn't empty
139 |
140 | ### Multiple Conditions
141 | - Combine with `'and'` (default) or `'or'` between conditions
142 | - Create nested condition groups with sub-arrays for complex logic
143 |
144 | ### Path References
145 | - Dot notation for nested fields: `parent.child`
146 | - Hash symbols for relative paths: `#` (parent), `##` (grandparent)
147 | - Array access: `multi_field[0]` for specific items
148 |
149 | ### Technical Implementation
150 | - Conditional logic lives in `Field.js`, `hooks.js` (useConditions), and `functions.js`
151 | - Hidden fields are still submitted but have `data-hide-field="true"` attribute
152 | - Conditions are evaluated in real-time as users interact with the form
153 |
--------------------------------------------------------------------------------
/src/Integrations/Taxonomy.php:
--------------------------------------------------------------------------------
1 | taxonomy = $args['taxonomy'] ?? '';
91 | $this->items = $args['items'] ?? array();
92 |
93 | if ( empty( $this->taxonomy ) ) {
94 | throw new MissingArgumentException(
95 | sprintf(
96 | /* translators: %1$s is a list of missing arguments, %2$s is the class name. */
97 | esc_html( __( 'Missing arguments %1$s in class %2$s.', 'wpify-custom-fields' ) ),
98 | esc_html( implode( ', ', array( 'taxonomy' ) ) ),
99 | __CLASS__,
100 | ),
101 | );
102 | }
103 |
104 | $this->id = $args['id'] ?? sanitize_title( $this->taxonomy ) . '_' . wp_generate_uuid4();
105 | $this->hook_priority = $args['hook_priority'] ?? 10;
106 | $this->init_priority = $args['init_priority'] ?? 10;
107 | $this->tabs = $args['tabs'] ?? array();
108 | $this->option_name = $args['meta_key'] ?? '';
109 |
110 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
111 | add_action( $this->taxonomy . '_add_form_fields', array( $this, 'render_add_form' ), $this->hook_priority );
112 | add_action(
113 | $this->taxonomy . '_edit_form_fields',
114 | array(
115 | $this,
116 | 'render_edit_form',
117 | ),
118 | $this->hook_priority
119 | );
120 | add_action( 'created_' . $this->taxonomy, array( $this, 'save' ) );
121 | add_action( 'edited_' . $this->taxonomy, array( $this, 'save' ) );
122 | add_action( 'init', array( $this, 'register_meta' ), $this->init_priority );
123 | }
124 | }
125 |
126 | /**
127 | * Renders the form for adding a new term.
128 | *
129 | * @return void
130 | */
131 | public function render_add_form(): void {
132 | $this->term_id = 0;
133 | $this->enqueue();
134 | $this->print_app( 'add_term', $this->tabs );
135 |
136 | $items = $this->normalize_items( $this->items );
137 |
138 | foreach ( $items as $item ) {
139 | $this->print_field( $item, array(), 'div', 'form-field' );
140 | }
141 | }
142 |
143 | /**
144 | * Renders the edit form for a given term.
145 | *
146 | * @param WP_Term $term The term object for which the edit form is being rendered.
147 | *
148 | * @return void
149 | */
150 | public function render_edit_form( WP_Term $term ): void {
151 | $this->term_id = $term->term_id;
152 | $this->enqueue();
153 | ?>
154 |
155 | |
156 | print_app( 'edit_term', $this->tabs ); ?>
157 | |
158 |
159 | normalize_items( $this->items );
162 |
163 | foreach ( $items as $item ) {
164 | $this->print_field( $item, array(), 'tr' );
165 | }
166 | }
167 |
168 | /**
169 | * Registers custom metadata for the specified taxonomy using the provided items.
170 | *
171 | * This method normalizes the items and registers each one as term meta for the given taxonomy.
172 | * It sets various properties such as type, description, default value, and sanitize callback.
173 | *
174 | * @return void
175 | */
176 | public function register_meta(): void {
177 | $items = $this->normalize_items( $this->items );
178 |
179 | foreach ( $items as $item ) {
180 | register_term_meta(
181 | $this->taxonomy,
182 | $item['id'],
183 | array(
184 | 'type' => $this->custom_fields->get_wp_type( $item ),
185 | 'description' => $item['label'],
186 | 'single' => true,
187 | 'default' => $this->custom_fields->get_default_value( $item ),
188 | 'sanitize_callback' => $this->custom_fields->sanitize_item_value( $item ),
189 | 'show_in_rest' => false,
190 | ),
191 | );
192 | }
193 | }
194 |
195 | /**
196 | * Save method for handling term data and updating custom fields.
197 | *
198 | * @param int $term_id The ID of the term being saved.
199 | *
200 | * @return void
201 | */
202 | public function save( int $term_id ): void {
203 | $this->term_id = $term_id;
204 |
205 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
206 | }
207 |
208 | /**
209 | * Retrieves the value of a specified option, if available.
210 | *
211 | * @param string $name The name of the option to retrieve.
212 | * @param mixed $default_value The default value to return if the option is not found.
213 | *
214 | * @return mixed The value of the specified option, or the default value if the option is not found.
215 | */
216 | public function get_option_value( string $name, mixed $default_value ): mixed {
217 | if ( $this->get_item_id() ) {
218 | return get_term_meta( $this->get_item_id(), $name, true ) ?? $default_value;
219 | } else {
220 | return $default_value;
221 | }
222 | }
223 |
224 | /**
225 | * Sets the value of a specified option.
226 | *
227 | * @param string $name The name of the option to set.
228 | * @param mixed $value The value to set for the specified option.
229 | *
230 | * @return WP_Error|bool|int The result of attempting to update the option value. It returns true on success, false on failure, or an error object on error.
231 | */
232 | public function set_option_value( string $name, mixed $value ): WP_Error|bool|int {
233 | return update_term_meta( $this->get_item_id(), $name, $value );
234 | }
235 |
236 | /**
237 | * Retrieves the item ID.
238 | *
239 | * @return int The ID of the item.
240 | */
241 | public function get_item_id(): int {
242 | return $this->term_id;
243 | }
244 | }
245 |
--------------------------------------------------------------------------------
/src/Fields/DirectFileField.php:
--------------------------------------------------------------------------------
1 | custom_fields = $custom_fields;
40 | $this->helpers = $custom_fields->helpers;
41 |
42 | $this->init_hooks();
43 | }
44 |
45 | /**
46 | * Initialize WordPress hooks.
47 | *
48 | * @return void
49 | */
50 | private function init_hooks(): void {
51 | add_filter( 'wpifycf_sanitize_direct_file', array( $this, 'sanitize_direct_file' ), 10, 3 );
52 | add_filter( 'wpifycf_sanitize_multi_direct_file', array( $this, 'sanitize_multi_direct_file' ), 10, 3 );
53 | add_filter( 'wpifycf_wp_type_direct_file', array( $this, 'get_wp_type' ), 10, 2 );
54 | add_filter( 'wpifycf_wp_type_multi_direct_file', array( $this, 'get_wp_type_multi' ), 10, 2 );
55 | add_filter( 'wpifycf_default_value_direct_file', array( $this, 'get_default_value' ), 10, 2 );
56 | add_filter( 'wpifycf_default_value_multi_direct_file', array( $this, 'get_default_value_multi' ), 10, 2 );
57 | }
58 |
59 | /**
60 | * Sanitizes the direct_file field value.
61 | *
62 | * @param mixed $value The sanitized value.
63 | * @param mixed $original_value The original value.
64 | * @param array $item The field definition.
65 | *
66 | * @return string The absolute file path or empty string.
67 | */
68 | public function sanitize_direct_file( $value, $original_value, array $item ): string {
69 | // If value is empty, handle deletion if needed.
70 | if ( empty( $value ) ) {
71 | $this->maybe_delete_old_file( $original_value, $item );
72 | return '';
73 | }
74 |
75 | // If value is the same as original, no change needed.
76 | if ( $value === $original_value ) {
77 | return sanitize_text_field( $value );
78 | }
79 |
80 | // Check if this is a temp file that needs to be moved.
81 | $temp_dir = $this->helpers->get_direct_file_temp_dir();
82 | if ( strpos( $value, $temp_dir ) === 0 ) {
83 | // This is a temp file, move it to the target directory.
84 | $target_directory = $this->get_target_directory( $item );
85 | $filename = $this->helpers->get_filename_from_path( $value );
86 | $replace_existing = $item['replace_existing'] ?? false;
87 |
88 | $moved_path = $this->helpers->move_temp_to_directory(
89 | $value,
90 | $target_directory,
91 | $filename,
92 | $replace_existing
93 | );
94 |
95 | if ( $moved_path ) {
96 | // Delete old file if it exists and is different.
97 | if ( ! empty( $original_value ) && $original_value !== $moved_path ) {
98 | $this->maybe_delete_old_file( $original_value, $item );
99 | }
100 |
101 | return $moved_path;
102 | }
103 |
104 | // If move failed, return empty string.
105 | return '';
106 | }
107 |
108 | // Value is already a final path (e.g., editing existing record).
109 | // Validate it exists.
110 | if ( file_exists( $value ) ) {
111 | return sanitize_text_field( $value );
112 | }
113 |
114 | return '';
115 | }
116 |
117 | /**
118 | * Sanitizes the multi_direct_file field value.
119 | *
120 | * @param mixed $value The sanitized value.
121 | * @param mixed $original_value The original value.
122 | * @param array $item The field definition.
123 | *
124 | * @return array Array of file paths.
125 | */
126 | public function sanitize_multi_direct_file( $value, $original_value, array $item ): array {
127 | // Decode JSON or convert to array, handling edge cases.
128 | if ( is_string( $value ) ) {
129 | $decoded = json_decode( $value, true );
130 | $value = is_array( $decoded ) ? $decoded : array( $value );
131 | } else {
132 | $value = (array) $value;
133 | }
134 |
135 | if ( is_string( $original_value ) ) {
136 | $decoded = json_decode( $original_value, true );
137 | $original_value = is_array( $decoded ) ? $decoded : array( $original_value );
138 | } else {
139 | $original_value = (array) $original_value;
140 | }
141 |
142 | $sanitized_value = array();
143 |
144 | if ( ! is_array( $value ) ) {
145 | return array();
146 | }
147 |
148 | // Track which original files are still present.
149 | $original_files = is_array( $original_value ) ? $original_value : array();
150 | $kept_files = array();
151 |
152 | foreach ( $value as $single_value ) {
153 | $sanitized = $this->sanitize_direct_file( $single_value, '', $item );
154 | if ( ! empty( $sanitized ) ) {
155 | $sanitized_value[] = $sanitized;
156 | $kept_files[] = $sanitized;
157 | }
158 | }
159 |
160 | // Delete files that were removed.
161 | foreach ( $original_files as $original_file ) {
162 | if ( ! in_array( $original_file, $kept_files, true ) ) {
163 | $this->maybe_delete_old_file( $original_file, $item );
164 | }
165 | }
166 |
167 | return $sanitized_value;
168 | }
169 |
170 | /**
171 | * Gets the target directory for the field.
172 | *
173 | * @param array $item The field definition.
174 | *
175 | * @return string The absolute target directory path.
176 | */
177 | private function get_target_directory( array $item ): string {
178 | $directory = $item['directory'] ?? 'wp-content/uploads/direct-files/';
179 | return $this->helpers->sanitize_directory_path( $directory );
180 | }
181 |
182 | /**
183 | * Maybe deletes the old file based on the field's on_delete setting.
184 | *
185 | * @param string $file_path The file path to delete.
186 | * @param array $item The field definition.
187 | *
188 | * @return void
189 | */
190 | private function maybe_delete_old_file( string $file_path, array $item ): void {
191 | if ( empty( $file_path ) || ! file_exists( $file_path ) ) {
192 | return;
193 | }
194 |
195 | $on_delete = $item['on_delete'] ?? 'keep';
196 |
197 | if ( 'delete' === $on_delete ) {
198 | $this->helpers->delete_direct_file( $file_path );
199 | }
200 | }
201 |
202 | /**
203 | * Returns the WordPress data type for direct_file field.
204 | *
205 | * @return string The WordPress data type.
206 | */
207 | public function get_wp_type(): string {
208 | return 'string';
209 | }
210 |
211 | /**
212 | * Returns the WordPress data type for multi_direct_file field.
213 | *
214 | * @return string The WordPress data type.
215 | */
216 | public function get_wp_type_multi(): string {
217 | return 'array';
218 | }
219 |
220 | /**
221 | * Returns the default value for direct_file field.
222 | *
223 | * @return string Empty string as default.
224 | */
225 | public function get_default_value(): string {
226 | return '';
227 | }
228 |
229 | /**
230 | * Returns the default value for multi_direct_file field.
231 | *
232 | * @return array Empty array as default.
233 | */
234 | public function get_default_value_multi(): array {
235 | return array();
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/Api.php:
--------------------------------------------------------------------------------
1 | register_rest_route(
43 | 'url-title',
44 | WP_REST_Server::READABLE,
45 | fn( WP_REST_Request $request ) => $this->helpers->get_url_title( $request->get_param( 'url' ) ),
46 | array(
47 | 'url' => array( 'required' => true ),
48 | ),
49 | );
50 |
51 | $this->register_rest_route(
52 | 'posts',
53 | WP_REST_Server::READABLE,
54 | fn( WP_REST_Request $request ) => $this->helpers->get_posts( $request->get_params() ),
55 | array(
56 | 'post_type' => array( 'required' => true ),
57 | ),
58 | );
59 |
60 | $this->register_rest_route(
61 | 'terms',
62 | WP_REST_Server::READABLE,
63 | fn( WP_REST_Request $request ) => $this->helpers->get_terms( $request->get_params() ),
64 | array(
65 | 'taxonomy' => array( 'required' => true ),
66 | ),
67 | );
68 |
69 | $this->register_rest_route(
70 | 'mapycz-api-key',
71 | WP_REST_Server::EDITABLE,
72 | fn( WP_REST_Request $request ) => update_option( 'mapy_cz_api_key', $request->get_param( 'api_key' ) ),
73 | array( 'api_key' => array( 'required' => true ) ),
74 | );
75 |
76 | $this->register_rest_route(
77 | 'mapycz-api-key',
78 | WP_REST_Server::READABLE,
79 | fn() => get_option( 'mapy_cz_api_key' ),
80 | );
81 |
82 | $this->register_rest_route(
83 | 'direct-file-upload',
84 | WP_REST_Server::CREATABLE,
85 | array( $this, 'handle_direct_file_upload' ),
86 | array(
87 | 'field_id' => array( 'required' => false ),
88 | ),
89 | );
90 |
91 | $this->register_rest_route(
92 | 'direct-file-info',
93 | WP_REST_Server::READABLE,
94 | array( $this, 'handle_direct_file_info' ),
95 | array(
96 | 'file_path' => array( 'required' => true ),
97 | ),
98 | );
99 | }
100 |
101 | /**
102 | * Registers a REST API route with specified parameters.
103 | *
104 | * @param string $route The endpoint route.
105 | * @param string $method The HTTP method (GET, POST, etc.) for this route.
106 | * @param callable $callback The callback function to handle the request.
107 | * @param array $args Optional. Array of arguments for the route.
108 | *
109 | * @return void
110 | */
111 | public function register_rest_route( string $route, string $method, callable $callback, array $args = array() ): void {
112 | register_rest_route(
113 | $this->get_rest_namespace(),
114 | $route,
115 | array(
116 | 'methods' => $method,
117 | 'callback' => $callback,
118 | 'permission_callback' => array( $this, 'permissions_callback' ),
119 | 'args' => $args,
120 | ),
121 | );
122 | }
123 |
124 | /**
125 | * Retrieves the REST namespace for the plugin.
126 | *
127 | * @return string The REST namespace string constructed from the plugin's basename.
128 | */
129 | public function get_rest_namespace(): string {
130 | return $this->custom_fields->get_api_basename() . '/wpifycf/v1';
131 | }
132 |
133 | /**
134 | * Checks if the current user has permission to edit posts.
135 | *
136 | * @return bool True if the current user can edit posts, false otherwise.
137 | */
138 | public function permissions_callback(): bool {
139 | return current_user_can( 'edit_posts' );
140 | }
141 |
142 | /**
143 | * Handles direct file upload to temporary directory.
144 | *
145 | * @return array|\WP_Error Response array with temp_path or WP_Error on failure.
146 | */
147 | public function handle_direct_file_upload() {
148 | // Check if file was uploaded.
149 | if ( empty( $_FILES['file'] ) ) { // phpcs:ignore
150 | return new \WP_Error( 'no_file', __( 'No file was uploaded.', 'wpify-custom-fields' ), array( 'status' => 400 ) );
151 | }
152 |
153 | $file = $_FILES['file']; // phpcs:ignore
154 |
155 | // Check for upload errors.
156 | if ( UPLOAD_ERR_OK !== $file['error'] ) {
157 | return new \WP_Error( 'upload_error', __( 'File upload failed.', 'wpify-custom-fields' ), array( 'status' => 400 ) );
158 | }
159 |
160 | // Validate file size.
161 | $max_upload_size = wp_max_upload_size();
162 | if ( $file['size'] > $max_upload_size ) {
163 | return new \WP_Error(
164 | 'file_too_large',
165 | sprintf(
166 | /* translators: %s: maximum file size */
167 | __( 'File size exceeds maximum allowed size of %s.', 'wpify-custom-fields' ),
168 | size_format( $max_upload_size )
169 | ),
170 | array( 'status' => 400 )
171 | );
172 | }
173 |
174 | // Sanitize filename.
175 | $filename = sanitize_file_name( $file['name'] );
176 |
177 | // Get temp directory.
178 | $temp_dir = $this->helpers->get_direct_file_temp_dir();
179 |
180 | // Ensure temp directory exists.
181 | if ( ! file_exists( $temp_dir ) ) {
182 | if ( ! wp_mkdir_p( $temp_dir ) ) {
183 | return new \WP_Error( 'directory_creation_failed', __( 'Failed to create temporary directory.', 'wpify-custom-fields' ), array( 'status' => 500 ) );
184 | }
185 | }
186 |
187 | // Generate unique filename to prevent conflicts.
188 | $unique_filename = wp_unique_filename( $temp_dir, $filename );
189 | $temp_path = trailingslashit( $temp_dir ) . $unique_filename;
190 |
191 | // Move uploaded file to temp directory.
192 | if ( ! move_uploaded_file( $file['tmp_name'], $temp_path ) ) {
193 | return new \WP_Error( 'move_failed', __( 'Failed to save uploaded file.', 'wpify-custom-fields' ), array( 'status' => 500 ) );
194 | }
195 |
196 | // Return temp path and metadata.
197 | return array(
198 | 'temp_path' => $temp_path,
199 | 'filename' => $unique_filename,
200 | 'size' => $file['size'],
201 | 'type' => $file['type'],
202 | );
203 | }
204 |
205 | /**
206 | * Handles retrieving file information for a direct file.
207 | *
208 | * @param WP_REST_Request $request The REST API request object.
209 | *
210 | * @return array|\WP_Error File information or error.
211 | */
212 | public function handle_direct_file_info( WP_REST_Request $request ) {
213 | $file_path = $request->get_param( 'file_path' );
214 |
215 | if ( empty( $file_path ) ) {
216 | return new \WP_Error( 'no_file_path', __( 'No file path provided.', 'wpify-custom-fields' ), array( 'status' => 400 ) );
217 | }
218 |
219 | // Sanitize the file path.
220 | $file_path = sanitize_text_field( $file_path );
221 |
222 | // Check if file exists.
223 | if ( ! file_exists( $file_path ) ) {
224 | return new \WP_Error( 'file_not_found', __( 'File not found.', 'wpify-custom-fields' ), array( 'status' => 404 ) );
225 | }
226 |
227 | // Get file information.
228 | $filesize = filesize( $file_path );
229 | $filetype = wp_check_filetype( $file_path );
230 |
231 | return array(
232 | 'size' => $filesize,
233 | 'type' => $filetype['type'],
234 | 'filename' => basename( $file_path ),
235 | );
236 | }
237 | }
238 |
--------------------------------------------------------------------------------
/src/Integrations/Comment.php:
--------------------------------------------------------------------------------
1 | title = $args['title'] ?? '';
104 | $this->priority = $args['priority'] ?? self::PRIORITY_DEFAULT;
105 | $this->callback_args = $args['callback_args'] ?? array();
106 | $this->id = $args['id'] ?? sanitize_title(
107 | join(
108 | '_',
109 | array(
110 | $this->title,
111 | 'comment',
112 | $this->priority,
113 | ),
114 | ),
115 | );
116 | $this->nonce = $this->id . '_nonce';
117 | $this->option_name = $args['meta_key'] ?? '';
118 | $this->items = $args['items'] ?? array();
119 | $this->tabs = $args['tabs'] ?? array();
120 |
121 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
122 | add_action( 'add_meta_boxes_comment', array( $this, 'add_meta_box' ) );
123 | add_action( 'edit_comment', array( $this, 'save_meta_box' ), 10, 2 );
124 | }
125 | }
126 |
127 | /**
128 | * Adds a meta box to the comment editing screen.
129 | *
130 | * @param WP_Comment $comment The comment object for which the meta box is being added.
131 | *
132 | * @return void
133 | */
134 | public function add_meta_box( WP_Comment $comment ): void {
135 | $this->set_comment( $comment->comment_ID );
136 | add_meta_box(
137 | $this->id,
138 | $this->title,
139 | array( $this, 'render' ),
140 | 'comment',
141 | 'normal',
142 | $this->priority,
143 | $this->callback_args,
144 | );
145 | }
146 |
147 | /**
148 | * Sets the comment ID for the current instance if it has not been set already.
149 | *
150 | * @param int $comment_id The ID of the comment to set.
151 | *
152 | * @return void
153 | */
154 | public function set_comment( int $comment_id ): void {
155 | if ( empty( $this->comment_id ) ) {
156 | $this->comment_id = $comment_id;
157 | }
158 | }
159 |
160 | /**
161 | * Saves the meta box data for a given comment.
162 | *
163 | * @param int $comment_id The ID of the comment being saved.
164 | *
165 | * @return void We don't need the return value.
166 | */
167 | public function save_meta_box( int $comment_id ): void {
168 | if ( ! isset( $_POST[ $this->nonce ] ) ) {
169 | return;
170 | }
171 |
172 | $nonce = sanitize_text_field( wp_unslash( $_POST[ $this->nonce ] ) );
173 |
174 | if ( ! wp_verify_nonce( $nonce, $this->id ) ) {
175 | return;
176 | }
177 |
178 | $this->set_comment( $comment_id );
179 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
180 | }
181 |
182 | /**
183 | * Registers metadata for each item in the list of normalized items.
184 | *
185 | * This method iterates over the items, normalizes them, and registers post metadata
186 | * for each post type with detailed settings including type, description, default values,
187 | * and sanitization callbacks.
188 | *
189 | * @return void
190 | */
191 | public function register_meta(): void {
192 | $items = $this->normalize_items( $this->items );
193 |
194 | foreach ( $items as $item ) {
195 | register_meta(
196 | 'comment',
197 | $item['id'],
198 | array(
199 | 'type' => $this->custom_fields->get_wp_type( $item ),
200 | 'description' => $item['label'],
201 | 'single' => true,
202 | 'default' => $this->custom_fields->get_default_value( $item ),
203 | 'sanitize_callback' => $this->custom_fields->sanitize_item_value( $item ),
204 | 'show_in_rest' => false,
205 | ),
206 | );
207 | }
208 | }
209 |
210 | /**
211 | * Renders the comment form with the normalized items, sets the current comment, enqueues necessary scripts,
212 | * prints the application structure, and prints each field.
213 | *
214 | * @param WP_Comment $comment The comment object for which the form is being rendered.
215 | *
216 | * @return void
217 | */
218 | public function render( WP_Comment $comment ): void {
219 | $items = $this->normalize_items( $this->items );
220 |
221 | $this->set_comment( $comment->comment_ID );
222 | $this->enqueue();
223 | $this->print_app( 'comment', $this->tabs );
224 |
225 | wp_nonce_field( $this->id, $this->nonce );
226 |
227 | foreach ( $items as $item ) {
228 | $this->print_field( $item );
229 | }
230 | }
231 |
232 | /**
233 | * Retrieve the value of a specified option.
234 | *
235 | * @param string $name The name of the option.
236 | * @param mixed $default_value The default value to return if the option is not found.
237 | *
238 | * @return mixed The value of the specified option if it exists, otherwise the default value.
239 | */
240 | public function get_option_value( string $name, mixed $default_value ): mixed {
241 | return get_comment_meta( $this->comment_id, $name, true ) ?? $default_value;
242 | }
243 |
244 | /**
245 | * Set the value for a specified option.
246 | *
247 | * @param string $name The name of the option.
248 | * @param mixed $value The value to set for the option.
249 | * @param array $item An optional array to specify additional item details.
250 | *
251 | * @return bool True if the option value was updated successfully, false otherwise.
252 | */
253 | public function set_option_value( string $name, mixed $value, array $item = array() ): bool {
254 | return update_comment_meta( $this->comment_id, $name, $value );
255 | }
256 |
257 | /**
258 | * Retrieve the item ID.
259 | *
260 | * @return int The ID of the item.
261 | */
262 | public function get_item_id(): int {
263 | return $this->comment_id;
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/src/Integrations/BaseIntegration.php:
--------------------------------------------------------------------------------
1 | id;
61 | }
62 |
63 | foreach ( $items as $key => $item ) {
64 | if ( empty( $item['id'] ) && is_string( $key ) ) {
65 | $item['id'] = $key;
66 | } elseif ( empty( $item['id'] ) ) {
67 | $item['id'] = uniqid();
68 | }
69 |
70 | $next_items[ $item['id'] ] = $this->normalize_item( $item, $global_id );
71 | }
72 |
73 | return apply_filters( 'wpifycf_items', array_values( $next_items ), $this->id );
74 | }
75 |
76 | /**
77 | * Normalizes a single item.
78 | *
79 | * @param array $item Item to normalize.
80 | * @param string $global_id A global identifier for the item.
81 | *
82 | * @return array Normalized item.
83 | */
84 | protected function normalize_item( array $item, string $global_id ): array {
85 | $item['global_id'] = $global_id . '__' . $item['id'];
86 |
87 | if ( isset( $item['custom_attributes'] ) ) {
88 | $item['attributes'] = $item['custom_attributes'];
89 | unset( $item['custom_attributes'] );
90 | }
91 |
92 | if ( ! empty( $item['title'] ) && empty( $item['label'] ) ) {
93 | $item['label'] = $item['title'];
94 | }
95 |
96 | if ( empty( $item['label'] ) ) {
97 | $item['label'] = '';
98 | }
99 |
100 | if ( isset( $item['desc'] ) ) {
101 | $item['description'] = $item['desc'];
102 | unset( $item['desc'] );
103 | }
104 |
105 | if ( ! isset( $item['default'] ) ) {
106 | $item['default'] = $this->custom_fields->get_default_value( $item );
107 | }
108 |
109 | /* Compatibility with WPify Woo */
110 | $type_aliases = array(
111 | 'multiswitch' => 'multi_toggle',
112 | 'switch' => 'toggle',
113 | 'multiselect' => 'multi_select',
114 | 'colorpicker' => 'color',
115 | 'gallery' => 'multi_attachment',
116 | 'repeater' => 'multi_group',
117 | );
118 |
119 | foreach ( $type_aliases as $alias => $correct ) {
120 | if ( $item['type'] === $alias ) {
121 | $item['type'] = $correct;
122 | }
123 | }
124 |
125 | if ( isset( $item['items'] ) ) {
126 | $item['items'] = $this->normalize_items( $item['items'], $item['global_id'] );
127 | }
128 |
129 | if ( isset( $item['options'] ) ) {
130 | if ( is_callable( $item['options'] ) && isset( $item['async'] ) && false === $item['async'] ) {
131 | $item['options'] = $this->normalize_options( $item['options']() );
132 | } elseif ( is_callable( $item['options'] ) ) {
133 | if ( empty( $item['options_key'] ) ) {
134 | $item['options_key'] = $item['global_id'];
135 | }
136 |
137 | $item['options_callback'] = $item['options'];
138 | $item['options'] = array();
139 | $item['async'] = true;
140 | } elseif ( is_array( $item['options'] ) ) {
141 | $item['options'] = $this->normalize_options( $item['options'] );
142 | }
143 | }
144 |
145 | return $item;
146 | }
147 |
148 | /**
149 | * Normalizes an array of options.
150 | *
151 | * @param array $options Options to normalize.
152 | *
153 | * @return array Normalized options.
154 | */
155 | public function normalize_options( array $options ): array {
156 | $next_options = array();
157 |
158 | foreach ( $options as $key => $value ) {
159 | if ( is_array( $value ) && isset( $value['label'] ) && isset( $value['value'] ) ) {
160 | $next_options[] = array(
161 | ...$value,
162 | 'value' => strval( $value['value'] ),
163 | );
164 | } elseif ( is_string( $value ) ) {
165 | $next_options[] = array(
166 | 'label' => $value,
167 | 'value' => strval( $key ),
168 | );
169 | }
170 | }
171 |
172 | return $next_options;
173 | }
174 |
175 | /**
176 | * Enqueues scripts and styles necessary for the integration.
177 | *
178 | * Only enqueues on the admin side.
179 | */
180 | public function enqueue(): void {
181 | if ( ! is_admin() ) {
182 | return;
183 | }
184 |
185 | $handle = $this->custom_fields->get_script_handle();
186 | $js = $this->custom_fields->get_js_asset( 'wpify-custom-fields' );
187 | $data = array(
188 | 'instance' => $this->custom_fields->get_script_handle(),
189 | 'stylesheet' => $this->custom_fields->get_css_asset( 'wpify-custom-fields' ),
190 | 'api_path' => $this->custom_fields->api->get_rest_namespace(),
191 | 'abspath' => ABSPATH,
192 | 'site_url' => get_site_url(),
193 | );
194 |
195 | // Dependencies for WYSIWYG field.
196 | wp_enqueue_editor();
197 | wp_tinymce_inline_scripts();
198 |
199 | $current_screen = get_current_screen();
200 |
201 | if ( ! $current_screen || ! $current_screen->is_block_editor() ) {
202 | // Dependencies for WYSIWYG field.
203 | wp_enqueue_script( 'wp-block-library' );
204 |
205 | // Dependencies for Attachment field.
206 | wp_enqueue_media();
207 |
208 | // Dependencies for Toggle field.
209 | wp_enqueue_style( 'wp-components' );
210 | }
211 |
212 | wp_enqueue_script(
213 | $handle,
214 | $js['src'],
215 | $js['dependencies'],
216 | $js['version'],
217 | array( 'in_footer' => false ),
218 | );
219 |
220 | wp_add_inline_script(
221 | $handle,
222 | 'window.wpifycf=' . wp_json_encode( $data ) . ';',
223 | 'before',
224 | );
225 | }
226 |
227 | /**
228 | * Registers REST API for options recursively.
229 | */
230 | public function register_rest_options(): void {
231 | $items = $this->normalize_items( $this->items );
232 |
233 | $this->register_options_routes( $items );
234 | }
235 |
236 | /**
237 | * Registers option routes for an array of items.
238 | *
239 | * @param array $items Array of items to register routes for.
240 | */
241 | public function register_options_routes( array $items = array() ): void {
242 | foreach ( $items as $item ) {
243 | $this->register_options_route( $item );
244 | }
245 | }
246 |
247 | /**
248 | * Registers a REST API route for a single item.
249 | *
250 | * @param array $item Item for which to register the route.
251 | */
252 | public function register_options_route( array $item ): void {
253 | if ( ! empty( $item['options_key'] ) ) {
254 | $this->custom_fields->api->register_rest_route(
255 | 'options/' . $item['options_key'],
256 | WP_REST_Server::READABLE,
257 | function ( WP_REST_Request $request ) use ( $item ) {
258 | $options = $item['options_callback']( $request->get_params() );
259 |
260 | if ( is_array( $options ) ) {
261 | return $this->normalize_options( $options );
262 | }
263 |
264 | return array();
265 | },
266 | );
267 | } elseif ( ! empty( $item['items'] ) ) {
268 | $this->register_options_routes( $item['items'] );
269 | }
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/src/Integrations/OptionsIntegration.php:
--------------------------------------------------------------------------------
1 | id . '__' . $loop : $this->id;
32 | ?>
33 | $value ) {
41 | printf( ' data-%s="%s"', esc_attr( $key ), esc_attr( $value ) );
42 | }
43 | ?>
44 | >
45 | option_name ) ) {
58 | $name = $item['id'];
59 |
60 | if ( isset( $data_attributes['loop'] ) ) {
61 | $name .= '[' . $data_attributes['loop'] . ']';
62 | }
63 | } else {
64 | $name = $this->option_name;
65 |
66 | if ( isset( $data_attributes['loop'] ) ) {
67 | $name .= '[' . $data_attributes['loop'] . ']';
68 | }
69 |
70 | $name .= '[' . $item['id'] . ']';
71 | }
72 |
73 | $item['name'] = $name;
74 | $item['value'] = $this->get_field( $item['id'], $item );
75 | $item['loop'] = $data_attributes['loop'] ?? '';
76 | $integration_id = isset( $data_attributes['loop'] ) ? $this->id . '__' . $data_attributes['loop'] : $this->id;
77 | ?>
78 | <
79 | data-item="custom_fields->helpers->json_encode( $item ) ); ?>"
80 | data-integration-id=""
81 | data-instance="custom_fields->get_script_handle() ); ?>"
82 | class="wpifycf-field-instance wpifycf-field-instance--custom_fields->get_script_handle() ); ?>"
83 | $value ) {
85 | printf( ' data-%s="%s"', esc_attr( $key ), esc_attr( $value ) );
86 | }
87 | ?>
88 | >>
89 | option_name ) ) {
104 | $data = $this->get_option_value( $this->option_name, array() );
105 |
106 | return $data[ $name ] ?? $this->custom_fields->get_default_value( $item );
107 | } else {
108 | return $this->get_option_value( $name, $this->custom_fields->get_default_value( $item ) );
109 | }
110 | }
111 |
112 | /**
113 | * Sets the field value for a given name and item.
114 | *
115 | * @param string $name Field name.
116 | * @param mixed $value Field value.
117 | * @param array $item Optional. Field item data.
118 | */
119 | public function set_field( string $name, mixed $value, array $item = array() ): mixed {
120 | if ( isset( $item['callback_set'] ) && is_callable( $item['callback_set'] ) ) {
121 | return call_user_func( $item['callback_set'], $item, $value );
122 | } elseif ( ! empty( $this->option_name ) ) {
123 | $data = $this->get_option_value( $this->option_name, array() );
124 | $data[ $name ] = $value;
125 |
126 | return $this->set_option_value( $this->option_name, $data );
127 | } else {
128 | return $this->set_option_value( $name, $value );
129 | }
130 | }
131 |
132 | /**
133 | * Sets fields for a given option name using sanitized values and item definitions.
134 | *
135 | * @param string $option_name The name of the option to set.
136 | * @param array $sanitized_values An array of sanitized values keyed by item IDs.
137 | * @param array $items An array of items where each item contains an 'id' and optionally a 'callback_set'.
138 | *
139 | * @return void
140 | */
141 | public function set_fields( string $option_name, array $sanitized_values, array $items ): void {
142 | $data = array();
143 |
144 | foreach ( $items as $item ) {
145 | if ( isset( $sanitized_values[ $item['id'] ] ) ) {
146 | if ( isset( $item['callback_set'] ) && is_callable( $item['callback_set'] ) ) {
147 | $data[ $item['id'] ] = call_user_func( $item['callback_set'], $item, $sanitized_values[ $item['id'] ] );
148 | } else {
149 | $data[ $item['id'] ] = $sanitized_values[ $item['id'] ];
150 | }
151 | }
152 | }
153 |
154 | $this->set_option_value( $option_name, $data );
155 | }
156 |
157 | /**
158 | * Retrieves the value of a given option.
159 | *
160 | * @param string $name The name of the option to retrieve.
161 | * @param mixed $default_value The default value to return if the option is not set.
162 | *
163 | * @return mixed The value of the option or the default value if not set.
164 | */
165 | abstract public function get_option_value( string $name, mixed $default_value ): mixed;
166 |
167 | /**
168 | * Sets the value of an option.
169 | *
170 | * @param string $name The name of the option to set.
171 | * @param mixed $value The value to assign to the option.
172 | */
173 | abstract public function set_option_value( string $name, mixed $value );
174 |
175 | /**
176 | * Set fields from $_POST request.
177 | *
178 | * @param array $items An array of items where each item contains an 'id' and optionally a 'callback_set'.
179 | * @param mixed|null $loop_id Optional. The loop ID to use for the fields.
180 | */
181 | public function set_fields_from_post_request( array $items, mixed $loop_id = null ): void {
182 | // Sanitization is done via custom sanitizer.
183 | // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
184 |
185 | // Nonce verification not needed here, is verified by caller.
186 | // phpcs:disable WordPress.Security.NonceVerification.Missing
187 |
188 | if ( ! empty( $this->option_name ) ) {
189 | if ( is_null( $loop_id ) ) {
190 | $post_data = isset( $_POST[ $this->option_name ] ) ? wp_unslash( $_POST[ $this->option_name ] ) : array();
191 | } else {
192 | $post_data = isset( $_POST[ $this->option_name ][ $loop_id ] ) ? wp_unslash( $_POST[ $this->option_name ][ $loop_id ] ) : array();
193 | }
194 |
195 | $this->set_fields(
196 | $this->option_name,
197 | $this->custom_fields->sanitize_option_value( $items )( $post_data ),
198 | $items
199 | );
200 | } else {
201 | foreach ( $items as $item ) {
202 | $wp_type = $this->custom_fields->get_wp_type( $item );
203 |
204 | if ( is_null( $loop_id ) ) {
205 | if ( ! isset( $_POST[ $item['id'] ] ) ) {
206 | continue;
207 | }
208 |
209 | $value = wp_unslash( $_POST[ $item['id'] ] );
210 | } else {
211 | if ( ! isset( $_POST[ $item['id'] ][ $loop_id ] ) ) {
212 | continue;
213 | }
214 |
215 | $value = wp_unslash( $_POST[ $item['id'] ][ $loop_id ] );
216 | }
217 |
218 | if ( 'string' !== $wp_type ) {
219 | $value = json_decode( $value, ARRAY_A );
220 | }
221 |
222 | $this->set_field(
223 | $item['id'],
224 | $this->custom_fields->sanitize_item_value( $item )( $value ),
225 | $item
226 | );
227 | }
228 | }
229 | // phpcs:enable
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/src/Integrations/Metabox.php:
--------------------------------------------------------------------------------
1 | title = $args['title'] ?? '';
128 | $this->screen = $args['screen'] ?? null;
129 | $this->context = $args['context'] ?? self::CONTEXT_NORMAL;
130 | $this->priority = $args['priority'] ?? self::PRIORITY_DEFAULT;
131 | $this->callback_args = $args['callback_args'] ?? array();
132 | $this->id = $args['id'] ?? sanitize_title(
133 | join(
134 | '_',
135 | array(
136 | $this->title,
137 | $this->screen,
138 | $this->context,
139 | $this->priority,
140 | ),
141 | ),
142 | );
143 | $this->nonce = $this->id . '_nonce';
144 | $this->option_name = $args['meta_key'] ?? '';
145 | $this->items = $args['items'] ?? array();
146 | $this->post_types = $args['post_types'] ?? array();
147 | $this->tabs = $args['tabs'] ?? array();
148 |
149 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
150 | add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
151 | add_action( 'save_post', array( $this, 'save_meta_box' ), 10, 2 );
152 | add_action( 'init', array( $this, 'register_meta' ) );
153 | }
154 | }
155 |
156 | /**
157 | * Adds a meta box to the specified post type.
158 | *
159 | * @param string $post_type The post type to which the meta box should be added.
160 | *
161 | * @return void
162 | */
163 | public function add_meta_box( string $post_type ): void {
164 | if ( in_array( $post_type, $this->post_types, true ) ) {
165 | add_meta_box(
166 | $this->id,
167 | $this->title,
168 | array( $this, 'render' ),
169 | $post_type,
170 | $this->context,
171 | $this->priority,
172 | $this->callback_args,
173 | );
174 | }
175 | }
176 |
177 | /**
178 | * Sets the post property if it is not already set.
179 | *
180 | * @param WP_Post $post The post object to set.
181 | *
182 | * @return void
183 | */
184 | public function set_post( WP_Post $post ): void {
185 | if ( empty( $this->post ) ) {
186 | $this->post = $post;
187 | }
188 | }
189 |
190 | /**
191 | * Saves the meta box associated with a given post.
192 | *
193 | * @param int $post_id The ID of the post to save the meta box for.
194 | * @param WP_Post $post The post object being saved.
195 | *
196 | * @return void Result not needed;
197 | */
198 | public function save_meta_box( int $post_id, WP_Post $post ): void {
199 | if ( ! isset( $_POST[ $this->nonce ] ) ) {
200 | return;
201 | }
202 |
203 | $nonce = sanitize_text_field( wp_unslash( $_POST[ $this->nonce ] ) );
204 |
205 | if ( ! wp_verify_nonce( $nonce, $this->id ) ) {
206 | return;
207 | }
208 |
209 | if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
210 | return;
211 | }
212 |
213 | $post_id = $_POST['ID'] ?? $_POST['post_ID'] ?? $post_id;
214 | $post = get_post( $post_id );
215 |
216 | if ( isset( $_POST['post_type'] ) && ! in_array( $_POST['post_type'], $this->post_types, true ) ) {
217 | return;
218 | }
219 |
220 | if ( ! current_user_can( 'edit_post', $post_id ) ) {
221 | return;
222 | }
223 |
224 | $this->set_post( $post );
225 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
226 | }
227 |
228 | /**
229 | * Registers meta data for specified post types.
230 | *
231 | * This method normalizes items and registers each item as post meta for the
232 | * defined post types. Each item will have its type, description, default
233 | * value, and sanitize callback configured.
234 | *
235 | * @return void
236 | */
237 | public function register_meta(): void {
238 | $items = $this->normalize_items( $this->items );
239 |
240 | foreach ( $this->post_types as $post_type ) {
241 | foreach ( $items as $item ) {
242 | register_post_meta(
243 | $post_type,
244 | $item['id'],
245 | array(
246 | 'type' => $this->custom_fields->get_wp_type( $item ),
247 | 'description' => $item['label'],
248 | 'single' => true,
249 | 'default' => $this->custom_fields->get_default_value( $item ),
250 | 'sanitize_callback' => $this->custom_fields->sanitize_item_value( $item ),
251 | 'show_in_rest' => false,
252 | ),
253 | );
254 | }
255 | add_action( 'wpifycf_register_post_meta', $items, $post_type );
256 | }
257 | }
258 |
259 | /**
260 | * Renders the metabox for a given post with the necessary components.
261 | *
262 | * @param WP_Post $post The post object for which the metabox is being rendered.
263 | *
264 | * @return void
265 | */
266 | public function render( WP_Post $post ): void {
267 | $items = $this->normalize_items( $this->items );
268 |
269 | $this->set_post( $post );
270 | $this->enqueue();
271 | $this->print_app( 'metabox', $this->tabs );
272 |
273 | wp_nonce_field( $this->id, $this->nonce );
274 |
275 | foreach ( $items as $item ) {
276 | $this->print_field( $item );
277 | }
278 | }
279 |
280 | /**
281 | * Retrieves the value of a specified option for the current item.
282 | *
283 | * @param string $name The name of the option to retrieve.
284 | * @param mixed $default_value The default value to return if the option is not found.
285 | *
286 | * @return mixed The value of the specified option, or the default value if the option does not exist.
287 | */
288 | public function get_option_value( string $name, mixed $default_value ): mixed {
289 | return get_post_meta( $this->get_item_id(), $name, true ) ?? $default_value;
290 | }
291 |
292 | /**
293 | * Sets the option value for a given item.
294 | *
295 | * @param string $name The name of the option to set.
296 | * @param mixed $value The value to set for the option.
297 | *
298 | * @return bool True on success, false on failure.
299 | */
300 | public function set_option_value( string $name, mixed $value ): bool {
301 | return update_post_meta( $this->get_item_id(), $name, $value );
302 | }
303 |
304 | /**
305 | * Retrieves the ID of the current post.
306 | *
307 | * @return int The ID of the current post.
308 | */
309 | public function get_item_id(): int {
310 | return $this->post->ID;
311 | }
312 | }
313 |
--------------------------------------------------------------------------------
/src/Integrations/WooCommerceSettings.php:
--------------------------------------------------------------------------------
1 | tab = $args['tab'] ?? array();
91 | $this->section = $args['section'] ?? array();
92 | $this->items = $args['items'] ?? array();
93 | $this->tabs = $args['tabs'] ?? array();
94 | $this->option_name = $args['option_name'] ?? '';
95 |
96 | if ( isset( $args['display'] ) && is_callable( $args['display'] ) ) {
97 | $this->display = $args['display'];
98 | } else {
99 | $this->display = function () use ( $args ) {
100 | return $args['display'] ?? true;
101 | };
102 | }
103 |
104 | $this->id = $args['id'] ?? join(
105 | '_',
106 | array(
107 | 'wc_settings_',
108 | $this->tabs['id'] ?? '',
109 | $this->section['id'] ?? '',
110 | wp_generate_uuid4(),
111 | ),
112 | );
113 |
114 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
115 | add_filter( 'woocommerce_settings_tabs_array', array( $this, 'woocommerce_settings_tabs_array' ), 30 );
116 | add_filter( 'woocommerce_get_sections_' . $this->tab['id'], array( $this, 'woocommerce_get_sections' ) );
117 | add_action( 'woocommerce_settings_' . $this->tab['id'], array( $this, 'render' ), 11 );
118 | add_action( 'woocommerce_settings_save_' . $this->tab['id'], array( $this, 'save' ) );
119 |
120 | // Nonce verification is not needed here, we are just setting a current tab, section and displaying message.
121 | // phpcs:disable WordPress.Security.NonceVerification.Recommended
122 | $tab = sanitize_text_field( wp_unslash( $_REQUEST['tab'] ?? '' ) );
123 | $section = sanitize_text_field( wp_unslash( $_REQUEST['section'] ?? '' ) );
124 | $settings_updated = sanitize_text_field( wp_unslash( $_REQUEST['settings-updated'] ?? '' ) );
125 | // phpcs:enable
126 |
127 | if ( $tab === $this->tab['id'] && $section === $this->section['id'] && '1' === $settings_updated ) {
128 | WC_Admin_Settings::add_message( __( 'Your settings have been saved.', 'wpify-custom-fields' ) );
129 | }
130 | }
131 | }
132 |
133 | /**
134 | * Filters the WooCommerce settings tabs array to add a custom tab if necessary.
135 | *
136 | * @param array $tabs The existing array of WooCommerce settings tabs.
137 | *
138 | * @return array The modified array of WooCommerce settings tabs.
139 | */
140 | public function woocommerce_settings_tabs_array( array $tabs ): array {
141 | $display_callback = $this->display;
142 |
143 | if ( ! $display_callback() ) {
144 | return $tabs;
145 | }
146 |
147 | if ( empty( $tabs[ $this->tab['id'] ] ) ) {
148 | $tabs[ $this->tab['id'] ] = $this->tab['label'];
149 | $this->is_new_tab = true;
150 | }
151 |
152 | return $tabs;
153 | }
154 |
155 | /**
156 | * Adds or updates WooCommerce sections based on the instance configuration.
157 | *
158 | * @param array $sections An array of existing sections.
159 | *
160 | * @return array Modified array of sections.
161 | */
162 | public function woocommerce_get_sections( array $sections ): array {
163 | $display_callback = $this->display;
164 |
165 | if ( ! $display_callback() ) {
166 | return $sections;
167 | }
168 |
169 | if ( ! empty( $this->section ) ) {
170 | $sections[ $this->section['id'] ] = $this->section['label'];
171 | }
172 |
173 | if ( isset( $sections[''] ) ) {
174 | $empty_section = $sections[''];
175 | $reordered_sections = array( '' => $empty_section );
176 | unset( $sections[''] );
177 |
178 | foreach ( $sections as $id => $label ) {
179 | $reordered_sections[ $id ] = $label;
180 | }
181 |
182 | $sections = $reordered_sections;
183 | }
184 |
185 | return $sections;
186 | }
187 |
188 | /**
189 | * Render the settings page or section.
190 | *
191 | * @return void
192 | */
193 | public function render(): void {
194 | global $current_section;
195 |
196 | $display_callback = $this->display;
197 |
198 | if ( ! $display_callback() ) {
199 | return;
200 | }
201 |
202 | if ( $this->is_new_tab ) {
203 | // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
204 | $sections = apply_filters( 'woocommerce_get_sections_' . $this->tab['id'], array() );
205 |
206 | if ( is_array( $sections ) && count( $sections ) > 1 ) {
207 | $array_keys = array_keys( $sections );
208 | ?>
209 |
210 | $label ) {
212 | $url = add_query_arg(
213 | array(
214 | 'page' => 'wc-settings',
215 | 'tab' => rawurlencode( $this->tab['id'] ),
216 | 'section' => sanitize_title( $id ),
217 | ),
218 | admin_url( 'admin.php' ),
219 | );
220 | ?>
221 | -
222 |
225 |
226 |
227 |
228 |
229 |
232 |
233 |
234 | section['id'] ) {
239 | return;
240 | }
241 |
242 | $this->enqueue();
243 | $this->print_app( 'woocommerce-options', $this->tabs );
244 |
245 | $items = $this->normalize_items( $this->items );
246 |
247 | foreach ( $items as $item ) {
248 | $this->print_field( $item );
249 | }
250 | }
251 |
252 | /**
253 | * Save the settings for the specified tab and section.
254 | *
255 | * @return void
256 | */
257 | public function save(): void {
258 | // Nonce verification is not needed here, nonce already verified in WooCommerce.
259 | // phpcs:disable WordPress.Security.NonceVerification.Recommended
260 | $tab = sanitize_text_field( wp_unslash( $_REQUEST['tab'] ?? '' ) );
261 | $section = sanitize_text_field( wp_unslash( $_REQUEST['section'] ?? '' ) );
262 | // phpcs:enable
263 |
264 | if ( $tab === $this->tab['id'] && $section === $this->section['id'] ) {
265 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
266 | wp_safe_redirect(
267 | add_query_arg(
268 | array(
269 | 'page' => 'wc-settings',
270 | 'tab' => $this->tab['id'],
271 | 'section' => $this->section['id'],
272 | 'settings-updated' => true,
273 | ),
274 | admin_url( 'admin.php' ),
275 | ),
276 | );
277 |
278 | exit;
279 | }
280 | }
281 |
282 | /**
283 | * Retrieves the value of the specified option.
284 | *
285 | * @param string $name The name of the option to retrieve.
286 | * @param mixed $default_value The default value to return if the option does not exist.
287 | *
288 | * @return mixed The value of the option or the default value if the option does not exist.
289 | */
290 | public function get_option_value( string $name, mixed $default_value ): mixed {
291 | return get_option( $name, $default_value );
292 | }
293 |
294 | /**
295 | * Sets the value of the specified option.
296 | *
297 | * @param string $name The name of the option to set.
298 | * @param mixed $value The value to set for the option.
299 | *
300 | * @return bool True on success, false on failure.
301 | */
302 | public function set_option_value( string $name, mixed $value ): bool {
303 | return update_option( $name, $value );
304 | }
305 | }
306 |
--------------------------------------------------------------------------------
/src/Integrations/OrderMetabox.php:
--------------------------------------------------------------------------------
1 | 0 ) {
145 | throw new MissingArgumentException(
146 | sprintf(
147 | /* translators: %1$s is a list of missing arguments, %2$s is the class name. */
148 | esc_html( __( 'Missing arguments %1$s in class %2$s.', 'wpify-custom-fields' ) ),
149 | esc_html( implode( ', ', $missing ) ),
150 | __CLASS__,
151 | ),
152 | );
153 | }
154 |
155 | if ( isset( $args['display'] ) && is_callable( $args['display'] ) ) {
156 | $this->display = $args['display'];
157 | } else {
158 | $this->display = function () use ( $args ) {
159 | return $args['display'];
160 | };
161 | }
162 |
163 | $this->title = $args['title'] ?? '';
164 | $this->context = $args['context'] ?? 'advanced';
165 | $this->hook_priority = 10;
166 | $this->priority = $args['priority'] ?? 'default';
167 | $this->option_name = $args['meta_key'] ?? '';
168 | $this->items = $args['items'] ?? array();
169 | $this->capability = $args['capability'] ?? 'manage_woocommerce';
170 | $this->callback = $args['callback'] ?? null;
171 | $this->tabs = $args['tabs'] ?? array();
172 | $this->id = sanitize_title(
173 | join(
174 | '-',
175 | array(
176 | 'order-meta',
177 | $this->hook_priority,
178 | $this->title,
179 | ),
180 | ),
181 | );
182 | $this->nonce = $this->id . '-nonce';
183 |
184 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
185 | add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
186 | add_action( 'woocommerce_update_order', array( $this, 'save' ) );
187 | }
188 | }
189 |
190 |
191 | /**
192 | * Adds a meta box to the specified post type.
193 | *
194 | * @param string $post_type The post type to which the meta box will be added.
195 | *
196 | * @return void
197 | */
198 | public function add_meta_box( string $post_type ): void {
199 | if ( ! $this->display || ! function_exists( 'wc_get_container' ) || ! function_exists( 'wc_get_page_screen_id' ) ) {
200 | return;
201 | }
202 |
203 | $screen = wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled()
204 | ? wc_get_page_screen_id( 'shop-order' )
205 | : 'shop_order';
206 |
207 | if ( $screen !== $post_type ) {
208 | return;
209 | }
210 |
211 | add_meta_box(
212 | $this->id,
213 | $this->title,
214 | array( $this, 'render' ),
215 | $screen,
216 | $this->context,
217 | $this->priority,
218 | );
219 | }
220 |
221 | /**
222 | * Renders the order meta and associated items.
223 | *
224 | * @param WP_Post|WC_Abstract_Order $post Post object.
225 | *
226 | * @return void
227 | */
228 | public function render( WP_Post|WC_Abstract_Order $post ): void {
229 | if ( ! current_user_can( $this->capability ) ) {
230 | return;
231 | }
232 |
233 | $id = is_a( $post, 'WC_Abstract_Order' ) ? $post->get_id() : $post->ID;
234 | $order = wc_get_order( $id );
235 | $this->order_id = $order->get_id();
236 | $this->enqueue();
237 |
238 | $items = $this->normalize_items( $this->items );
239 |
240 | if ( is_callable( $this->callback ) ) {
241 | call_user_func( $this->callback );
242 | }
243 |
244 | wp_nonce_field( $this->id, $this->nonce );
245 | $this->print_app( 'order-meta', $this->tabs );
246 |
247 | foreach ( $items as $item ) {
248 | ?>
249 |
250 | print_field( $item ); ?>
251 |
252 | order_id );
267 | }
268 |
269 | /**
270 | * Saves the order metadata and updates the order items.
271 | *
272 | * @param int $post_id The post ID of the order being saved.
273 | *
274 | * @return void Result not needed.
275 | */
276 | public function save( int $post_id ): void {
277 | remove_action( 'woocommerce_update_order', array( $this, 'save' ) );
278 |
279 | if ( ! isset( $_POST[ $this->nonce ] ) ) {
280 | return;
281 | }
282 |
283 | $nonce = sanitize_text_field( wp_unslash( $_POST[ $this->nonce ] ) );
284 |
285 | if ( ! wp_verify_nonce( $nonce, $this->id ) ) {
286 | return;
287 | }
288 |
289 | if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
290 | return;
291 | }
292 |
293 | $this->order_id = $post_id;
294 |
295 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
296 | }
297 |
298 | /**
299 | * Retrieves the value of a specified option from the order metadata.
300 | *
301 | * @param string $name The name of the option whose value is to be retrieved.
302 | * @param mixed $default_value The default value to return if the option is not found.
303 | *
304 | * @return mixed The value of the specified option, or the default value if the option is not found.
305 | */
306 | public function get_option_value( string $name, mixed $default_value ): mixed {
307 | return $this->get_order()->get_meta( $name ) ?? $default_value;
308 | }
309 |
310 | /**
311 | * Sets the value of a specified option in the order metadata.
312 | *
313 | * @param string $name The name of the option to set.
314 | * @param mixed $value The value to assign to the specified option.
315 | *
316 | * @return WC_Order|int Returns true if the metadata was successfully saved, false otherwise.
317 | */
318 | public function set_option_value( string $name, mixed $value ): WC_Order|int {
319 | $order = $this->get_order();
320 | $order->update_meta_data( $name, $value );
321 |
322 | return $order->save();
323 | }
324 |
325 | /**
326 | * Retrieves the item ID associated with the order.
327 | *
328 | * @return int The item ID.
329 | */
330 | public function get_item_id(): int {
331 | return $this->order_id;
332 | }
333 | }
334 |
--------------------------------------------------------------------------------
/src/Integrations/SubscriptionMetabox.php:
--------------------------------------------------------------------------------
1 | 0 ) {
157 | throw new MissingArgumentException(
158 | sprintf(
159 | /* translators: %1$s is a list of missing arguments, %2$s is the class name. */
160 | esc_html( __( 'Missing arguments %1$s in class %2$s.', 'wpify-custom-fields' ) ),
161 | esc_html( implode( ', ', $missing ) ),
162 | __CLASS__,
163 | ),
164 | );
165 | }
166 |
167 | if ( isset( $args['display'] ) && is_callable( $args['display'] ) ) {
168 | $this->display = $args['display'];
169 | } else {
170 | $this->display = function () use ( $args ) {
171 | return $args['display'];
172 | };
173 | }
174 |
175 | $this->title = $args['title'] ?? '';
176 | $this->context = $args['context'] ?? 'advanced';
177 | $this->hook_priority = 10;
178 | $this->priority = $args['priority'] ?? 'default';
179 | $this->option_name = $args['meta_key'] ?? '';
180 | $this->items = $args['items'] ?? array();
181 | $this->capability = $args['capability'] ?? 'manage_woocommerce';
182 | $this->callback = $args['callback'] ?? null;
183 | $this->tabs = $args['tabs'] ?? array();
184 | $this->id = sanitize_title(
185 | join(
186 | '-',
187 | array(
188 | 'order-meta',
189 | $this->hook_priority,
190 | $this->title,
191 | ),
192 | ),
193 | );
194 | $this->nonce = $this->id . '-nonce';
195 |
196 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
197 | add_action( 'add_meta_boxes', array( $this, 'add_meta_box' ) );
198 | add_action( 'woocommerce_update_order', array( $this, 'save' ) );
199 | }
200 | }
201 |
202 | /**
203 | * Adds a meta box to the specified post type screen.
204 | *
205 | * @param string $post_type The post type to which the meta box is added.
206 | *
207 | * @return void
208 | */
209 | public function add_meta_box( string $post_type ): void {
210 | if ( ! $this->display || ! function_exists( 'wc_get_container' ) || ! function_exists( 'wc_get_page_screen_id' ) ) {
211 | return;
212 | }
213 |
214 | $screen = wc_get_container()->get( CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled()
215 | ? wc_get_page_screen_id( 'shop-subscription' )
216 | : 'shop_subscription';
217 |
218 | if ( $screen !== $post_type ) {
219 | return;
220 | }
221 |
222 | add_meta_box(
223 | $this->id,
224 | $this->title,
225 | array( $this, 'render' ),
226 | $screen,
227 | $this->context,
228 | $this->priority,
229 | );
230 | }
231 |
232 | /**
233 | * Renders the order meta details.
234 | *
235 | * @param WC_Order $order The order object containing order details.
236 | *
237 | * @return void
238 | */
239 | public function render( WC_Order $order ): void {
240 | if ( ! current_user_can( $this->capability ) ) {
241 | return;
242 | }
243 |
244 | $this->order_id = $order->get_id();
245 | $this->enqueue();
246 |
247 | $items = $this->normalize_items( $this->items );
248 |
249 | if ( is_callable( $this->callback ) ) {
250 | call_user_func( $this->callback );
251 | }
252 |
253 | wp_nonce_field( $this->id, $this->nonce );
254 | $this->print_app( 'order-meta', $this->tabs );
255 |
256 | foreach ( $items as $item ) {
257 | ?>
258 |
259 | print_field( $item ); ?>
260 |
261 | get_item_id() );
277 | }
278 |
279 | /**
280 | * Saves the order details.
281 | *
282 | * @param int $post_id The ID of the post (order) being saved.
283 | *
284 | * @return void
285 | */
286 | public function save( int $post_id ): void {
287 | remove_action( 'woocommerce_update_order', array( $this, 'save' ) );
288 |
289 | if ( ! isset( $_POST[ $this->nonce ] ) ) {
290 | return;
291 | }
292 |
293 | $nonce = sanitize_text_field( wp_unslash( $_POST[ $this->nonce ] ) );
294 |
295 | if ( ! wp_verify_nonce( $nonce, $this->id ) ) {
296 | return;
297 | }
298 |
299 | if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
300 | return;
301 | }
302 |
303 | $this->order_id = $post_id;
304 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
305 | }
306 |
307 | /**
308 | * Retrieves the value of a specified option.
309 | *
310 | * @param string $name The name of the option to retrieve.
311 | * @param mixed $default_value The default value to return if the option is not found.
312 | *
313 | * @return mixed The value of the specified option, or the default value if the option is not found.
314 | */
315 | public function get_option_value( string $name, mixed $default_value ): mixed {
316 | return $this->get_order()->get_meta( $name ) ?? $default_value;
317 | }
318 |
319 | /**
320 | * Sets the value of a specified option.
321 | *
322 | * @param string $name The name of the option to set.
323 | * @param mixed $value The value to set for the specified option.
324 | *
325 | * @return bool True on success, false on failure.
326 | */
327 | public function set_option_value( string $name, mixed $value ): bool {
328 | $order = $this->get_order();
329 | $order->update_meta_data( $name, $value );
330 |
331 | return $order->save();
332 | }
333 |
334 | /**
335 | * Retrieves the item ID.
336 | *
337 | * @return int The ID of the item.
338 | */
339 | public function get_item_id(): int {
340 | return $this->order_id;
341 | }
342 | }
343 |
--------------------------------------------------------------------------------
/src/Helpers.php:
--------------------------------------------------------------------------------
1 | (.*?)<\/title>/is', $body, $matches );
37 |
38 | if ( isset( $matches[1] ) ) {
39 | return trim( $matches[1] );
40 | }
41 |
42 | return '';
43 | }
44 |
45 | /**
46 | * Retrieves posts based on specified arguments, ensuring some posts are included and others excluded.
47 | *
48 | * @param array $args Arguments for get_posts.
49 | *
50 | * @return array An array of post data arrays.
51 | */
52 | public function get_posts( array $args = array() ): array {
53 | unset( $args['_locale'] );
54 |
55 | if ( empty( $args['numberposts'] ) ) {
56 | $args['numberposts'] = 50;
57 | }
58 |
59 | if ( empty( $args['post_status'] ) ) {
60 | $args['post_status'] = 'any';
61 | }
62 |
63 | $posts = array();
64 | $ensured_posts = array();
65 | $added_posts = array();
66 |
67 | if ( ! empty( $args['exclude'] ) && is_array( $args['exclude'] ) ) {
68 | $exclude = array_values( array_filter( array_map( 'intval', $args['exclude'] ) ) );
69 | } else {
70 | $exclude = array();
71 | }
72 |
73 | unset( $args['exclude'] );
74 |
75 | if ( ! empty( $args['ensure'] ) && is_array( $args['ensure'] ) ) {
76 | $ensured_posts = get_posts(
77 | array(
78 | ...$args,
79 | 'include' => array_values( array_filter( array_map( 'intval', $args['ensure'] ) ) ),
80 | ),
81 | );
82 | }
83 |
84 | unset( $args['ensure'] );
85 |
86 | if ( ! empty( $args['s'] ) ) {
87 | $args['orderby'] = 'relevance';
88 | $args['order'] = 'DESC';
89 | }
90 |
91 | $raw_posts = get_posts(
92 | array(
93 | ...$args,
94 | 'limit' => $args['numberposts'] + count( $ensured_posts ) + count( $exclude ),
95 | ),
96 | );
97 |
98 | foreach ( $ensured_posts as $post ) {
99 | $posts[] = $post;
100 | $added_posts[] = $post->ID;
101 | }
102 |
103 | foreach ( $raw_posts as $post ) {
104 | if ( in_array( $post->ID, $exclude, true ) || in_array( $post->ID, $added_posts, true ) || count( $posts ) >= $args['numberposts'] ) {
105 | continue;
106 | }
107 |
108 | $posts[] = $post;
109 | }
110 |
111 | $placeholder = plugin_dir_url( __DIR__ ) . 'assets/images/placeholder-image.svg';
112 |
113 | return array_map(
114 | fn( WP_Post $post ) => array(
115 | 'id' => $post->ID,
116 | 'title' => $post->post_title,
117 | 'post_type' => $post->post_type,
118 | 'post_status' => $post->post_status,
119 | 'post_status_label' => get_post_status_object( $post->post_status )->label,
120 | 'permalink' => get_permalink( $post ),
121 | 'thumbnail' => get_the_post_thumbnail_url( $post ) ?? $placeholder,
122 | 'excerpt' => get_the_excerpt( $post ),
123 | ),
124 | $posts,
125 | );
126 | }
127 |
128 | /**
129 | * Retrieves terms based on the given arguments and arranges them into a hierarchical tree structure.
130 | *
131 | * @param array $args Arguments to be passed to the get_terms function.
132 | *
133 | * @return array An array representing the hierarchical tree of terms.
134 | */
135 | public function get_terms( array $args ): array {
136 | $terms = get_terms(
137 | array(
138 | 'hide_empty' => false,
139 | ...$args,
140 | ),
141 | );
142 |
143 | if ( is_wp_error( $terms ) || empty( $terms ) ) {
144 | return array();
145 | }
146 |
147 | $terms_by_id = array();
148 |
149 | foreach ( $terms as $term ) {
150 | $terms_by_id[ $term->term_id ] = array(
151 | 'id' => $term->term_id,
152 | 'name' => $term->name,
153 | 'parent' => $term->parent,
154 | );
155 | }
156 |
157 | $tree = array();
158 |
159 | foreach ( $terms_by_id as &$term ) {
160 | if ( 0 !== $term['parent'] && isset( $terms_by_id[ $term['parent'] ] ) ) {
161 | $parent =& $terms_by_id[ $term['parent'] ];
162 |
163 | if ( ! isset( $parent['children'] ) ) {
164 | $parent['children'] = array();
165 | }
166 |
167 | $parent['children'][] =& $term;
168 | } else {
169 | $tree[] =& $term;
170 | }
171 | }
172 |
173 | unset( $term );
174 |
175 | return $tree;
176 | }
177 |
178 | /**
179 | * Encodes data as JSON, ensuring that all characters are properly escaped.
180 | *
181 | * This function is a wrapper around wp_json_encode with additional flags. The following flags are used:
182 | * - JSON_HEX_TAG
183 | * - JSON_HEX_APOS
184 | * - JSON_HEX_QUOT
185 | * - JSON_HEX_AMP
186 | * - JSON_UNESCAPED_UNICODE
187 | *
188 | * @param mixed $data The data to encode as JSON.
189 | *
190 | * @return string
191 | */
192 | public function json_encode( $data ): string {
193 | $json = wp_json_encode( $data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE );
194 |
195 | return $json ? $json : '';
196 | }
197 |
198 | /**
199 | * Gets the temporary directory for direct file uploads.
200 | *
201 | * @return string The absolute path to the temp directory.
202 | */
203 | public function get_direct_file_temp_dir(): string {
204 | $upload_dir = wp_upload_dir();
205 | return trailingslashit( $upload_dir['basedir'] ) . 'wpifycf-temp';
206 | }
207 |
208 | /**
209 | * Sanitizes and resolves directory path (relative or absolute).
210 | *
211 | * @param string $directory The directory path from field definition.
212 | *
213 | * @return string The absolute, sanitized directory path.
214 | */
215 | public function sanitize_directory_path( string $directory ): string {
216 | // Remove any traversal attempts.
217 | $directory = str_replace( array( '../', '..' ), '', $directory );
218 |
219 | // If path is absolute, use it as-is.
220 | if ( path_is_absolute( $directory ) ) {
221 | return trailingslashit( $directory );
222 | }
223 |
224 | // Otherwise, treat as relative to ABSPATH.
225 | return trailingslashit( ABSPATH ) . trailingslashit( $directory );
226 | }
227 |
228 | /**
229 | * Generates a unique filename by appending -n if file exists and replace is false.
230 | *
231 | * @param string $directory The target directory.
232 | * @param string $filename The desired filename.
233 | * @param bool $replace Whether to replace existing files.
234 | *
235 | * @return string The final filename (unique if replace is false).
236 | */
237 | public function generate_unique_filename( string $directory, string $filename, bool $replace = false ): string {
238 | if ( $replace ) {
239 | return $filename;
240 | }
241 |
242 | return wp_unique_filename( $directory, $filename );
243 | }
244 |
245 | /**
246 | * Moves a file from temp directory to target directory.
247 | *
248 | * @param string $temp_path The temporary file path.
249 | * @param string $target_directory The target directory.
250 | * @param string $filename The desired filename.
251 | * @param bool $replace_existing Whether to replace existing files.
252 | *
253 | * @return string|false The absolute path to the moved file, or false on failure.
254 | */
255 | public function move_temp_to_directory( string $temp_path, string $target_directory, string $filename, bool $replace_existing = false ) {
256 | // Ensure target directory exists.
257 | if ( ! file_exists( $target_directory ) ) {
258 | if ( ! wp_mkdir_p( $target_directory ) ) {
259 | return false;
260 | }
261 | }
262 |
263 | // Check if temp file exists.
264 | if ( ! file_exists( $temp_path ) ) {
265 | return false;
266 | }
267 |
268 | // Generate final filename.
269 | $final_filename = $this->generate_unique_filename( $target_directory, $filename, $replace_existing );
270 | $target_path = trailingslashit( $target_directory ) . $final_filename;
271 |
272 | // If replacing and file exists, delete it first.
273 | if ( $replace_existing && file_exists( $target_path ) ) {
274 | wp_delete_file( $target_path );
275 | }
276 |
277 | // Move file from temp to target.
278 | if ( rename( $temp_path, $target_path ) ) { // phpcs:ignore
279 | return $target_path;
280 | }
281 |
282 | return false;
283 | }
284 |
285 | /**
286 | * Deletes a direct file from the filesystem.
287 | *
288 | * @param string $file_path The absolute path to the file.
289 | *
290 | * @return bool True on success, false on failure.
291 | */
292 | public function delete_direct_file( string $file_path ): bool {
293 | if ( ! file_exists( $file_path ) ) {
294 | return false;
295 | }
296 |
297 | return wp_delete_file( $file_path );
298 | }
299 |
300 | /**
301 | * Extracts filename from a file path.
302 | *
303 | * @param string $file_path The file path.
304 | *
305 | * @return string The filename.
306 | */
307 | public function get_filename_from_path( string $file_path ): string {
308 | return basename( $file_path );
309 | }
310 |
311 | /**
312 | * Cleans up old temporary files from the temp directory.
313 | *
314 | * Deletes files older than the age threshold (default 24 hours).
315 | * The age threshold can be filtered using 'wpifycf_temp_file_age_threshold'.
316 | *
317 | * @return int Number of files deleted.
318 | */
319 | public function cleanup_temp_files(): int {
320 | $temp_dir = $this->get_direct_file_temp_dir();
321 |
322 | // Skip if directory doesn't exist.
323 | if ( ! is_dir( $temp_dir ) ) {
324 | return 0;
325 | }
326 |
327 | // Get age threshold in seconds (default 24 hours).
328 | $age_threshold = apply_filters( 'wpifycf_temp_file_age_threshold', DAY_IN_SECONDS );
329 | $cutoff_time = time() - $age_threshold;
330 | $deleted_count = 0;
331 |
332 | // Get all files in temp directory.
333 | $files = glob( trailingslashit( $temp_dir ) . '*' );
334 |
335 | if ( ! is_array( $files ) ) {
336 | return 0;
337 | }
338 |
339 | foreach ( $files as $file ) {
340 | // Skip if not a file.
341 | if ( ! is_file( $file ) ) {
342 | continue;
343 | }
344 |
345 | // Get file modification time.
346 | $file_mtime = filemtime( $file );
347 |
348 | if ( false === $file_mtime ) {
349 | continue;
350 | }
351 |
352 | // Delete if older than threshold.
353 | if ( $file_mtime < $cutoff_time ) {
354 | if ( wp_delete_file( $file ) ) {
355 | ++$deleted_count;
356 | } else {
357 | // Log error but continue processing.
358 | error_log( sprintf( '[wpify-custom-fields] Failed to delete temp file: %s', $file ) ); // phpcs:ignore
359 | }
360 | }
361 | }
362 |
363 | return $deleted_count;
364 | }
365 | }
366 |
--------------------------------------------------------------------------------
/src/Integrations/ProductVariationOptions.php:
--------------------------------------------------------------------------------
1 | 0 ) {
167 | throw new MissingArgumentException(
168 | sprintf(
169 | /* translators: %1$s is a list of missing arguments, %2$s is the class name. */
170 | esc_html( __( 'Missing arguments %1$s in class %2$s.', 'wpify-custom-fields' ) ),
171 | esc_html( implode( ', ', $missing ) ),
172 | __CLASS__,
173 | ),
174 | );
175 | }
176 |
177 | if ( isset( $args['display'] ) && is_callable( $args['display'] ) ) {
178 | $this->display = $args['display'];
179 | } else {
180 | $this->display = function () use ( $args ) {
181 | return $args['display'] ?? true;
182 | };
183 | }
184 |
185 | $this->capability = $args['capability'] ?? 'manage_options';
186 | $this->callback = $args['callback'] ?? null;
187 | $this->after = $args['after'] ?? '';
188 | $this->hook_priority = $args['hook_priority'] ?? 10;
189 | $this->help_tabs = $args['help_tabs'] ?? array();
190 | $this->help_sidebar = $args['help_sidebar'] ?? '';
191 | $this->option_name = $args['meta_key'] ?? '';
192 | $this->items = $args['items'] ?? array();
193 | $this->tabs = $args['tabs'] ?? array();
194 |
195 | $tab = $args['tab'] ?? array();
196 |
197 | if ( empty( $tab['label'] ) ) {
198 | throw new MissingArgumentException(
199 | sprintf(
200 | /* translators: %1$s is the class name. */
201 | esc_html( __( 'Missing argument $tab["label"] in class %2$s.', 'wpify-custom-fields' ) ),
202 | __CLASS__,
203 | ),
204 | );
205 | }
206 |
207 | if ( empty( $tab['id'] ) ) {
208 | $tab['id'] = sanitize_title( $tab['label'] );
209 | }
210 |
211 | if ( empty( $tab['target'] ) ) {
212 | $tab['target'] = $tab['id'];
213 | }
214 |
215 | if ( empty( $tab['priority'] ) ) {
216 | $tab['priority'] = 100;
217 | }
218 |
219 | if ( empty( $tab['class'] ) ) {
220 | $tab['class'] = array();
221 | }
222 |
223 | $this->tab = $tab;
224 | $this->is_new_tab = false;
225 | $this->id = sanitize_title(
226 | join(
227 | '-',
228 | array(
229 | 'variation-options',
230 | $this->tab['id'],
231 | sanitize_title( $this->tab['label'] ),
232 | $this->hook_priority,
233 | ),
234 | ),
235 | );
236 |
237 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
238 | if ( in_array( $this->after, array( 'pricing', 'inventory', 'dimensions', 'download' ), true ) ) {
239 | add_action( 'woocommerce_variation_options_' . $this->after, array( $this, 'render' ), 10, 3 );
240 | } else {
241 | add_action( 'woocommerce_product_after_variable_attributes', array( $this, 'render' ), 10, 3 );
242 | }
243 |
244 | add_action( 'woocommerce_save_product_variation', array( $this, 'save' ), 10, 2 );
245 | add_action( 'init', array( $this, 'register_meta' ), $this->hook_priority );
246 | add_action( 'admin_footer', array( $this, 'maybe_enqueue' ) );
247 | }
248 | }
249 |
250 | /**
251 | * Renders the variation options group for the product.
252 | *
253 | * @param int $loop The current loop iteration.
254 | * @param array $variation_data Data associated with the variation.
255 | * @param WP_Post $variation The WordPress post object for the variation.
256 | *
257 | * @return void
258 | */
259 | public function render( int $loop, array $variation_data, WP_Post $variation ): void {
260 | if ( ! current_user_can( $this->capability ) ) {
261 | return;
262 | }
263 |
264 | $this->variation_id = $variation->ID;
265 | $items = $this->normalize_items( $this->items );
266 |
267 | if ( is_callable( $this->callback ) ) {
268 | call_user_func( $this->callback );
269 | }
270 | ?>
271 |
272 | print_app( 'product-variation', $this->tabs, array( 'loop' => $loop ) );
274 |
275 | foreach ( $items as $item ) {
276 | $this->print_field( $item, array( 'loop' => $loop ), 'div', 'form-field' );
277 | }
278 | ?>
279 |
280 | get_item_id() );
290 | }
291 |
292 | /**
293 | * Saves data for a product variation.
294 | *
295 | * @param int $product_variation_id The ID of the product variation.
296 | * @param int $loop The current loop iteration.
297 | *
298 | * @return void
299 | */
300 | public function save( int $product_variation_id, int $loop ): void {
301 | $this->variation_id = $product_variation_id;
302 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ), $loop );
303 | }
304 |
305 | /**
306 | * Conditionally enqueues scripts or styles based on the current screen.
307 | *
308 | * If the current screen is a product-related screen, it calls the enqueue method.
309 | *
310 | * @return void
311 | */
312 | public function maybe_enqueue(): void {
313 | $current_screen = get_current_screen();
314 | if ( 'product' === $current_screen->id || 'product_page_product' === $current_screen->id ) {
315 | $this->enqueue();
316 | }
317 | }
318 |
319 | /**
320 | * Registers custom meta fields for product variations.
321 | *
322 | * Normalizes the items and registers each item as a post meta for product variations with specific type, description, and sanitization callbacks.
323 | *
324 | * @return void
325 | */
326 | public function register_meta(): void {
327 | $items = $this->normalize_items( $this->items );
328 |
329 | foreach ( $items as $item ) {
330 | register_post_meta(
331 | 'product_variation',
332 | $item['id'],
333 | array(
334 | 'type' => $this->custom_fields->get_wp_type( $item ),
335 | 'description' => $item['label'],
336 | 'single' => true,
337 | 'default' => $this->custom_fields->get_default_value( $item ),
338 | 'sanitize_callback' => $this->custom_fields->sanitize_item_value( $item ),
339 | 'show_in_rest' => false,
340 | ),
341 | );
342 | }
343 | }
344 |
345 | /**
346 | * Retrieves the value of a product option.
347 | *
348 | * @param string $name The name of the option to retrieve.
349 | * @param mixed $default_value The default value to return if the option is not set.
350 | *
351 | * @return mixed The value of the specified option or the default value.
352 | */
353 | public function get_option_value( string $name, mixed $default_value ): mixed {
354 | return $this->get_product()->get_meta( $name ) ?? $default_value;
355 | }
356 |
357 | /**
358 | * Sets the value of a product option and saves the product.
359 | *
360 | * @param string $name The name of the option to set.
361 | * @param mixed $value The value to set for the option.
362 | *
363 | * @return bool True if the product was saved successfully, false otherwise.
364 | */
365 | public function set_option_value( string $name, mixed $value ): bool {
366 | $product = $this->get_product();
367 | $product->update_meta_data( $name, $value );
368 |
369 | return $product->save();
370 | }
371 |
372 | /**
373 | * Retrieves the ID of the item.
374 | *
375 | * @return int The ID of the item.
376 | */
377 | public function get_item_id(): int {
378 | return $this->variation_id;
379 | }
380 | }
381 |
--------------------------------------------------------------------------------
/src/Integrations/WcMembershipPlanOptions.php:
--------------------------------------------------------------------------------
1 | 0 ) {
152 | throw new MissingArgumentException(
153 | sprintf(
154 | /* translators: %1$s is a list of missing arguments, %2$s is the class name. */
155 | esc_html( __( 'Missing arguments %1$s in class %2$s.', 'wpify-custom-fields' ) ),
156 | esc_html( implode( ', ', $missing ) ),
157 | __CLASS__,
158 | ),
159 | );
160 | }
161 |
162 | if ( isset( $args['display'] ) && is_callable( $args['display'] ) ) {
163 | $this->display = $args['display'];
164 | } else {
165 | $this->display = function () use ( $args ) {
166 | return $args['display'] ?? true;
167 | };
168 | }
169 |
170 | $this->capability = $args['capability'] ?? 'manage_options';
171 | $this->callback = $args['callback'] ?? null;
172 | $this->hook_priority = $args['hook_priority'] ?? 10;
173 | $this->help_tabs = $args['help_tabs'] ?? array();
174 | $this->help_sidebar = $args['help_sidebar'] ?? '';
175 | $this->items = $args['items'] ?? array();
176 | $this->option_name = $args['meta_key'] ?? '';
177 | $this->tabs = $args['tabs'] ?? array();
178 | $this->is_new_tab = false;
179 |
180 | $tab = $args['tab'] ?? array();
181 |
182 | if ( empty( $tab['label'] ) ) {
183 | throw new MissingArgumentException(
184 | sprintf(
185 | /* translators: %1$s is the class name. */
186 | esc_html( __( 'Missing argument $tab["label"] in class %1$s.', 'wpify-custom-fields' ) ),
187 | __CLASS__,
188 | ),
189 | );
190 | }
191 |
192 | if ( empty( $tab['id'] ) ) {
193 | $tab['id'] = sanitize_title( $tab['label'] );
194 | }
195 |
196 | if ( empty( $tab['target'] ) ) {
197 | $tab['target'] = $tab['id'];
198 | }
199 |
200 | if ( empty( $tab['priority'] ) ) {
201 | $tab['priority'] = 100;
202 | }
203 |
204 | if ( empty( $tab['class'] ) ) {
205 | $tab['class'] = array();
206 | }
207 |
208 | $this->tab = $tab;
209 | $this->id = sanitize_title(
210 | join(
211 | '-',
212 | array(
213 | 'wc_membership_plan_options',
214 | $this->tab['id'],
215 | sanitize_title( $this->tab['label'] ),
216 | $this->hook_priority,
217 | ),
218 | ),
219 | );
220 |
221 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
222 | add_filter( 'wc_membership_plan_data_tabs', array( $this, 'wc_membership_plan_data_tabs' ), 98 );
223 | add_action( 'wc_membership_plan_data_panels', array( $this, 'render_data_panels' ) );
224 | add_action(
225 | 'wc_membership_plan_options_' . $this->tab['target'] ?? $this->tab['id'],
226 | array(
227 | $this,
228 | 'render',
229 | )
230 | );
231 | add_action( 'wc_memberships_save_meta_box', array( $this, 'save' ) );
232 | add_action( 'init', array( $this, 'register_meta' ), $this->hook_priority );
233 | }
234 | }
235 |
236 | /**
237 | * Updates or adds membership plan data tabs.
238 | *
239 | * @param array $tabs The existing tabs.
240 | *
241 | * @return array The modified tabs.
242 | */
243 | public function wc_membership_plan_data_tabs( array $tabs ): array {
244 | if ( isset( $tabs[ $this->tab['id'] ] ) ) {
245 | if ( ! empty( $this->tab['label'] ) ) {
246 | $tabs[ $this->tab['id'] ]['label'] = $this->tab['label'];
247 | }
248 |
249 | if ( ! empty( $this->tab['priority'] ) ) {
250 | $tabs[ $this->tab['id'] ]['priority'] = $this->tab['priority'];
251 | }
252 | } else {
253 | $this->is_new_tab = true;
254 |
255 | $tabs[ $this->tab['id'] ] = array(
256 | 'label' => $this->tab['label'],
257 | 'priority' => $this->tab['priority'],
258 | 'target' => $this->tab['target'],
259 | 'class' => $this->tab['class'],
260 | );
261 | }
262 |
263 | return $tabs;
264 | }
265 |
266 |
267 | /**
268 | * Renders the data panels for the membership plan.
269 | *
270 | * This method outputs the HTML for the data panels and triggers the associated actions for the specified tab target.
271 | *
272 | * @return void
273 | */
274 | public function render_data_panels(): void {
275 | ?>
276 |
277 | tab['target'] );
280 | ?>
281 |
282 | capability ) ) {
292 | return;
293 | }
294 |
295 | global $post;
296 |
297 | $this->membership_plan_id = $post->ID;
298 | $this->enqueue();
299 | $items = $this->normalize_items( $this->items );
300 |
301 | if ( is_callable( $this->callback ) ) {
302 | call_user_func( $this->callback );
303 | }
304 | ?>
305 |
306 | print_app( 'product-options', $this->tabs );
308 |
309 | foreach ( $items as $item ) {
310 | ?>
311 |
312 | print_field( $item ); ?>
313 |
314 |
317 |
318 | get_item_id() );
328 | }
329 |
330 | /**
331 | * Saves the membership plan data.
332 | *
333 | * @param array $data Data from the form.
334 | *
335 | * @return void
336 | */
337 | public function save( array $data ): void {
338 | $this->membership_plan_id = $data['post_ID'];
339 |
340 | if ( ! $this->membership_plan_id ) {
341 | return;
342 | }
343 |
344 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
345 | }
346 |
347 | /**
348 | * Registers meta fields for a custom post type 'product'.
349 | *
350 | * This method normalizes and registers each item as a post meta
351 | * for the 'product' post type, setting various properties such as type,
352 | * description, default value, and sanitize callback.
353 | *
354 | * @return void
355 | */
356 | public function register_meta(): void {
357 | $items = $this->normalize_items( $this->items );
358 |
359 | foreach ( $items as $item ) {
360 | register_post_meta(
361 | 'product',
362 | $item['id'],
363 | array(
364 | 'type' => $this->custom_fields->get_wp_type( $item ),
365 | 'description' => $item['label'],
366 | 'single' => true,
367 | 'default' => $this->custom_fields->get_default_value( $item ),
368 | 'sanitize_callback' => $this->custom_fields->sanitize_item_value( $item ),
369 | 'show_in_rest' => false,
370 | ),
371 | );
372 | }
373 | }
374 |
375 | /**
376 | * Retrieves an option value for a given post meta field.
377 | *
378 | * @param string $name The name of the meta field.
379 | * @param mixed $default_value The default value to return if the meta field does not exist.
380 | *
381 | * @return mixed The value of the meta field or the default value if the field does not exist.
382 | */
383 | public function get_option_value( string $name, mixed $default_value ): mixed {
384 | return get_post_meta( $this->get_item_id(), $name, true ) ?? $default_value;
385 | }
386 |
387 | /**
388 | * Sets the value of a specified option.
389 | *
390 | * @param string $name The name of the option to be set.
391 | * @param mixed $value The value to set for the option.
392 | *
393 | * @return bool|int True on success, false on failure.
394 | */
395 | public function set_option_value( string $name, mixed $value ): bool|int {
396 | return update_post_meta( $this->get_item_id(), $name, $value );
397 | }
398 |
399 | /**
400 | * Retrieves the membership plan item ID.
401 | *
402 | * @return int The membership plan item ID.
403 | */
404 | public function get_item_id(): int {
405 | return $this->membership_plan_id;
406 | }
407 | }
408 |
--------------------------------------------------------------------------------
/src/Integrations/CouponOptions.php:
--------------------------------------------------------------------------------
1 | 0 ) {
153 | throw new MissingArgumentException(
154 | sprintf(
155 | /* translators: %1$s is a list of missing arguments, %2$s is the class name. */
156 | esc_html( __( 'Missing arguments %1$s in class %2$s.', 'wpify-custom-fields' ) ),
157 | esc_html( implode( ', ', $missing ) ),
158 | __CLASS__,
159 | ),
160 | );
161 | }
162 |
163 | if ( isset( $args['display'] ) && is_callable( $args['display'] ) ) {
164 | $this->display = $args['display'];
165 | } else {
166 | $this->display = function () use ( $args ) {
167 | return $args['display'] ?? true;
168 | };
169 | }
170 |
171 | $this->capability = $args['capability'] ?? 'manage_options';
172 | $this->callback = $args['callback'] ?? null;
173 | $this->hook_priority = $args['hook_priority'] ?? 10;
174 | $this->help_tabs = $args['help_tabs'] ?? array();
175 | $this->help_sidebar = $args['help_sidebar'] ?? '';
176 | $this->items = $args['items'] ?? array();
177 | $this->option_name = $args['meta_key'] ?? '';
178 | $this->tabs = $args['tabs'] ?? array();
179 | $this->is_new_tab = false;
180 |
181 | $tab = $args['tab'] ?? array();
182 |
183 | $tab['label'] = $tab['label'] ?? '';
184 |
185 | if ( empty( $tab['id'] ) ) {
186 | $tab['id'] = sanitize_title( $tab['label'] );
187 | }
188 |
189 | if ( empty( $tab['target'] ) ) {
190 | $tab['target'] = $tab['id'];
191 | }
192 |
193 | if ( empty( $tab['class'] ) ) {
194 | $tab['class'] = array();
195 | }
196 |
197 | $this->tab = $tab;
198 | $this->id = sanitize_title(
199 | join(
200 | '-',
201 | array(
202 | 'product-options',
203 | $this->tab['id'],
204 | sanitize_title( $this->tab['label'] ),
205 | $this->hook_priority,
206 | ),
207 | ),
208 | );
209 |
210 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
211 | add_filter( 'woocommerce_coupon_data_tabs', array( $this, 'woocommerce_coupon_data_tabs' ), 98 );
212 | add_action( 'woocommerce_coupon_data_panels', array( $this, 'render_data_panels' ) );
213 | add_action( 'woocommerce_coupon_options_' . $this->tab['target'] ?? $this->tab['id'], array( $this, 'render' ) );
214 | add_action( 'woocommerce_coupon_options_save', array( $this, 'save' ) );
215 | add_action( 'init', array( $this, 'register_meta' ), $this->hook_priority );
216 | }
217 | }
218 |
219 | /**
220 | * Modifies the WooCommerce product data tabs by updating existing tabs or adding new custom tabs.
221 | *
222 | * @param array $tabs An associative array of existing product data tabs.
223 | *
224 | * @return array The modified array of product data tabs.
225 | */
226 | public function woocommerce_coupon_data_tabs( array $tabs ): array {
227 | if ( isset( $tabs[ $this->tab['id'] ] ) ) {
228 | if ( ! empty( $this->tab['label'] ) ) {
229 | $tabs[ $this->tab['id'] ]['label'] = $this->tab['label'];
230 | }
231 |
232 | if ( ! empty( $this->tab['priority'] ) ) {
233 | $tabs[ $this->tab['id'] ]['priority'] = $this->tab['priority'];
234 | }
235 | } else {
236 | $this->is_new_tab = true;
237 |
238 | $tabs[ $this->tab['id'] ] = array(
239 | 'label' => $this->tab['label'],
240 | 'priority' => $this->tab['priority'],
241 | 'target' => $this->tab['target'],
242 | 'class' => $this->tab['class'],
243 | );
244 | }
245 |
246 | return $tabs;
247 | }
248 |
249 |
250 | /**
251 | * Renders the data panels for WooCommerce product options.
252 | *
253 | * This method outputs a div element with a specific ID and class, and triggers a custom WooCommerce action
254 | * based on the target attribute of the tab.
255 | *
256 | * @return void No return value.
257 | */
258 | public function render_data_panels(): void {
259 | if ( ! $this->is_new_tab ) {
260 | return;
261 | }
262 | ?>
263 |
264 | tab['target'] );
267 | ?>
268 |
269 | capability ) ) {
283 | return;
284 | }
285 |
286 | global $post;
287 |
288 | $this->coupon_id = $post->ID;
289 | $this->enqueue();
290 | $items = $this->normalize_items( $this->items );
291 |
292 | if ( is_callable( $this->callback ) ) {
293 | call_user_func( $this->callback );
294 | }
295 | ?>
296 |
297 | print_app( 'coupon-options', $this->tabs );
299 |
300 | foreach ( $items as $item ) {
301 | ?>
302 |
303 | print_field( $item ); ?>
304 |
305 |
308 |
309 | get_item_id() );
321 | }
322 |
323 | /**
324 | * Saves the product fields data for a given post.
325 | *
326 | * This function processes and saves custom fields associated with a product
327 | * for the specified post ID. The items are normalized and sanitized before
328 | * being stored.
329 | *
330 | * @param int $post_id The ID of the post being saved.
331 | *
332 | * @return void
333 | */
334 | public function save( int $post_id ): void {
335 | $this->coupon_id = $post_id;
336 |
337 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
338 | }
339 |
340 | /**
341 | * Registers custom metadata for the products.
342 | *
343 | * This method normalizes the defined items and registers each as post meta for 'product'.
344 | * It sets the meta properties such as type, description, default value, and sanitize callback.
345 | *
346 | * @return void
347 | */
348 | public function register_meta(): void {
349 | $items = $this->normalize_items( $this->items );
350 |
351 | foreach ( $items as $item ) {
352 | register_post_meta(
353 | 'coupon',
354 | $item['id'],
355 | array(
356 | 'type' => $this->custom_fields->get_wp_type( $item ),
357 | 'description' => $item['label'],
358 | 'single' => true,
359 | 'default' => $this->custom_fields->get_default_value( $item ),
360 | 'sanitize_callback' => $this->custom_fields->sanitize_item_value( $item ),
361 | 'show_in_rest' => false,
362 | ),
363 | );
364 | }
365 | }
366 |
367 | /**
368 | * Retrieves an option value from the product meta data.
369 | *
370 | * @param string $name The name of the meta key to retrieve.
371 | * @param mixed $default_value The default value to return if the meta key does not exist.
372 | *
373 | * @return mixed The value of the meta key if it exists, otherwise the default value.
374 | */
375 | public function get_option_value( string $name, mixed $default_value ): mixed {
376 | return $this->get_coupon()->get_meta( $name ) ?? $default_value;
377 | }
378 |
379 | /**
380 | * Sets an option value in the product meta data.
381 | *
382 | * @param string $name The name of the meta key to set.
383 | * @param mixed $value The value to be set for the meta key.
384 | *
385 | * @return bool True on success, false on failure.
386 | */
387 | public function set_option_value( string $name, mixed $value ): bool {
388 | $product = $this->get_coupon();
389 | $product->update_meta_data( $name, $value );
390 |
391 | return $product->save();
392 | }
393 |
394 | /**
395 | * Retrieves the item ID of the product.
396 | *
397 | * @return int The product's item ID.
398 | */
399 | public function get_item_id(): int {
400 | return $this->coupon_id;
401 | }
402 | }
403 |
--------------------------------------------------------------------------------
/src/Integrations/ProductOptions.php:
--------------------------------------------------------------------------------
1 | 0 ) {
153 | throw new MissingArgumentException(
154 | sprintf(
155 | /* translators: %1$s is a list of missing arguments, %2$s is the class name. */
156 | esc_html( __( 'Missing arguments %1$s in class %2$s.', 'wpify-custom-fields' ) ),
157 | esc_html( implode( ', ', $missing ) ),
158 | __CLASS__,
159 | ),
160 | );
161 | }
162 |
163 | if ( isset( $args['display'] ) && is_callable( $args['display'] ) ) {
164 | $this->display = $args['display'];
165 | } else {
166 | $this->display = function () use ( $args ) {
167 | return $args['display'] ?? true;
168 | };
169 | }
170 |
171 | $this->capability = $args['capability'] ?? 'manage_options';
172 | $this->callback = $args['callback'] ?? null;
173 | $this->hook_priority = $args['hook_priority'] ?? 10;
174 | $this->help_tabs = $args['help_tabs'] ?? array();
175 | $this->help_sidebar = $args['help_sidebar'] ?? '';
176 | $this->items = $args['items'] ?? array();
177 | $this->option_name = $args['meta_key'] ?? '';
178 | $this->tabs = $args['tabs'] ?? array();
179 | $this->is_new_tab = false;
180 |
181 | $tab = $args['tab'] ?? array();
182 |
183 | $tab['label'] = $tab['label'] ?? '';
184 |
185 | if ( empty( $tab['id'] ) ) {
186 | $tab['id'] = sanitize_title( $tab['label'] );
187 | }
188 |
189 | if ( empty( $tab['target'] ) ) {
190 | $tab['target'] = $tab['id'];
191 | }
192 |
193 | if ( empty( $tab['class'] ) ) {
194 | $tab['class'] = array();
195 | }
196 |
197 | $this->tab = $tab;
198 | $this->id = sanitize_title(
199 | join(
200 | '-',
201 | array(
202 | 'product-options',
203 | $this->tab['id'],
204 | sanitize_title( $this->tab['label'] ),
205 | $this->hook_priority,
206 | ),
207 | ),
208 | );
209 |
210 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
211 | add_filter( 'woocommerce_product_data_tabs', array( $this, 'woocommerce_product_data_tabs' ), 98 );
212 | add_action( 'woocommerce_product_data_panels', array( $this, 'render_data_panels' ) );
213 | add_action(
214 | 'woocommerce_product_options_' . $this->tab['target'] ?? $this->tab['id'],
215 | array(
216 | $this,
217 | 'render',
218 | )
219 | );
220 | add_action( 'woocommerce_process_product_meta', array( $this, 'save' ) );
221 | add_action( 'init', array( $this, 'register_meta' ), $this->hook_priority );
222 | }
223 | }
224 |
225 | /**
226 | * Modifies the WooCommerce product data tabs by updating existing tabs or adding new custom tabs.
227 | *
228 | * @param array $tabs An associative array of existing product data tabs.
229 | *
230 | * @return array The modified array of product data tabs.
231 | */
232 | public function woocommerce_product_data_tabs( array $tabs ): array {
233 | if ( isset( $tabs[ $this->tab['id'] ] ) ) {
234 | if ( ! empty( $this->tab['label'] ) ) {
235 | $tabs[ $this->tab['id'] ]['label'] = $this->tab['label'];
236 | }
237 |
238 | if ( ! empty( $this->tab['priority'] ) ) {
239 | $tabs[ $this->tab['id'] ]['priority'] = $this->tab['priority'];
240 | }
241 | } else {
242 | $this->is_new_tab = true;
243 |
244 | $tabs[ $this->tab['id'] ] = array(
245 | 'label' => $this->tab['label'],
246 | 'priority' => $this->tab['priority'],
247 | 'target' => $this->tab['target'],
248 | 'class' => $this->tab['class'],
249 | );
250 | }
251 |
252 | return $tabs;
253 | }
254 |
255 |
256 | /**
257 | * Renders the data panels for WooCommerce product options.
258 | *
259 | * This method outputs a div element with a specific ID and class, and triggers a custom WooCommerce action
260 | * based on the target attribute of the tab.
261 | *
262 | * @return void No return value.
263 | */
264 | public function render_data_panels(): void {
265 | if ( ! $this->is_new_tab ) {
266 | return;
267 | }
268 | ?>
269 |
270 | tab['target'] );
273 | ?>
274 |
275 | capability ) ) {
289 | return;
290 | }
291 |
292 | global $post;
293 |
294 | $this->product_id = $post->ID;
295 | $this->enqueue();
296 | $items = $this->normalize_items( $this->items );
297 |
298 | if ( is_callable( $this->callback ) ) {
299 | call_user_func( $this->callback );
300 | }
301 | ?>
302 |
303 | print_app( 'product-options', $this->tabs );
305 |
306 | foreach ( $items as $item ) {
307 | ?>
308 |
309 | print_field( $item ); ?>
310 |
311 |
314 |
315 | get_item_id() );
328 | }
329 |
330 | /**
331 | * Saves the product fields data for a given post.
332 | *
333 | * This function processes and saves custom fields associated with a product
334 | * for the specified post ID. The items are normalized and sanitized before
335 | * being stored.
336 | *
337 | * @param int $post_id The ID of the post being saved.
338 | *
339 | * @return void
340 | */
341 | public function save( int $post_id ): void {
342 | $this->product_id = $post_id;
343 |
344 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
345 | }
346 |
347 | /**
348 | * Registers custom metadata for the products.
349 | *
350 | * This method normalizes the defined items and registers each as post meta for 'product'.
351 | * It sets the meta properties such as type, description, default value, and sanitize callback.
352 | *
353 | * @return void
354 | */
355 | public function register_meta(): void {
356 | $items = $this->normalize_items( $this->items );
357 |
358 | foreach ( $items as $item ) {
359 | register_post_meta(
360 | 'product',
361 | $item['id'],
362 | array(
363 | 'type' => $this->custom_fields->get_wp_type( $item ),
364 | 'description' => $item['label'],
365 | 'single' => true,
366 | 'default' => $this->custom_fields->get_default_value( $item ),
367 | 'sanitize_callback' => $this->custom_fields->sanitize_item_value( $item ),
368 | 'show_in_rest' => false,
369 | ),
370 | );
371 | }
372 | }
373 |
374 | /**
375 | * Retrieves an option value from the product meta data.
376 | *
377 | * @param string $name The name of the meta key to retrieve.
378 | * @param mixed $default_value The default value to return if the meta key does not exist.
379 | *
380 | * @return mixed The value of the meta key if it exists, otherwise the default value.
381 | */
382 | public function get_option_value( string $name, mixed $default_value ): mixed {
383 | return $this->get_product()->get_meta( $name ) ?? $default_value;
384 | }
385 |
386 | /**
387 | * Sets an option value in the product meta data.
388 | *
389 | * @param string $name The name of the meta key to set.
390 | * @param mixed $value The value to be set for the meta key.
391 | *
392 | * @return bool True on success, false on failure.
393 | */
394 | public function set_option_value( string $name, mixed $value ): bool {
395 | $product = $this->get_product();
396 | $product->update_meta_data( $name, $value );
397 |
398 | return $product->save();
399 | }
400 |
401 | /**
402 | * Retrieves the item ID of the product.
403 | *
404 | * @return int The product's item ID.
405 | */
406 | public function get_item_id(): int {
407 | return $this->product_id;
408 | }
409 | }
410 |
--------------------------------------------------------------------------------
/src/Integrations/GutenbergBlock.php:
--------------------------------------------------------------------------------
1 | name = $args['name'];
263 | $this->id = $args['id'] ?? sprintf( 'wpifycf_block_%s', sanitize_title( $this->name ) );
264 | $this->api_version = $args['api_version'] ?? '3';
265 | $this->title = $args['title'];
266 | $this->category = $args['category'] ?? null;
267 | $this->parent = $args['parent'] ?? null;
268 | $this->ancestor = $args['ancestor'] ?? null;
269 | $this->allowed_blocks = $args['allowed_blocks'] ?? null;
270 |
271 | if ( ! empty( $args['icon'] ) && file_exists( $args['icon'] ) ) {
272 | global $wp_filesystem;
273 | if ( empty( $wp_filesystem ) ) {
274 | require_once ABSPATH . 'wp-admin/includes/file.php';
275 | WP_Filesystem();
276 | }
277 | $this->icon = $wp_filesystem->get_contents( $args['icon'] );
278 | } elseif ( ! empty( $args['icon'] ) ) {
279 | $this->icon = $args['icon'];
280 | } else {
281 | $this->icon = null;
282 | }
283 |
284 | $this->description = $args['description'] ?? '';
285 | $this->keywords = $args['keywords'] ?? array();
286 | $this->textdomain = $args['textdomain'] ?? null;
287 | $this->styles = $args['styles'] ?? array();
288 | $this->variations = $args['variations'] ?? array();
289 | $this->selectors = $args['selectors'] ?? array();
290 | $this->supports = $args['supports'] ?? null;
291 | $this->example = $args['example'] ?? null;
292 | $this->render_callback = $args['render_callback'] ?? null;
293 | $this->variation_callback = $args['variation_callback'] ?? null;
294 | $this->uses_context = $args['uses_context'] ?? array();
295 | $this->provides_context = $args['provides_context'] ?? null;
296 | $this->block_hooks = $args['block_hooks'] ?? array();
297 | $this->editor_script_handles = $args['editor_script_handles'] ?? array();
298 | $this->script_handles = $args['script_handles'] ?? array();
299 | $this->view_script_handles = $args['view_script_handles'] ?? array();
300 | $this->editor_style_handles = $args['editor_style_handles'] ?? array();
301 | $this->style_handles = $args['style_handles'] ?? array();
302 | $this->view_style_handles = $args['view_style_handles'] ?? array();
303 | $this->items = $args['items'] ?? array();
304 | $this->attributes = $this->get_attributes();
305 | $this->tabs = $args['tabs'] ?? array();
306 |
307 | add_action( 'init', array( $this, 'register_block' ) );
308 | add_action( 'enqueue_block_assets', array( $this, 'enqueue' ) );
309 | add_action( 'rest_api_init', array( $this, 'register_routes' ) );
310 | }
311 |
312 | /**
313 | * Registers a custom block with the given arguments and configurations.
314 | *
315 | * @return void
316 | */
317 | public function register_block(): void {
318 | $args = array(
319 | ...$this->get_args(),
320 | 'variation_callback' => $this->variation_callback,
321 | 'editor_script_handles' => $this->editor_script_handles,
322 | 'script_handles' => $this->script_handles,
323 | 'view_script_handles' => $this->view_script_handles,
324 | 'editor_style_handles' => $this->editor_style_handles,
325 | 'style_handles' => $this->style_handles,
326 | 'view_style_handles' => $this->view_style_handles,
327 | );
328 |
329 | if ( $this->render_callback ) {
330 | $args['render_callback'] = array( $this, 'render' );
331 | }
332 |
333 | register_block_type( $this->name, $args );
334 | do_action(
335 | 'wpifycf_register_block',
336 | $this->name,
337 | array(
338 | ...$args,
339 | 'items' => $this->normalize_items( $this->items ),
340 | )
341 | );
342 | }
343 |
344 | /**
345 | * Enqueue the script and dispatch a custom event with the block data.
346 | *
347 | * @return void
348 | */
349 | public function enqueue(): void {
350 | parent::enqueue();
351 |
352 | $data = array(
353 | 'name' => $this->name,
354 | 'items' => $this->normalize_items( $this->items ),
355 | 'args' => $this->get_args(),
356 | 'tabs' => $this->tabs,
357 | 'instance' => $this->custom_fields->get_script_handle(),
358 | );
359 |
360 | wp_add_inline_script(
361 | $this->custom_fields->get_script_handle(),
362 | 'document.dispatchEvent(new CustomEvent("wpifycf_register_block_' . $this->custom_fields->get_script_handle() . '",{detail:' . wp_json_encode( $data ) . '}));',
363 | );
364 | }
365 |
366 | /**
367 | * Retrieves the arguments for the block configuration.
368 | *
369 | * This method collects various configuration properties of the block,
370 | * filters out any empty values, and returns the resulting array.
371 | *
372 | * @return array The array of block configuration arguments, with empty values removed.
373 | */
374 | public function get_args(): array {
375 | $args = array(
376 | 'api_version' => $this->api_version,
377 | 'title' => $this->title,
378 | 'category' => $this->category,
379 | 'parent' => $this->parent,
380 | 'ancestor' => $this->ancestor,
381 | 'allowed_blocks' => $this->allowed_blocks,
382 | 'icon' => $this->icon,
383 | 'description' => $this->description,
384 | 'keywords' => $this->keywords,
385 | 'textdomain' => $this->textdomain,
386 | 'styles' => $this->styles,
387 | 'variations' => $this->variations,
388 | 'selectors' => $this->selectors,
389 | 'supports' => $this->supports,
390 | 'example' => $this->example,
391 | 'attributes' => $this->attributes,
392 | 'uses_context' => $this->uses_context,
393 | 'provides_context' => $this->provides_context,
394 | 'block_hooks' => $this->block_hooks,
395 | );
396 |
397 | foreach ( $args as $key => $arg ) {
398 | if ( empty( $arg ) ) {
399 | unset( $args[ $key ] );
400 | }
401 | }
402 |
403 | return $args;
404 | }
405 |
406 | /**
407 | * Retrieves the attributes for the items.
408 | *
409 | * Normalizes the items and then creates an array of attributes, where each attribute is identified by
410 | * its ID and contains its type and default value.
411 | *
412 | * @return array Associative array of attributes, with each key being an item ID and each value being
413 | * an array containing 'type' and 'default' keys.
414 | */
415 | public function get_attributes(): array {
416 | $items = $this->normalize_items( $this->items );
417 | $attributes = array();
418 |
419 | foreach ( $items as $item ) {
420 | $attributes[ $item['id'] ] = array(
421 | 'type' => $this->custom_fields->get_wp_type( $item ),
422 | 'default' => $this->custom_fields->get_default_value( $item ),
423 | );
424 | }
425 |
426 | return $attributes;
427 | }
428 |
429 | /**
430 | * Renders a block's content based on provided attributes and a rendering callback.
431 | *
432 | * @param array $attributes The block attributes.
433 | * @param string $content The block content.
434 | * @param WP_Block $block The block instance.
435 | *
436 | * @return string The rendered block content.
437 | */
438 | public function render( array $attributes, string $content, WP_Block $block ): string {
439 | if (
440 | ( defined( 'REST_REQUEST' ) && REST_REQUEST && filter_input( INPUT_GET, 'context' ) !== 'edit' )
441 | || ( null === $this->render_callback )
442 | ) {
443 | return $content;
444 | }
445 |
446 | $attributes = $this->normalize_attributes( $attributes );
447 | $rendered_content = call_user_func( $this->render_callback, $attributes, $content, $block );
448 |
449 | if ( empty( $rendered_content ) || ! is_string( $rendered_content ) ) {
450 | return $content;
451 | }
452 |
453 | if ( preg_match( '//', $rendered_content ) ) {
454 | $rendered_content = preg_replace( '//', $content, $rendered_content );
455 | }
456 |
457 | return $rendered_content;
458 | }
459 |
460 | /**
461 | * Registers the REST API routes for the block.
462 | *
463 | * @return void
464 | */
465 | public function register_routes(): void {
466 | $this->custom_fields->api->register_rest_route(
467 | 'render-block/' . $this->name,
468 | WP_REST_Server::CREATABLE,
469 | array( $this, 'render_from_api' ),
470 | );
471 | }
472 |
473 | /**
474 | * Renders a block's content based on provided attributes and a rendering callback.
475 | *
476 | * @param \WP_REST_Request $request The REST API request object.
477 | *
478 | * @return string The rendered block content.
479 | */
480 | public function render_from_api( \WP_REST_Request $request ): string {
481 | $attributes = $request->get_param( 'attributes' );
482 | $post_id = $request->get_param( 'postId' );
483 |
484 | $parsed_block = array(
485 | 'blockName' => $this->name,
486 | 'attrs' => $attributes,
487 | 'innerBlocks' => array(),
488 | 'innerHTML' => '',
489 | 'innerContent' => array(),
490 | );
491 |
492 | if ( null === $this->render_callback ) {
493 | return '';
494 | }
495 |
496 | if ( $post_id ) {
497 | setup_postdata( $post_id );
498 | }
499 |
500 | $attributes = $this->normalize_attributes( $attributes );
501 |
502 | return call_user_func( $this->render_callback, $attributes, '', new WP_Block( $parsed_block ) );
503 | }
504 |
505 | /**
506 | * Normalizes the attributes array.
507 | *
508 | * @param array $attributes Block attributes.
509 | *
510 | * @return array
511 | */
512 | public function normalize_attributes( array $attributes ): array {
513 | $normalized_attributes = array();
514 |
515 | foreach ( $attributes as $attribute_name => $attribute_value ) {
516 | if ( $attribute_value instanceof stdClass ) {
517 | $normalized_attributes[ $attribute_name ] = (array) $attribute_value;
518 | } else {
519 | $normalized_attributes[ $attribute_name ] = $attribute_value;
520 | }
521 | }
522 |
523 | return $normalized_attributes;
524 | }
525 | }
526 |
--------------------------------------------------------------------------------
/src/Integrations/SiteOptions.php:
--------------------------------------------------------------------------------
1 | 0 ) {
191 | throw new MissingArgumentException(
192 | sprintf(
193 | /* translators: %1$s is a list of missing arguments, %2$s is the class name. */
194 | esc_html( __( 'Missing arguments %1$s in class %2$s.', 'wpify-custom-fields' ) ),
195 | esc_html( implode( ', ', $missing ) ),
196 | __CLASS__,
197 | ),
198 | );
199 | }
200 |
201 | // Nonce verification not needed.
202 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended
203 | $this->blog_id = empty( $_REQUEST['id'] ) ? 0 : absint( $_REQUEST['id'] );
204 | $this->page_title = $args['page_title'];
205 | $this->menu_title = $args['menu_title'];
206 | $this->capability = $args['capability'] ?? 'manage_options';
207 | $this->menu_slug = $args['menu_slug'];
208 | $this->callback = $args['callback'] ?? null;
209 | $this->hook_priority = $args['hook_priority'] ?? 10;
210 | $this->help_tabs = $args['help_tabs'] ?? array();
211 | $this->help_sidebar = $args['help_sidebar'] ?? '';
212 | $this->display = $args['display'] ?? null;
213 | $this->submit_button = $args['submit_button'] ?? true;
214 | $this->items = $args['items'] ?? array();
215 | $this->option_name = $args['option_name'] ?? '';
216 | $this->option_group = empty( $this->option_name ) ? sanitize_title( $this->menu_slug ) : $this->option_name;
217 | $this->tabs = $args['tabs'] ?? array();
218 | $this->success_message = empty( $args['success_message'] )
219 | ? __( 'Settings saved', 'wpify-custom-fields' )
220 | : $args['success_message'];
221 |
222 | if ( empty( $args['sections'] ) ) {
223 | $args['sections'] = array();
224 | }
225 |
226 | $sections = array();
227 |
228 | foreach ( $args['sections'] as $key => $section ) {
229 | if ( empty( $section['id'] ) && is_string( $key ) ) {
230 | $section['id'] = $key;
231 | }
232 |
233 | $sections[ $section['id'] ] = $section;
234 | }
235 |
236 | if ( empty( $sections ) ) {
237 | $sections = array(
238 | 'default' => array(
239 | 'id' => 'default',
240 | 'title' => '',
241 | 'callback' => '__return_true',
242 | 'page' => $this->menu_slug,
243 | ),
244 | );
245 | }
246 |
247 | $this->sections = $sections;
248 |
249 | $this->id = sanitize_title(
250 | join(
251 | '-',
252 | array(
253 | 'site-options',
254 | $this->blog_id,
255 | $this->menu_slug,
256 | ),
257 | ),
258 | );
259 |
260 | foreach ( $this->sections as $section ) {
261 | $this->default_section = $section['id'];
262 | break;
263 | }
264 |
265 | if ( ! defined( 'WP_CLI' ) || false === WP_CLI ) {
266 | add_filter( 'network_edit_site_nav_links', array( $this, 'create_tab' ) );
267 | add_action( 'network_admin_menu', array( $this, 'register' ) );
268 | add_action( 'admin_init', array( $this, 'register_settings' ), $this->hook_priority );
269 | add_action( 'network_admin_edit_' . $this::SAVE_ACTION, array( $this, 'save_site_options' ) );
270 | add_action( 'current_screen', array( $this, 'set_page_title' ) );
271 | }
272 | }
273 |
274 | /**
275 | * Registers the submenu page and its related actions if the display condition is met.
276 | *
277 | * @return void
278 | */
279 | public function register(): void {
280 | if (
281 | ( $this->display && is_callable( $this->display ) && ! call_user_func( $this->display ) )
282 | || ( ! is_null( $this->display ) && ! $this->display )
283 | ) {
284 | return;
285 | }
286 |
287 | $hook_suffix = add_submenu_page(
288 | '',
289 | $this->page_title,
290 | $this->menu_title,
291 | $this->capability,
292 | $this->menu_slug,
293 | array( $this, 'render' ),
294 | );
295 |
296 | $this->hook_suffix = $hook_suffix . '-network';
297 |
298 | add_action( 'load-' . $this->hook_suffix, array( $this, 'render_help' ) );
299 | }
300 |
301 | /**
302 | * Adds a new tab to the given tabs array.
303 | *
304 | * @param array $tabs The array of existing tabs.
305 | *
306 | * @return array The updated array of tabs including the newly added tab.
307 | */
308 | public function create_tab( array $tabs ): array {
309 | $tabs[ $this->menu_slug ] = array(
310 | 'label' => $this->menu_title,
311 | 'url' => add_query_arg( 'page', $this->menu_slug, 'sites.php' ),
312 | 'cap' => 'manage_sites',
313 | );
314 |
315 | return $tabs;
316 | }
317 |
318 | /**
319 | * Renders the settings page for editing a site.
320 | *
321 | * This method checks user capabilities, retrieves the site details based on the provided ID,
322 | * and renders the settings page along with necessary actions and messages.
323 | *
324 | * @return void
325 | */
326 | public function render(): void {
327 | if ( ! current_user_can( $this->capability ) ) {
328 | return;
329 | }
330 |
331 | // Nonce verification not needed.
332 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended
333 | $id = isset( $_REQUEST['id'] ) ? absint( $_REQUEST['id'] ) : 0;
334 | $site = get_site( $id );
335 |
336 | if ( empty( $site ) ) {
337 | return;
338 | }
339 | $action = add_query_arg( 'action', $this::SAVE_ACTION, 'edit.php' );
340 |
341 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended
342 | if ( ! empty( $_GET['settings-updated'] ) ) {
343 | add_settings_error( $this->menu_slug . '_error', $this->menu_slug . '_message', $this->success_message, 'updated' );
344 | }
345 |
346 | settings_errors( $this->menu_slug . '_error' );
347 | $this->enqueue();
348 | ?>
349 |
350 |
351 | blogname ) );
354 | ?>
355 |
356 |
357 |
358 | |
359 |
360 |
361 | $id,
365 | 'selected' => $this->menu_slug,
366 | ),
367 | );
368 | ?>
369 |
370 | page_title ); ?>
371 |
372 | callback ) ) {
374 | call_user_func( $this->callback );
375 | }
376 | ?>
377 |
411 |
412 | help_tabs as $key => $tab ) {
426 | $tab = wp_parse_args(
427 | $tab,
428 | array(
429 | 'id' => '',
430 | 'title' => '',
431 | 'content' => '',
432 | ),
433 | );
434 |
435 | if ( empty( $tab['id'] ) ) {
436 | if ( ! empty( $key ) && is_string( $key ) ) {
437 | $tab['id'] = $key;
438 | } else {
439 | $tab['id'] = sanitize_title( $tab['title'] ) . '_' . $key;
440 | }
441 | }
442 |
443 | if ( is_callable( $tab['content'] ) ) {
444 | $tab['content'] = call_user_func( $tab['content'] );
445 | }
446 |
447 | get_current_screen()->add_help_tab( $tab );
448 | }
449 |
450 | if ( ! empty( $this->help_sidebar ) ) {
451 | get_current_screen()->set_help_sidebar( $this->help_sidebar );
452 | }
453 | }
454 |
455 | /**
456 | * Saves the site options.
457 | *
458 | * This method normalizes the items, processes the incoming POST data for either
459 | * a named option or individual items, sanitizes the data, and sets the fields accordingly.
460 | * It then redirects the user back to the referring page with a success flag.
461 | *
462 | * @return void
463 | */
464 | public function save_site_options(): void {
465 | $this->set_fields_from_post_request( $this->normalize_items( $this->items ) );
466 | wp_safe_redirect(
467 | add_query_arg(
468 | array( 'updated' => true ),
469 | wp_get_referer(),
470 | ),
471 | );
472 | exit;
473 | }
474 |
475 | /**
476 | * Registers the settings for the custom fields.
477 | *
478 | * This method normalizes the items, registers individual or grouped settings based on the given configuration,
479 | * and adds the necessary sections and fields to the settings page.
480 | *
481 | * @return void
482 | */
483 | public function register_settings(): void {
484 | $items = $this->normalize_items( $this->items );
485 |
486 | if ( empty( $this->option_name ) ) {
487 | foreach ( $items as $item ) {
488 | register_setting(
489 | $this->option_group,
490 | $item['id'],
491 | array(
492 | 'type' => $this->custom_fields->get_wp_type( $item ),
493 | 'label' => $item['label'] ?? '',
494 | 'sanitize_callback' => $this->custom_fields->sanitize_item_value( $item ),
495 | 'show_in_rest' => false,
496 | 'default' => $this->custom_fields->get_default_value( $item ),
497 | ),
498 | );
499 | }
500 | } else {
501 | register_setting(
502 | $this->option_group,
503 | $this->option_name,
504 | array(
505 | 'type' => 'object',
506 | 'label' => $this->page_title,
507 | 'sanitize_callback' => $this->custom_fields->sanitize_option_value( $items ),
508 | 'show_in_rest' => false,
509 | 'default' => array(),
510 | ),
511 | );
512 | }
513 |
514 | foreach ( $this->sections as $id => $section ) {
515 | add_settings_section(
516 | $id,
517 | $section['label'] ?? '',
518 | $section['callback'] ?? '__return_true',
519 | $this->menu_slug,
520 | $section['args'] ?? array(),
521 | );
522 | }
523 |
524 | foreach ( $items as $item ) {
525 | $section = $this->sections[ $item['section'] ]['id'] ?? $this->default_section;
526 |
527 | add_settings_field(
528 | $item['id'],
529 | $item['label'],
530 | array( $this, 'print_field' ),
531 | $this->menu_slug,
532 | $section,
533 | array(
534 | 'label_for' => $item['id'],
535 | ...$item,
536 | ),
537 | );
538 | }
539 | }
540 |
541 | /**
542 | * Normalizes an item by setting default values for its properties.
543 | *
544 | * This method overrides the parent function to provide additional normalization for the item.
545 | * If the 'section' property is not set, it defaults to 'general'.
546 | *
547 | * @param array $item The item to be normalized.
548 | * @param string $global_id Optional. A global identifier that can be used during normalization. Default is an empty string.
549 | *
550 | * @return array The normalized item.
551 | */
552 | protected function normalize_item( array $item, string $global_id = '' ): array {
553 | $item = parent::normalize_item( $item, $global_id );
554 |
555 | if ( empty( $item['section'] ) ) {
556 | $item['section'] = 'general';
557 | }
558 |
559 | return $item;
560 | }
561 |
562 | /**
563 | * Sets the page title for a network admin screen when editing a specific site.
564 | *
565 | * This method updates the global `$title` variable with a formatted string representing the current site's name.
566 | *
567 | * @param WP_Screen $current_screen The current screen object.
568 | *
569 | * @return void
570 | */
571 | public function set_page_title( WP_Screen $current_screen ): void {
572 | if ( is_network_admin() && $current_screen->id === $this->hook_suffix ) {
573 | global $title;
574 | $site = get_site( $this->blog_id );
575 |
576 | /* translators: Blog name */
577 | $title = sprintf( __( 'Edit Site: %s', 'wpify-custom-fields' ), esc_html( $site->blogname ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
578 | }
579 | }
580 |
581 | /**
582 | * Retrieves the value of a specified option for the current blog.
583 | *
584 | * @param string $name The name of the option to retrieve.
585 | * @param mixed $default_value The default value to return if the option does not exist.
586 | */
587 | public function get_option_value( string $name, mixed $default_value ): mixed {
588 | return get_blog_option( $this->blog_id, $name, $default_value );
589 | }
590 |
591 | /**
592 | * Sets the value of a specified option for the current blog.
593 | *
594 | * @param string $name The name of the option to set.
595 | * @param mixed $value The value to set for the specified option.
596 | *
597 | * @return bool True if the option was successfully set, false otherwise.
598 | */
599 | public function set_option_value( string $name, mixed $value ): bool {
600 | return update_blog_option( $this->blog_id, $name, $value );
601 | }
602 | }
603 |
--------------------------------------------------------------------------------