PluginProbe
Extendify / 3.2.2
Extendify v3.2.2
3.2.2 3.2.1 3.2.0 3.1.6 3.1.5 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 3.0.6 3.0.5 3.0.4 trunk 0.1.0 0.10.0 0.10.1 0.10.2 0.11.0 0.11.1 0.2.0 0.3.0 0.3.1 0.4.0 0.5.0 All 128 releases
extendify / app / Mcp / Allowed.php

Allowed.php in Extendify 3.2.2, at app/Mcp/Allowed.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 /**
4 * What a connection may reach, and how far the partner opened it.
5 */
6
7 namespace Extendify\Mcp;
8
9 defined('ABSPATH') || die('No direct access.');
10
11 use Extendify\PartnerData;
12
13 /**
14 * The partner opens the surface with two dials and two lists, all sent by the
15 * backend. An install that has never heard back from it reaches nothing.
16 *
17 * mcpConfig holds the dials, read and write, each none, some or all. Read is
18 * the off switch: none there turns connections off and closes writes with it.
19 * At all, nothing is checked against a list.
20 *
21 * At some, a list names what is reachable, spelled as the tool or ability is
22 * named. mcpReadList holds our read tools by name (list_posts) and ability
23 * namespaces as ability:<namespace>, which admits every ability there that
24 * annotates itself readonly. mcpWriteList holds our write tools by name and
25 * abilities one at a time as <namespace>/<ability>. A write ability also needs
26 * its namespace on the read list: readonly is the plugin's own claim, so
27 * nothing a plugin registers reaches a connection until the read list names it.
28 *
29 * A few tools skip the lists and are offered whenever connections are on.
30 */
31 class Allowed
32 {
33 // phpcs:disable PSR12.Properties.ConstantVisibility.NotFound
34 const NONE = 'none';
35
36 const SOME = 'some';
37
38 const ALL = 'all';
39
40 const NAMESPACE_PREFIX = 'ability:';
41
42 const ALWAYS_OFFERED = ['request_feature'];
43 // phpcs:enable PSR12.Properties.ConstantVisibility.NotFound
44
45 /**
46 * @return string
47 */
48 public static function reads()
49 {
50 return self::dial('read');
51 }
52
53 /**
54 * @return string
55 */
56 public static function writes()
57 {
58 return self::reads() === self::NONE ? self::NONE : self::dial('write');
59 }
60
61 /**
62 * Whether the partner turned on a write that a connection could reach.
63 *
64 * @return boolean
65 */
66 public static function writable()
67 {
68 if (self::writes() === self::ALL) {
69 return true;
70 }
71
72 if (self::writes() !== self::SOME) {
73 return false;
74 }
75
76 foreach (self::listed('mcpWriteList') as $entry) {
77 if (strpos($entry, '/') === false || self::readsNamespace(self::namespaceOf($entry))) {
78 return true;
79 }
80 }
81
82 return false;
83 }
84
85 /**
86 * @param string $name - The ability name, namespace first.
87 * @param array $annotations - The ability's meta annotations.
88 * @return string|null - The grant the ability needs, or null when no connection reaches it.
89 */
90 public static function forAbility($name, array $annotations = [])
91 {
92 if (!self::readsNamespace(self::namespaceOf($name))) {
93 return null;
94 }
95
96 if (!empty($annotations['readonly'])) {
97 return Grants::READ;
98 }
99
100 return self::forTool($name, Grants::WRITE) ? Grants::WRITE : null;
101 }
102
103 /**
104 * @param string $name - The tool or ability name.
105 * @param string $mode - The grant the tool declares it needs.
106 * @return boolean
107 */
108 public static function forTool($name, $mode = Grants::WRITE)
109 {
110 if (self::reads() !== self::NONE && in_array((string) $name, self::ALWAYS_OFFERED, true)) {
111 return true;
112 }
113
114 $reach = $mode === Grants::WRITE ? self::writes() : self::reads();
115 if ($reach === self::ALL) {
116 return true;
117 }
118
119 $list = $mode === Grants::WRITE ? 'mcpWriteList' : 'mcpReadList';
120
121 return $reach === self::SOME && in_array((string) $name, self::listed($list), true);
122 }
123
124 /**
125 * @param string $namespace - The ability's namespace.
126 * @return boolean
127 */
128 private static function readsNamespace($namespace)
129 {
130 if (self::reads() === self::ALL) {
131 return true;
132 }
133
134 return self::reads() === self::SOME
135 && in_array(self::NAMESPACE_PREFIX . $namespace, self::listed('mcpReadList'), true);
136 }
137
138 /**
139 * @param string $name - An ability name, or a write list entry naming one.
140 * @return string
141 */
142 private static function namespaceOf($name)
143 {
144 return explode('/', (string) $name)[0];
145 }
146
147 /**
148 * @param string $side - read or write.
149 * @return string
150 */
151 private static function dial($side)
152 {
153 $config = PartnerData::setting('mcpConfig');
154 $reach = is_array($config) ? ($config[$side] ?? '') : '';
155
156 return in_array($reach, [self::SOME, self::ALL], true) ? $reach : self::NONE;
157 }
158
159 /**
160 * @param string $setting - The partner setting holding a list of names.
161 * @return array
162 */
163 private static function listed($setting)
164 {
165 $named = PartnerData::setting($setting);
166 if (!is_array($named)) {
167 return [];
168 }
169
170 // A hand-typed entry may carry a leading slash that no tool or ability name has.
171 return array_map(function ($entry) {
172 return ltrim((string) $entry, '/');
173 }, $named);
174 }
175 }
176