PluginProbe
ZIP AI – AI Website Builder & AI Agent (Beta) / 0.0.4
ZIP AI – AI Website Builder & AI Agent (Beta) v0.0.4
0.0.10 0.0.9 trunk 0.0.4 0.0.5 0.0.6 0.0.7 0.0.8
zip-ai / classes / abilities / Ability_Loader.php

Ability_Loader.php in ZIP AI – AI Website Builder & AI Agent (Beta) 0.0.4, at classes/abilities/Ability_Loader.php

176 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Ability Loader Trait
4 *
5 * @package zip-ai
6 */
7
8 namespace ZipAI\Classes\Abilities;
9
10 defined( 'ABSPATH' ) || exit;
11
12 use ZipAI\Classes\Abilities\Abstract_Ability;
13
14 /**
15 * Trait Ability_Loader
16 */
17 trait Ability_Loader {
18
19 public function check_ability_format( $ability ) {
20 $id = $ability->get_id();
21 $canonical_actions = array(
22 // CRUD.
23 'list',
24 'get',
25 'create',
26 'update',
27 'delete',
28 // State.
29 'activate',
30 'deactivate',
31 'restore',
32 // Lifecycle.
33 'install',
34 'uninstall',
35 'upload',
36 // Data.
37 'import',
38 'export',
39 'flush',
40 'replace',
41 // Operations.
42 'check',
43 'clean',
44 'run',
45 'edit',
46 'read',
47 'search',
48 'scan',
49 );
50 $pattern = '/^[a-z0-9_-]+\/(' . implode( '|', $canonical_actions ) . ')-[a-z0-9-]+$/';
51
52 if ( ! preg_match( $pattern, $id ) ) {
53 // Developer-only diagnostic: triggered on plugin boot under
54 // WP_DEBUG so ability authors see malformed IDs early. All
55 // substitutions are controlled values (ability id, class name,
56 // action allowlist) — not user input — but we still escape for
57 // defense-in-depth.
58 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
59 _doing_it_wrong(
60 esc_html( get_class( $ability ) . '::register_ability' ),
61 esc_html(
62 sprintf(
63 /* translators: 1: ability id, 2: class name, 3: comma-separated list of allowed actions */
64 'Invalid Ability ID format: "%1$s" in class %2$s. Expected format: "{namespace}/{action}-{resource}" (e.g., "zip-ai/update-post_format", "zip-ai/list-block-patterns"). Action must be one of: %3$s.',
65 $id,
66 get_class( $ability ),
67 implode( ', ', $canonical_actions )
68 )
69 ),
70 esc_html( ZIP_AI_VERSION )
71 );
72 }
73 return false;
74 }
75 return true;
76 }
77
78 protected function load_abilities_from_dir( $directory, $namespace ) {
79 // Normalize directory path.
80 $directory = trailingslashit( $directory );
81
82 // Check if directory exists.
83 if ( ! is_dir( $directory ) ) {
84 return;
85 }
86
87 // Use RecursiveDirectoryIterator for recursive scanning.
88 $directory_iterator = new \RecursiveDirectoryIterator( $directory, \RecursiveDirectoryIterator::SKIP_DOTS );
89 $iterator = new \RecursiveIteratorIterator( $directory_iterator );
90
91 foreach ( $iterator as $file_info ) {
92 if ( $file_info->isDir() || 'php' !== $file_info->getExtension() ) {
93 continue;
94 }
95
96 $file_path = $file_info->getPathname();
97 $class_name = $file_info->getBasename( '.php' );
98
99 // Skip index and handler files.
100 if ( 'index' === $class_name || 'handler' === $class_name ) {
101 continue;
102 }
103
104 // Calculate class namespace based on relative path.
105 $relative_path = str_replace( array( $directory, '.php' ), '', $file_path );
106 $path_parts = explode( DIRECTORY_SEPARATOR, $relative_path );
107 array_pop( $path_parts ); // Remove class name from parts.
108
109 // Capitalize each path part to match PSR-4 namespace convention.
110 $path_parts = array_map( 'ucfirst', $path_parts );
111
112 $sub_namespace = ! empty( $path_parts ) ? '\\' . implode( '\\', $path_parts ) : '';
113 $full_class_name = $namespace . $sub_namespace . '\\' . $class_name;
114
115 // Check if class exists.
116 if ( class_exists( $full_class_name ) ) {
117 // Instantiate the ability.
118 $ability = new $full_class_name();
119
120 // Check if it's an instance of Abstract_Ability.
121 if ( $ability instanceof Abstract_Ability ) {
122 // Collect meta data.
123 $meta = array(
124 'tool_type' => $ability->get_tool_type(),
125 'examples' => $ability->get_examples(),
126 'api_endpoint' => $ability->get_api_endpoint(),
127 );
128
129 // Add boost screens if defined.
130 if ( method_exists( $ability, 'get_boost_screens' ) ) {
131 $boost_screens = $ability->get_boost_screens();
132 if ( ! empty( $boost_screens ) ) {
133 $meta['boost_screens'] = $boost_screens;
134 }
135 }
136
137 // Add resource identifier if defined.
138 if ( method_exists( $ability, 'get_resource' ) ) {
139 $resource = $ability->get_resource();
140 if ( ! empty( $resource ) ) {
141 $meta['resource'] = $resource;
142 }
143 }
144
145 // Merge with any class-specific meta.
146 if ( method_exists( $ability, 'get_meta_data' ) ) {
147 $meta = array_merge( $meta, $ability->get_meta_data() );
148 }
149
150 // Add constraints if the method exists.
151 if ( method_exists( $ability, 'get_constraints' ) ) {
152 $meta['constraints'] = $ability->get_constraints();
153 }
154
155 // Validate ID format.
156 $this->check_ability_format( $ability );
157
158 // Register directly with wp_register_ability (no lazy loading).
159 wp_register_ability(
160 $ability->get_id(),
161 array(
162 'label' => $ability->get_label(),
163 'description' => $ability->get_description(),
164 'category' => $ability->get_category(),
165 'input_schema' => $ability->get_final_input_schema(),
166 'execute_callback' => array( $ability, 'handle_execute' ),
167 'permission_callback' => array( $ability, 'check_permission' ),
168 'meta' => $meta,
169 )
170 );
171 }
172 }
173 }
174 }
175 }
176