PluginProbe ʕ •ᴥ•ʔ
Presto Player / trunk
Presto Player vtrunk
4.4.1 4.4.0 4.3.3 4.3.2 4.3.1 4.3.0 4.2.4 4.2.3 4.2.2 4.2.0 4.2.1 trunk 1.10.0 1.10.1 1.10.2 1.11.0 1.12.0 1.13.0 1.14.0 1.14.1 1.5.10 1.5.11 1.5.12 1.5.13 1.5.14 1.5.15 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.10 1.6.11 1.6.12 1.6.13 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 1.6.7 1.6.8 1.6.9 1.7.0 1.7.1 1.7.2 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 1.8.5 1.8.6 1.9.0 1.9.1 1.9.10 1.9.11 1.9.12 1.9.13 1.9.14 1.9.2 1.9.3 1.9.4 1.9.5 1.9.6 1.9.7 1.9.8 1.9.9 2.0.0 2.0.1 2.0.10 2.0.11 2.0.12 2.0.13 2.0.14 2.0.15 2.0.16 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.0.7 2.0.8 2.0.9 2.1.0 2.2.0 2.2.1 2.2.2 2.2.3 2.2.3-beta1 2.3.0 2.3.1 2.3.2 2.3.3 3.0.0 3.0.0-beta1 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.1.0 3.1.1 3.1.2 3.1.3 4.0.0 4.0.1 4.0.2 4.0.3 4.0.4 4.0.5 4.0.6 4.0.7 4.0.8 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4
presto-player / inc / Services / Abilities / Module.php
presto-player / inc / Services / Abilities Last commit date
Abilities 1 week ago Concerns 1 week ago Ability.php 1 week ago Module.php 1 week ago
Module.php
622 lines
1 <?php
2 /**
3 * Abilities API + MCP module bootstrap.
4 *
5 * Registers the presto-player ability category, all free abilities, and the
6 * option that stores the two toggles (enabled, allow_changes). Pro abilities
7 * are appended via the "presto_player_abilities" filter from pro's bootstrap.
8 *
9 * @package PrestoPlayer
10 * @subpackage Services\Abilities
11 */
12
13 namespace PrestoPlayer\Services\Abilities;
14
15 use PrestoPlayer\Contracts\Service;
16
17 /**
18 * Service entry point for the Abilities API integration.
19 */
20 class Module implements Service {
21
22 /**
23 * Option key for the two toggles (enabled, allow_changes).
24 *
25 * @var string
26 */
27 public const OPTION_KEY = 'presto_player_mcp';
28
29 /**
30 * Memoized list of collected ability instances, built once per request.
31 *
32 * @var Ability[]|null
33 */
34 protected $abilities = null;
35
36 /**
37 * Hook into WordPress.
38 *
39 * @return void
40 */
41 public function register() {
42 // The option and the read-only list route are always registered so the
43 // MCP settings screen loads and saves cleanly. The abilities themselves
44 // need the Abilities API (WordPress 6.9+); the UI disables the toggle and
45 // shows a "requires 6.9" note when it's missing (abilitiesSupported flag).
46 //
47 // Defer option registration to `init` (matches Settings.php) so it runs
48 // after translations load and isn't double-registered at plugin-load.
49 add_action( 'init', array( $this, 'registerOption' ) );
50 add_action( 'rest_api_init', array( $this, 'registerRestRoutes' ) );
51
52 if ( ! function_exists( 'wp_register_ability' ) ) {
53 return;
54 }
55 add_action( 'wp_abilities_api_categories_init', array( $this, 'registerCategory' ) );
56 add_action( 'wp_abilities_api_init', array( $this, 'registerAll' ) );
57 add_action( 'mcp_adapter_init', array( $this, 'registerMcpServer' ) );
58 }
59
60 /**
61 * Register the read-only REST route that powers the admin abilities list.
62 *
63 * @return void
64 */
65 public function registerRestRoutes() {
66 register_rest_route(
67 'presto-player/v1',
68 '/abilities',
69 array(
70 'methods' => 'GET',
71 'callback' => array( $this, 'getAbilitiesList' ),
72 'permission_callback' => function () {
73 return current_user_can( 'manage_options' );
74 },
75 )
76 );
77 }
78
79 /**
80 * Return the registered ability catalog (name/label/description/type/tier)
81 * plus counts, for the settings UI. Active/inactive is derived client-side
82 * from the two toggles, so this stays a pure catalog.
83 *
84 * @return \WP_REST_Response
85 */
86 public function getAbilitiesList() {
87 $items = array();
88 $present = array();
89
90 foreach ( $this->collectAbilities() as $ability ) {
91 $name = $ability->getName();
92 $present[ $name ] = true;
93 $items[] = array(
94 'name' => $name,
95 'label' => $ability->getLabel(),
96 'description' => $ability->getDescription(),
97 'type' => $this->abilityType( $ability->getAnnotations() ),
98 'tier' => ( 0 === strpos( $name, 'presto-player-pro/' ) ) ? 'pro' : 'free',
99 'available' => true,
100 'upcoming' => false,
101 );
102 }
103
104 // Display-only pro catalog: surface pro abilities even when the pro
105 // plugin is inactive so users can see what pro unlocks. These entries
106 // are never registered nor exposed as MCP tools — registerAll() and
107 // registerMcpServer() iterate collectAbilities() only, not this list.
108 foreach ( $this->proCatalog() as $entry ) {
109 if ( isset( $present[ $entry['name'] ] ) ) {
110 continue;
111 }
112 $items[] = array(
113 'name' => $entry['name'],
114 'label' => $entry['label'],
115 'description' => $entry['description'],
116 'type' => $entry['type'],
117 'tier' => 'pro',
118 'available' => false,
119 'upcoming' => false,
120 );
121 }
122
123 // Display-only roadmap catalog: presets, LMS and playlist abilities are
124 // built but held back from this release, so they show as "Upcoming" in
125 // the dashboard instead of silently disappearing from the list.
126 foreach ( $this->upcomingCatalog() as $entry ) {
127 if ( isset( $present[ $entry['name'] ] ) ) {
128 continue;
129 }
130 $items[] = array(
131 'name' => $entry['name'],
132 'label' => $entry['label'],
133 'description' => $entry['description'],
134 'type' => $entry['type'],
135 'tier' => $entry['tier'],
136 'available' => false,
137 'upcoming' => true,
138 );
139 }
140
141 $free = 0;
142 $pro = 0;
143 foreach ( $items as $item ) {
144 if ( 'pro' === $item['tier'] ) {
145 ++$pro;
146 } else {
147 ++$free;
148 }
149 }
150
151 return rest_ensure_response(
152 array(
153 'abilities' => $items,
154 'counts' => array(
155 'total' => count( $items ),
156 'free' => $free,
157 'pro' => $pro,
158 ),
159 )
160 );
161 }
162
163 /**
164 * Display-only manifest of pro abilities, shown in the dashboard list when
165 * the pro plugin is inactive (its ability classes are not loaded then, so
166 * this metadata is mirrored here). Kept in sync with the pro contract via
167 * AbilityContractTest. Never used for registration or MCP exposure.
168 *
169 * @return array<int, array{name: string, label: string, description: string, type: string}>
170 */
171 protected function proCatalog() {
172 return array(
173 array(
174 'name' => 'presto-player-pro/list-top-videos',
175 'label' => __( 'List top videos', 'presto-player' ),
176 'description' => __( 'Returns videos ordered by view count for an optional date range.', 'presto-player' ),
177 'type' => 'read',
178 ),
179 array(
180 'name' => 'presto-player-pro/list-top-viewers',
181 'label' => __( 'List top viewers', 'presto-player' ),
182 'description' => __( 'Returns users ordered by total view count for an optional date range.', 'presto-player' ),
183 'type' => 'read',
184 ),
185 array(
186 'name' => 'presto-player-pro/get-video-views',
187 'label' => __( 'Get video views', 'presto-player' ),
188 'description' => __( 'Returns the total number of views for one video over an optional date range.', 'presto-player' ),
189 'type' => 'read',
190 ),
191 array(
192 'name' => 'presto-player-pro/get-video-drop-off',
193 'label' => __( 'Get video drop-off curve', 'presto-player' ),
194 'description' => __( 'Returns retention buckets for a video — how many viewers were still watching at each timeline point.', 'presto-player' ),
195 'type' => 'read',
196 ),
197 array(
198 'name' => 'presto-player-pro/captions-translate',
199 'label' => __( 'Translate captions', 'presto-player' ),
200 'description' => __( 'Translates an existing transcript on a Bunny video into one or more target languages.', 'presto-player' ),
201 'type' => 'write',
202 ),
203 array(
204 'name' => 'presto-player-pro/upload-bunny-video',
205 'label' => __( 'Create Bunny Stream video', 'presto-player' ),
206 'description' => __( 'Uploads a video to Bunny Stream from a Media Library attachment, a file URL, or raw base64 bytes, and returns an embeddable Media Hub video.', 'presto-player' ),
207 'type' => 'write',
208 ),
209 array(
210 'name' => 'presto-player-pro/create-video-bunny',
211 'label' => __( 'Add Bunny video', 'presto-player' ),
212 'description' => __( 'Adds an existing Bunny Stream video (by GUID) to the Media Hub and returns a ready-to-embed shortcode and block.', 'presto-player' ),
213 'type' => 'write',
214 ),
215 array(
216 'name' => 'presto-player-pro/list-bunny-collections',
217 'label' => __( 'List Bunny collections', 'presto-player' ),
218 'description' => __( 'Returns all Bunny Stream collections for the active library.', 'presto-player' ),
219 'type' => 'read',
220 ),
221 );
222 }
223
224 /**
225 * Display-only manifest of abilities that are implemented but deferred to a
226 * later release. Listed in the dashboard with an "Upcoming" badge so the
227 * roadmap is visible; never registered and never exposed as MCP tools.
228 *
229 * @return array<int, array{name: string, label: string, description: string, type: string, tier: string}>
230 */
231 protected function upcomingCatalog() {
232 return array(
233 array(
234 'name' => 'presto-player/get-video-attributes',
235 'label' => __( 'Get video attributes', 'presto-player' ),
236 'description' => __( 'Returns how one video is configured in the editor — its preset, poster, caption tracks, overlays and playback toggles.', 'presto-player' ),
237 'type' => 'read',
238 'tier' => 'free',
239 ),
240 array(
241 'name' => 'presto-player/update-video-attributes',
242 'label' => __( 'Update video attributes', 'presto-player' ),
243 'description' => __( 'Changes how one video is configured — assign a preset, set the poster, replace caption tracks or overlays, or flip the playback toggles.', 'presto-player' ),
244 'type' => 'destructive',
245 'tier' => 'free',
246 ),
247 array(
248 'name' => 'presto-player/list-video-tags',
249 'label' => __( 'List video tags', 'presto-player' ),
250 'description' => __( 'Returns the Media Tags in the library with how many videos each one holds.', 'presto-player' ),
251 'type' => 'read',
252 'tier' => 'free',
253 ),
254 array(
255 'name' => 'presto-player/create-video-tag',
256 'label' => __( 'Create video tag', 'presto-player' ),
257 'description' => __( 'Creates a Media Tag so videos can be tagged with it later. Calling it again returns the existing tag instead of a duplicate.', 'presto-player' ),
258 'type' => 'write',
259 'tier' => 'free',
260 ),
261 array(
262 'name' => 'presto-player/rename-video-tag',
263 'label' => __( 'Rename video tag', 'presto-player' ),
264 'description' => __( 'Renames a Media Tag, keeping it attached to every video that already has it.', 'presto-player' ),
265 'type' => 'write',
266 'tier' => 'free',
267 ),
268 array(
269 'name' => 'presto-player/delete-video-tag',
270 'label' => __( 'Delete video tag', 'presto-player' ),
271 'description' => __( 'Deletes a Media Tag, or folds it into another one. The videos themselves are never touched.', 'presto-player' ),
272 'type' => 'destructive',
273 'tier' => 'free',
274 ),
275 array(
276 'name' => 'presto-player/list-presets',
277 'label' => __( 'List presets', 'presto-player' ),
278 'description' => __( 'Returns every player preset with its id, name and settings.', 'presto-player' ),
279 'type' => 'read',
280 'tier' => 'free',
281 ),
282 array(
283 'name' => 'presto-player/get-preset',
284 'label' => __( 'Get preset', 'presto-player' ),
285 'description' => __( 'Returns one player preset by id, including all of its settings.', 'presto-player' ),
286 'type' => 'read',
287 'tier' => 'free',
288 ),
289 array(
290 'name' => 'presto-player/create-preset',
291 'label' => __( 'Create preset', 'presto-player' ),
292 'description' => __( 'Creates a new player preset and returns it.', 'presto-player' ),
293 'type' => 'write',
294 'tier' => 'free',
295 ),
296 array(
297 'name' => 'presto-player/update-preset',
298 'label' => __( 'Update preset', 'presto-player' ),
299 'description' => __( 'Updates the name and settings of an existing player preset.', 'presto-player' ),
300 'type' => 'write',
301 'tier' => 'free',
302 ),
303 array(
304 'name' => 'presto-player/delete-preset',
305 'label' => __( 'Delete preset', 'presto-player' ),
306 'description' => __( 'Permanently deletes a player preset.', 'presto-player' ),
307 'type' => 'destructive',
308 'tier' => 'free',
309 ),
310 array(
311 'name' => 'presto-player-pro/captions-list',
312 'label' => __( 'List caption tracks', 'presto-player' ),
313 'description' => __( 'Returns the caption tracks on a video with their languages, so you can check what is already captioned.', 'presto-player' ),
314 'type' => 'read',
315 'tier' => 'pro',
316 ),
317 array(
318 'name' => 'presto-player-pro/captions-upload',
319 'label' => __( 'Upload a caption track', 'presto-player' ),
320 'description' => __( 'Puts a WebVTT caption file on a self-hosted or Bunny video.', 'presto-player' ),
321 'type' => 'write',
322 'tier' => 'pro',
323 ),
324 array(
325 'name' => 'presto-player-pro/captions-bulk-upload',
326 'label' => __( 'Bulk upload caption tracks', 'presto-player' ),
327 'description' => __( 'Puts a WebVTT caption file on each of several videos in one call, reporting on every item individually.', 'presto-player' ),
328 'type' => 'write',
329 'tier' => 'pro',
330 ),
331 array(
332 'name' => 'presto-player-pro/captions-transcribe-bunny',
333 'label' => __( 'Transcribe Bunny video', 'presto-player' ),
334 'description' => __( 'Asks Bunny.net to auto-transcribe a video. Requires an active Bunny Stream library + transcription enabled.', 'presto-player' ),
335 'type' => 'write',
336 'tier' => 'pro',
337 ),
338 array(
339 'name' => 'presto-player-pro/captions-bulk-translate-collection',
340 'label' => __( 'Bulk translate collection captions', 'presto-player' ),
341 'description' => __( 'Iterates videos in a Bunny collection and queues transcription + translation into N target languages for each.', 'presto-player' ),
342 'type' => 'write',
343 'tier' => 'pro',
344 ),
345 array(
346 'name' => 'presto-player/link-video-to-learndash-step',
347 'label' => __( 'Link video to LearnDash step', 'presto-player' ),
348 'description' => __( 'Embeds a Presto Player video into a LearnDash lesson or topic. Requires LearnDash to be active.', 'presto-player' ),
349 'type' => 'write',
350 'tier' => 'free',
351 ),
352 array(
353 'name' => 'presto-player-pro/create-playlist',
354 'label' => __( 'Build playlist from a list of videos', 'presto-player' ),
355 'description' => __( 'Builds a playlist from an ordered list of videos, publishes it as a page, and returns the page URL.', 'presto-player' ),
356 'type' => 'write',
357 'tier' => 'pro',
358 ),
359 array(
360 'name' => 'presto-player-pro/update-playlist',
361 'label' => __( 'Reorder or replace playlist videos', 'presto-player' ),
362 'description' => __( 'Sets the videos on an existing playlist to a new ordered list — reorder, add, or remove videos.', 'presto-player' ),
363 'type' => 'write',
364 'tier' => 'pro',
365 ),
366 array(
367 'name' => 'presto-player-pro/create-smart-playlist',
368 'label' => __( 'Build smart playlist', 'presto-player' ),
369 'description' => __( 'Builds a playlist from a rule (most-watched, by tag, etc.), publishes it as a page, and returns the page URL.', 'presto-player' ),
370 'type' => 'write',
371 'tier' => 'pro',
372 ),
373 array(
374 'name' => 'presto-player-pro/course-create-from-collection',
375 'label' => __( 'Build LMS course from Bunny collection', 'presto-player' ),
376 'description' => __( 'Creates a LearnDash course and a lesson for every video in a Bunny collection. Requires LearnDash to be active.', 'presto-player' ),
377 'type' => 'write',
378 'tier' => 'pro',
379 ),
380 );
381 }
382
383 /**
384 * Map an ability's annotations to a coarse type for the UI badge.
385 *
386 * @param array<string, bool> $annotations Ability annotations.
387 * @return string One of read|write|destructive.
388 */
389 protected function abilityType( $annotations ) {
390 if ( ! empty( $annotations['destructive'] ) ) {
391 return 'destructive';
392 }
393 if ( ! empty( $annotations['readonly'] ) ) {
394 return 'read';
395 }
396 return 'write';
397 }
398
399 /**
400 * Register the presto-player category on the correct early hook.
401 *
402 * Runs regardless of the master toggle so the category exists in the
403 * admin UI even when abilities are disabled.
404 *
405 * @return void
406 */
407 public function registerCategory() {
408 wp_register_ability_category(
409 'presto-player',
410 array(
411 'label' => __( 'Presto Player', 'presto-player' ),
412 'description' => __( 'Video and audio player management abilities.', 'presto-player' ),
413 )
414 );
415 }
416
417 /**
418 * Register every gated ability.
419 *
420 * @return void
421 */
422 public function registerAll() {
423 if ( ! $this->isEnabled( 'enabled' ) ) {
424 return;
425 }
426
427 foreach ( $this->collectAbilities() as $ability ) {
428 if ( ! $this->shouldRegister( $ability ) ) {
429 continue;
430 }
431 wp_register_ability( $ability->getName(), $ability->getConfig() );
432 }
433 }
434
435 /**
436 * Build the ability list. Pro and 3rd parties append via filter.
437 *
438 * @return Ability[]
439 */
440 protected function collectAbilities() {
441 if ( null !== $this->abilities ) {
442 return $this->abilities;
443 }
444
445 $abilities = array(
446 new Abilities\GetSettingsAbility(),
447 new Abilities\UpdateSettingsAbility(),
448 new Abilities\CreateYouTubeVideoAbility(),
449 new Abilities\CreateVimeoVideoAbility(),
450 new Abilities\CreateSelfHostedVideoAbility(),
451 new Abilities\GetVideoAbility(),
452 new Abilities\ListVideosAbility(),
453 new Abilities\UpdateVideoAbility(),
454 new Abilities\DeleteVideoAbility(),
455 new Abilities\GetVideoShortcodeAbility(),
456 new Abilities\ChaptersListAbility(),
457 new Abilities\ChaptersSaveAbility(),
458 new Abilities\ChaptersGenerateFromCaptionsAbility(),
459 );
460
461 /**
462 * Filters the list of ability instances to register.
463 *
464 * Pro plugins and third parties append their own Ability
465 * instances here.
466 *
467 * @param Ability[] $abilities
468 */
469 $this->abilities = apply_filters( 'presto_player_abilities', $abilities );
470
471 return $this->abilities;
472 }
473
474 /**
475 * Annotation-driven gating: readonly abilities register when enabled;
476 * write and destructive abilities require the allow_changes toggle.
477 *
478 * @param Ability $ability Ability instance.
479 * @return bool
480 */
481 protected function shouldRegister( Ability $ability ) {
482 $annotations = $ability->getAnnotations();
483 $readonly = ! empty( $annotations['readonly'] );
484
485 if ( $readonly ) {
486 return true;
487 }
488
489 return $this->isEnabled( 'allow_changes' );
490 }
491
492 /**
493 * Look up one toggle.
494 *
495 * @param string $key One of "enabled", "allow_changes".
496 * @return bool
497 */
498 protected function isEnabled( $key ) {
499 $option = get_option( self::OPTION_KEY, array() );
500 return ! empty( $option[ $key ] );
501 }
502
503 /**
504 * Register the two-toggle option so /wp/v2/settings exposes it.
505 *
506 * Public because it is hooked to `init` (WordPress invokes it as a callback).
507 *
508 * @return void
509 */
510 public function registerOption() {
511 register_setting(
512 'presto_player',
513 self::OPTION_KEY,
514 array(
515 'type' => 'object',
516 'show_in_rest' => array(
517 'schema' => array(
518 'type' => 'object',
519 'additionalProperties' => false,
520 'properties' => array(
521 'enabled' => array( 'type' => 'boolean' ),
522 'allow_changes' => array( 'type' => 'boolean' ),
523 ),
524 ),
525 ),
526 'default' => array(
527 'enabled' => false,
528 'allow_changes' => false,
529 ),
530 'sanitize_callback' => array( $this, 'sanitizeOption' ),
531 )
532 );
533 }
534
535 /**
536 * Coerce option values to clean booleans.
537 *
538 * @param mixed $value Raw incoming value.
539 * @return array<string, bool>
540 */
541 public function sanitizeOption( $value ) {
542 return array(
543 'enabled' => ! empty( $value['enabled'] ),
544 'allow_changes' => ! empty( $value['allow_changes'] ),
545 );
546 }
547
548 /**
549 * Register the Presto Player MCP server with the MCP Adapter plugin.
550 *
551 * Hooked to "mcp_adapter_init". Bails when the master abilities toggle is
552 * off. Derives the tool list from the same collected, gated ability set
553 * registerAll() uses and exposes them via an HTTP transport at
554 * /wp-json/presto-player/v1/mcp.
555 *
556 * @param object $adapter MCP Adapter instance supplied by the plugin.
557 * @return void
558 */
559 public function registerMcpServer( $adapter ) {
560 if ( ! $this->isEnabled( 'enabled' ) ) {
561 return;
562 }
563
564 $tools = array();
565
566 foreach ( $this->collectAbilities() as $ability ) {
567 if ( ! $this->shouldRegister( $ability ) ) {
568 continue;
569 }
570 $tools[] = $ability->getName();
571 }
572
573 if ( empty( $tools ) ) {
574 return;
575 }
576
577 if ( class_exists( '\WP\MCP\Transport\HttpTransport' ) ) {
578 $transport_class = 'WP\\MCP\\Transport\\HttpTransport';
579 } elseif ( class_exists( '\WP\MCP\Transport\Http\RestTransport' ) ) {
580 $transport_class = 'WP\\MCP\\Transport\\Http\\RestTransport';
581 } else {
582 return;
583 }
584
585 // Guard against MCP-adapter API drift (the create_server signature is
586 // version-specific): degrade gracefully instead of fataling on a hook.
587 try {
588 if ( ! is_callable( array( $adapter, 'create_server' ) ) ) {
589 return;
590 }
591 $adapter->create_server(
592 'presto-player',
593 'presto-player/v1',
594 'mcp',
595 __( 'Presto Player MCP Server', 'presto-player' ),
596 __( 'Presto Player MCP Server for video, captions, and analytics management.', 'presto-player' ),
597 defined( 'PRESTO_PLAYER_VERSION' ) ? PRESTO_PLAYER_VERSION : '1.0.0',
598 array( $transport_class ),
599 'WP\\MCP\\Infrastructure\\ErrorHandling\\ErrorLogMcpErrorHandler',
600 'WP\\MCP\\Infrastructure\\Observability\\NullMcpObservabilityHandler',
601 $tools,
602 array(),
603 array(),
604 // Without this the adapter falls back to a bare "read" check, which
605 // let a Subscriber's Application Password enumerate every tool
606 // schema. `edit_posts` is the floor the OAuth authorize flow grants
607 // at (Endpoints::requiredCapabilityForScopes()), so gating harder
608 // than that — on manage_options, say — locks an Editor or Author
609 // out of the session their own grant just authorised. Execution
610 // stays gated per ability on top of this.
611 function () {
612 return current_user_can( 'edit_posts' );
613 }
614 );
615 } catch ( \Throwable $e ) {
616 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
617 error_log( 'Presto Player MCP: create_server failed: ' . $e->getMessage() ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
618 }
619 }
620 }
621 }
622