PluginProbe
Metricool – Social media and site statistics / 2.0.2
Metricool – Social media and site statistics v2.0.2
2.1.0 2.0.2 2.0.1 2.0.0 1.27 trunk
metricool / app / Managers / EndpointManager.php

EndpointManager.php in Metricool – Social media and site statistics 2.0.2, at app/Managers/EndpointManager.php

266 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare(strict_types=1);
4
5 namespace Metricool\Managers;
6
7 use Metricool\Bootstrap\App;
8 use Metricool\Interfaces\MiddlewareInterface;
9 use Metricool\Interfaces\MultiEndpointInterface;
10 use Metricool\Interfaces\SingleEndpointInterface;
11 use Metricool\Support\Helpers\Storages\EnvironmentConfig;
12 use Metricool\Support\Helpers\Storages\MiddlewareConfig;
13 use Metricool\Traits\HasAllowlistControl;
14 use Metricool\Traits\HasNonces;
15
16 final class EndpointManager extends AbstractManager
17 {
18 use HasNonces;
19 use HasAllowlistControl;
20
21 private EnvironmentConfig $env;
22
23 private array $routes = [];
24 private array $defaultMiddleware;
25 private array $aliases;
26
27 public function __construct(MiddlewareConfig $middleware, EnvironmentConfig $env)
28 {
29 $this->env = $env;
30 $this->aliases = $middleware->get('aliases', []);
31 $this->defaultMiddleware = $middleware->get('default_middleware', []);
32 }
33
34 /**
35 * @inheritDoc
36 */
37 public function isRegistrable(object $class): bool
38 {
39 return ($class instanceof SingleEndpointInterface
40 || $class instanceof MultiEndpointInterface
41 );
42 }
43
44 /**
45 * @inheritDoc
46 */
47 public function registerClass(object $class): void
48 {
49 if ($class instanceof SingleEndpointInterface) {
50 $this->registerSingleEndpointRoute($class);
51 }
52
53 if ($class instanceof MultiEndpointInterface) {
54 $this->registerMultiEndpointRoute($class);
55 }
56 }
57
58 /**
59 * @inheritDoc
60 */
61 public function afterRegister(): void
62 {
63 $this->registerWordPressRestRoutes();
64 do_action('metricool_endpoints_loaded');
65 }
66
67 /**
68 * Register a plugin route for and endpoint instance that implements the
69 * {@see SingleEndpointInterface}
70 */
71 private function registerSingleEndpointRoute(SingleEndpointInterface $endpoint): void
72 {
73 if ($endpoint->enabled() === false) {
74 return;
75 }
76
77 $this->routes[$endpoint->registerRoute()] = $endpoint->registerArguments();
78 }
79
80 /**
81 * Register plugin routes for an endpoint instance that implements the
82 * {@see MultiEndpointInterface}
83 */
84 private function registerMultiEndpointRoute(MultiEndpointInterface $endpoint): void
85 {
86 if ($endpoint->enabled() === false) {
87 return;
88 }
89
90 $routeEndpoints = $endpoint->registerRoutes();
91 foreach ($routeEndpoints as $route => $arguments) {
92 $this->routes[$route] = $arguments;
93 }
94 }
95
96 /**
97 * This method provides a way to register custom REST routes via the
98 * metricool_rest_routes filter. A controller or feature should be
99 * instantiated before this manager is called and the controller should
100 * hook into the metricool_rest_routes filter to add its own routes.
101 *
102 * public function registerArguments(): array
103 * {
104 * return [
105 * 'methods' => \WP_REST_Server::READABLE,
106 * 'callback' => [$this, 'callback'],
107 * 'permission_callback' => [$this, 'permissionCallback'],
108 * 'middleware' => [
109 * 'user_can:administrator', // alias in config/middleware.php
110 * ExampleMiddleware::class, // MiddlewareInterface class
111 * ],
112 * 'apply_default_middleware' => true, // optional, default is true
113 * 'version' => 'v1', // optional, default is the value config/env.php
114 * 'args' => [], // optional, args passed to register_rest_route
115 * ];
116 * }
117 *
118 * @uses apply_filters metricool_rest_routes
119 * @throws \InvalidArgumentException
120 * @throws \ReflectionException
121 */
122 public function registerWordPressRestRoutes(): void
123 {
124 $routes = $this->getPluginRestRoutes();
125
126 foreach ($routes as $route => $data) {
127 $methods = ($data['methods'] ?? 'GET');
128 $callback = ($data['callback'] ?? null);
129 $permissionCallback = ($data['permission_callback'] ?? '__return_true');
130 $middleware = ($data['middleware'] ?? []);
131 $applyDefaultMiddleware = ($data['apply_default_middleware'] ?? true);
132 $version = ($data['version'] ?? $this->env->getString('http.version'));
133 $args = ($data['args'] ?? null);
134
135 if (!is_callable($callback)) {
136 throw new \InvalidArgumentException(
137 esc_html(sprintf('The callback for the route: %s is not callable.', $route))
138 );
139 }
140
141 if ($applyDefaultMiddleware === true) {
142 $middleware = array_merge($this->defaultMiddleware, $middleware);
143 }
144
145 $arguments = [
146 'methods' => $this->normalizeMethods($methods),
147 'callback' => $this->callbackMiddleware($callback, $middleware),
148 'permission_callback' => $permissionCallback,
149 ];
150
151 if ($args !== null) {
152 $arguments['args'] = $args;
153 }
154
155 register_rest_route($this->env->getString('http.namespace') . '/' . $version, $route, $arguments);
156 }
157 }
158
159 /**
160 * Get the plugins REST routes
161 * @uses apply_filters metricool_rest_routes
162 */
163 private function getPluginRestRoutes(): array
164 {
165 /**
166 * Filter: metricool_rest_routes
167 * Can be used to add or modify the REST routes
168 *
169 * @param array $routes
170 * @return array
171 * @example [
172 * 'route' => [ // key is the route name
173 * 'methods' => 'GET', // required
174 * 'callback' => 'callback_function', // required
175 * 'permission_callback' => 'permission_callback_function', // optional to override the default permission callback
176 * 'version' => 'v1' // optional to override the default version
177 * ]
178 * ]
179 */
180 return apply_filters('metricool_rest_routes', $this->routes);
181 }
182
183 /**
184 * Wrap the endpoint callback with the default middleware and any named middleware from the pipeline.
185 * Each entry can be either a registered alias (e.g. 'auth:metricool') or a fully qualified class name (e.g. MetricoolAuthenticated::class).
186 * @throws \ReflectionException
187 */
188 public function callbackMiddleware(callable $callback, array $middlewares = []): callable
189 {
190 $middlewareInstances = $this->resolveMiddleware($middlewares);
191
192 return static function (\WP_REST_Request $request) use ($callback, $middlewareInstances) {
193 try {
194 foreach ($middlewareInstances as $middleware) {
195 $response = $middleware->handle($request);
196 if ($response instanceof \WP_REST_Response) {
197 return $response;
198 }
199 }
200 } catch (\Exception $e) {
201 return new \WP_REST_Response(['message' => $e->getMessage()], 500);
202 }
203
204 return $callback($request);
205 };
206 }
207
208 /**
209 * Resolve middleware entries to MiddlewareInterface instances.
210 *
211 * @param string[] $middleware
212 * @return MiddlewareInterface[]
213 * @throws \ReflectionException
214 */
215 private function resolveMiddleware(array $middleware): array
216 {
217 $resolved = [];
218
219 foreach ($middleware as $entry) {
220 $class = $this->aliases[$entry] ?? $entry;
221
222 if (!is_string($class) || !class_exists($class)) {
223 throw new \InvalidArgumentException(
224 esc_html(sprintf("Middleware: %s could not be resolved to a valid class.", $entry))
225 );
226 }
227
228 $instance = App::getInstance()->get($class);
229
230 if (!$instance instanceof MiddlewareInterface) {
231 throw new \InvalidArgumentException(
232 esc_html(sprintf('Middleware: %s must implement MiddlewareInterface.', $entry))
233 );
234 }
235
236 $resolved[] = $instance;
237 }
238
239 return $resolved;
240 }
241
242 /**
243 * Process the given methods and compare them to the allowed
244 * {@see \WP_REST_Server::ALLMETHODS} methods. Remove unwanted entries and
245 * cleanup method usage from, for example, "get " to "GET".
246 *
247 * @return string From "get, POSt, fake" to "GET,POST"
248 */
249 private function normalizeMethods(string $methods): string
250 {
251 // Split into array, trim whitespace and uppercase entries
252 $methodsArray = array_map('trim', explode(',', $methods));
253 $methodsArray = array_map('strtoupper', $methodsArray);
254
255 // Split allowed entries into array and trim whitespaces
256 $allowedMethodsArray = array_map('trim', explode(',', \WP_REST_Server::ALLMETHODS));
257
258 // Keep only allowed methods
259 $methodsArray = array_intersect($methodsArray, $allowedMethodsArray);
260 $methodsArray = array_values(array_unique($methodsArray));
261
262 // Convert back to CSV format for register_rest_route usage
263 return implode(',', $methodsArray);
264 }
265 }
266