xref: /dokuwiki/inc/Remote/Api.php (revision dd87735d917b53fa3e3ac66675834419ed24f832)
1*dd87735dSAndreas Gohr<?php
2*dd87735dSAndreas Gohr
3*dd87735dSAndreas Gohrnamespace dokuwiki\Remote;
4*dd87735dSAndreas Gohr
5*dd87735dSAndreas Gohruse DokuWiki_Remote_Plugin;
6*dd87735dSAndreas Gohruse Input;
7*dd87735dSAndreas Gohr
8*dd87735dSAndreas Gohr/**
9*dd87735dSAndreas Gohr * This class provides information about remote access to the wiki.
10*dd87735dSAndreas Gohr *
11*dd87735dSAndreas Gohr * == Types of methods ==
12*dd87735dSAndreas Gohr * There are two types of remote methods. The first is the core methods.
13*dd87735dSAndreas Gohr * These are always available and provided by dokuwiki.
14*dd87735dSAndreas Gohr * The other is plugin methods. These are provided by remote plugins.
15*dd87735dSAndreas Gohr *
16*dd87735dSAndreas Gohr * == Information structure ==
17*dd87735dSAndreas Gohr * The information about methods will be given in an array with the following structure:
18*dd87735dSAndreas Gohr * array(
19*dd87735dSAndreas Gohr *     'method.remoteName' => array(
20*dd87735dSAndreas Gohr *          'args' => array(
21*dd87735dSAndreas Gohr *              'type eg. string|int|...|date|file',
22*dd87735dSAndreas Gohr *          )
23*dd87735dSAndreas Gohr *          'name' => 'method name in class',
24*dd87735dSAndreas Gohr *          'return' => 'type',
25*dd87735dSAndreas Gohr *          'public' => 1/0 - method bypass default group check (used by login)
26*dd87735dSAndreas Gohr *          ['doc' = 'method documentation'],
27*dd87735dSAndreas Gohr *     )
28*dd87735dSAndreas Gohr * )
29*dd87735dSAndreas Gohr *
30*dd87735dSAndreas Gohr * plugin names are formed the following:
31*dd87735dSAndreas Gohr *   core methods begin by a 'dokuwiki' or 'wiki' followed by a . and the method name itself.
32*dd87735dSAndreas Gohr *   i.e.: dokuwiki.version or wiki.getPage
33*dd87735dSAndreas Gohr *
34*dd87735dSAndreas Gohr * plugin methods are formed like 'plugin.<plugin name>.<method name>'.
35*dd87735dSAndreas Gohr * i.e.: plugin.clock.getTime or plugin.clock_gmt.getTime
36*dd87735dSAndreas Gohr */
37*dd87735dSAndreas Gohrclass Api
38*dd87735dSAndreas Gohr{
39*dd87735dSAndreas Gohr
40*dd87735dSAndreas Gohr    /**
41*dd87735dSAndreas Gohr     * @var ApiCore
42*dd87735dSAndreas Gohr     */
43*dd87735dSAndreas Gohr    private $coreMethods = null;
44*dd87735dSAndreas Gohr
45*dd87735dSAndreas Gohr    /**
46*dd87735dSAndreas Gohr     * @var array remote methods provided by dokuwiki plugins - will be filled lazy via
47*dd87735dSAndreas Gohr     * {@see dokuwiki\Remote\RemoteAPI#getPluginMethods}
48*dd87735dSAndreas Gohr     */
49*dd87735dSAndreas Gohr    private $pluginMethods = null;
50*dd87735dSAndreas Gohr
51*dd87735dSAndreas Gohr    /**
52*dd87735dSAndreas Gohr     * @var array contains custom calls to the api. Plugins can use the XML_CALL_REGISTER event.
53*dd87735dSAndreas Gohr     * The data inside is 'custom.call.something' => array('plugin name', 'remote method name')
54*dd87735dSAndreas Gohr     *
55*dd87735dSAndreas Gohr     * The remote method name is the same as in the remote name returned by _getMethods().
56*dd87735dSAndreas Gohr     */
57*dd87735dSAndreas Gohr    private $pluginCustomCalls = null;
58*dd87735dSAndreas Gohr
59*dd87735dSAndreas Gohr    private $dateTransformation;
60*dd87735dSAndreas Gohr    private $fileTransformation;
61*dd87735dSAndreas Gohr
62*dd87735dSAndreas Gohr    /**
63*dd87735dSAndreas Gohr     * constructor
64*dd87735dSAndreas Gohr     */
65*dd87735dSAndreas Gohr    public function __construct()
66*dd87735dSAndreas Gohr    {
67*dd87735dSAndreas Gohr        $this->dateTransformation = array($this, 'dummyTransformation');
68*dd87735dSAndreas Gohr        $this->fileTransformation = array($this, 'dummyTransformation');
69*dd87735dSAndreas Gohr    }
70*dd87735dSAndreas Gohr
71*dd87735dSAndreas Gohr    /**
72*dd87735dSAndreas Gohr     * Get all available methods with remote access.
73*dd87735dSAndreas Gohr     *
74*dd87735dSAndreas Gohr     * @return array with information to all available methods
75*dd87735dSAndreas Gohr     * @throws RemoteException
76*dd87735dSAndreas Gohr     */
77*dd87735dSAndreas Gohr    public function getMethods()
78*dd87735dSAndreas Gohr    {
79*dd87735dSAndreas Gohr        return array_merge($this->getCoreMethods(), $this->getPluginMethods());
80*dd87735dSAndreas Gohr    }
81*dd87735dSAndreas Gohr
82*dd87735dSAndreas Gohr    /**
83*dd87735dSAndreas Gohr     * Call a method via remote api.
84*dd87735dSAndreas Gohr     *
85*dd87735dSAndreas Gohr     * @param string $method name of the method to call.
86*dd87735dSAndreas Gohr     * @param array $args arguments to pass to the given method
87*dd87735dSAndreas Gohr     * @return mixed result of method call, must be a primitive type.
88*dd87735dSAndreas Gohr     * @throws RemoteException
89*dd87735dSAndreas Gohr     */
90*dd87735dSAndreas Gohr    public function call($method, $args = array())
91*dd87735dSAndreas Gohr    {
92*dd87735dSAndreas Gohr        if ($args === null) {
93*dd87735dSAndreas Gohr            $args = array();
94*dd87735dSAndreas Gohr        }
95*dd87735dSAndreas Gohr        list($type, $pluginName, /* $call */) = explode('.', $method, 3);
96*dd87735dSAndreas Gohr        if ($type === 'plugin') {
97*dd87735dSAndreas Gohr            return $this->callPlugin($pluginName, $method, $args);
98*dd87735dSAndreas Gohr        }
99*dd87735dSAndreas Gohr        if ($this->coreMethodExist($method)) {
100*dd87735dSAndreas Gohr            return $this->callCoreMethod($method, $args);
101*dd87735dSAndreas Gohr        }
102*dd87735dSAndreas Gohr        return $this->callCustomCallPlugin($method, $args);
103*dd87735dSAndreas Gohr    }
104*dd87735dSAndreas Gohr
105*dd87735dSAndreas Gohr    /**
106*dd87735dSAndreas Gohr     * Check existance of core methods
107*dd87735dSAndreas Gohr     *
108*dd87735dSAndreas Gohr     * @param string $name name of the method
109*dd87735dSAndreas Gohr     * @return bool if method exists
110*dd87735dSAndreas Gohr     */
111*dd87735dSAndreas Gohr    private function coreMethodExist($name)
112*dd87735dSAndreas Gohr    {
113*dd87735dSAndreas Gohr        $coreMethods = $this->getCoreMethods();
114*dd87735dSAndreas Gohr        return array_key_exists($name, $coreMethods);
115*dd87735dSAndreas Gohr    }
116*dd87735dSAndreas Gohr
117*dd87735dSAndreas Gohr    /**
118*dd87735dSAndreas Gohr     * Try to call custom methods provided by plugins
119*dd87735dSAndreas Gohr     *
120*dd87735dSAndreas Gohr     * @param string $method name of method
121*dd87735dSAndreas Gohr     * @param array $args
122*dd87735dSAndreas Gohr     * @return mixed
123*dd87735dSAndreas Gohr     * @throws \dokuwiki\Remote\RemoteException if method not exists
124*dd87735dSAndreas Gohr     */
125*dd87735dSAndreas Gohr    private function callCustomCallPlugin($method, $args)
126*dd87735dSAndreas Gohr    {
127*dd87735dSAndreas Gohr        $customCalls = $this->getCustomCallPlugins();
128*dd87735dSAndreas Gohr        if (!array_key_exists($method, $customCalls)) {
129*dd87735dSAndreas Gohr            throw new \dokuwiki\Remote\RemoteException('Method does not exist', -32603);
130*dd87735dSAndreas Gohr        }
131*dd87735dSAndreas Gohr        $customCall = $customCalls[$method];
132*dd87735dSAndreas Gohr        return $this->callPlugin($customCall[0], $customCall[1], $args);
133*dd87735dSAndreas Gohr    }
134*dd87735dSAndreas Gohr
135*dd87735dSAndreas Gohr    /**
136*dd87735dSAndreas Gohr     * Returns plugin calls that are registered via RPC_CALL_ADD action
137*dd87735dSAndreas Gohr     *
138*dd87735dSAndreas Gohr     * @return array with pairs of custom plugin calls
139*dd87735dSAndreas Gohr     * @triggers RPC_CALL_ADD
140*dd87735dSAndreas Gohr     */
141*dd87735dSAndreas Gohr    private function getCustomCallPlugins()
142*dd87735dSAndreas Gohr    {
143*dd87735dSAndreas Gohr        if ($this->pluginCustomCalls === null) {
144*dd87735dSAndreas Gohr            $data = array();
145*dd87735dSAndreas Gohr            trigger_event('RPC_CALL_ADD', $data);
146*dd87735dSAndreas Gohr            $this->pluginCustomCalls = $data;
147*dd87735dSAndreas Gohr        }
148*dd87735dSAndreas Gohr        return $this->pluginCustomCalls;
149*dd87735dSAndreas Gohr    }
150*dd87735dSAndreas Gohr
151*dd87735dSAndreas Gohr    /**
152*dd87735dSAndreas Gohr     * Call a plugin method
153*dd87735dSAndreas Gohr     *
154*dd87735dSAndreas Gohr     * @param string $pluginName
155*dd87735dSAndreas Gohr     * @param string $method method name
156*dd87735dSAndreas Gohr     * @param array $args
157*dd87735dSAndreas Gohr     * @return mixed return of custom method
158*dd87735dSAndreas Gohr     * @throws \dokuwiki\Remote\RemoteException
159*dd87735dSAndreas Gohr     */
160*dd87735dSAndreas Gohr    private function callPlugin($pluginName, $method, $args)
161*dd87735dSAndreas Gohr    {
162*dd87735dSAndreas Gohr        $plugin = plugin_load('remote', $pluginName);
163*dd87735dSAndreas Gohr        $methods = $this->getPluginMethods();
164*dd87735dSAndreas Gohr        if (!$plugin) {
165*dd87735dSAndreas Gohr            throw new \dokuwiki\Remote\RemoteException('Method does not exist', -32603);
166*dd87735dSAndreas Gohr        }
167*dd87735dSAndreas Gohr        $this->checkAccess($methods[$method]);
168*dd87735dSAndreas Gohr        $name = $this->getMethodName($methods, $method);
169*dd87735dSAndreas Gohr        return call_user_func_array(array($plugin, $name), $args);
170*dd87735dSAndreas Gohr    }
171*dd87735dSAndreas Gohr
172*dd87735dSAndreas Gohr    /**
173*dd87735dSAndreas Gohr     * Call a core method
174*dd87735dSAndreas Gohr     *
175*dd87735dSAndreas Gohr     * @param string $method name of method
176*dd87735dSAndreas Gohr     * @param array $args
177*dd87735dSAndreas Gohr     * @return mixed
178*dd87735dSAndreas Gohr     * @throws \dokuwiki\Remote\RemoteException if method not exist
179*dd87735dSAndreas Gohr     */
180*dd87735dSAndreas Gohr    private function callCoreMethod($method, $args)
181*dd87735dSAndreas Gohr    {
182*dd87735dSAndreas Gohr        $coreMethods = $this->getCoreMethods();
183*dd87735dSAndreas Gohr        $this->checkAccess($coreMethods[$method]);
184*dd87735dSAndreas Gohr        if (!isset($coreMethods[$method])) {
185*dd87735dSAndreas Gohr            throw new RemoteException('Method does not exist', -32603);
186*dd87735dSAndreas Gohr        }
187*dd87735dSAndreas Gohr        $this->checkArgumentLength($coreMethods[$method], $args);
188*dd87735dSAndreas Gohr        return call_user_func_array(array($this->coreMethods, $this->getMethodName($coreMethods, $method)), $args);
189*dd87735dSAndreas Gohr    }
190*dd87735dSAndreas Gohr
191*dd87735dSAndreas Gohr    /**
192*dd87735dSAndreas Gohr     * Check if access should be checked
193*dd87735dSAndreas Gohr     *
194*dd87735dSAndreas Gohr     * @param array $methodMeta data about the method
195*dd87735dSAndreas Gohr     * @throws AccessDeniedException
196*dd87735dSAndreas Gohr     */
197*dd87735dSAndreas Gohr    private function checkAccess($methodMeta)
198*dd87735dSAndreas Gohr    {
199*dd87735dSAndreas Gohr        if (!isset($methodMeta['public'])) {
200*dd87735dSAndreas Gohr            $this->forceAccess();
201*dd87735dSAndreas Gohr        } else {
202*dd87735dSAndreas Gohr            if ($methodMeta['public'] == '0') {
203*dd87735dSAndreas Gohr                $this->forceAccess();
204*dd87735dSAndreas Gohr            }
205*dd87735dSAndreas Gohr        }
206*dd87735dSAndreas Gohr    }
207*dd87735dSAndreas Gohr
208*dd87735dSAndreas Gohr    /**
209*dd87735dSAndreas Gohr     * Check the number of parameters
210*dd87735dSAndreas Gohr     *
211*dd87735dSAndreas Gohr     * @param array $methodMeta data about the method
212*dd87735dSAndreas Gohr     * @param array $args
213*dd87735dSAndreas Gohr     * @throws RemoteException if wrong parameter count
214*dd87735dSAndreas Gohr     */
215*dd87735dSAndreas Gohr    private function checkArgumentLength($methodMeta, $args)
216*dd87735dSAndreas Gohr    {
217*dd87735dSAndreas Gohr        if (count($methodMeta['args']) < count($args)) {
218*dd87735dSAndreas Gohr            throw new RemoteException('Method does not exist - wrong parameter count.', -32603);
219*dd87735dSAndreas Gohr        }
220*dd87735dSAndreas Gohr    }
221*dd87735dSAndreas Gohr
222*dd87735dSAndreas Gohr    /**
223*dd87735dSAndreas Gohr     * Determine the name of the real method
224*dd87735dSAndreas Gohr     *
225*dd87735dSAndreas Gohr     * @param array $methodMeta list of data of the methods
226*dd87735dSAndreas Gohr     * @param string $method name of method
227*dd87735dSAndreas Gohr     * @return string
228*dd87735dSAndreas Gohr     */
229*dd87735dSAndreas Gohr    private function getMethodName($methodMeta, $method)
230*dd87735dSAndreas Gohr    {
231*dd87735dSAndreas Gohr        if (isset($methodMeta[$method]['name'])) {
232*dd87735dSAndreas Gohr            return $methodMeta[$method]['name'];
233*dd87735dSAndreas Gohr        }
234*dd87735dSAndreas Gohr        $method = explode('.', $method);
235*dd87735dSAndreas Gohr        return $method[count($method) - 1];
236*dd87735dSAndreas Gohr    }
237*dd87735dSAndreas Gohr
238*dd87735dSAndreas Gohr    /**
239*dd87735dSAndreas Gohr     * Perform access check for current user
240*dd87735dSAndreas Gohr     *
241*dd87735dSAndreas Gohr     * @return bool true if the current user has access to remote api.
242*dd87735dSAndreas Gohr     * @throws AccessDeniedException If remote access disabled
243*dd87735dSAndreas Gohr     */
244*dd87735dSAndreas Gohr    public function hasAccess()
245*dd87735dSAndreas Gohr    {
246*dd87735dSAndreas Gohr        global $conf;
247*dd87735dSAndreas Gohr        global $USERINFO;
248*dd87735dSAndreas Gohr        /** @var Input $INPUT */
249*dd87735dSAndreas Gohr        global $INPUT;
250*dd87735dSAndreas Gohr
251*dd87735dSAndreas Gohr        if (!$conf['remote']) {
252*dd87735dSAndreas Gohr            throw new AccessDeniedException('server error. RPC server not enabled.', -32604);
253*dd87735dSAndreas Gohr        }
254*dd87735dSAndreas Gohr        if (trim($conf['remoteuser']) == '!!not set!!') {
255*dd87735dSAndreas Gohr            return false;
256*dd87735dSAndreas Gohr        }
257*dd87735dSAndreas Gohr        if (!$conf['useacl']) {
258*dd87735dSAndreas Gohr            return true;
259*dd87735dSAndreas Gohr        }
260*dd87735dSAndreas Gohr        if (trim($conf['remoteuser']) == '') {
261*dd87735dSAndreas Gohr            return true;
262*dd87735dSAndreas Gohr        }
263*dd87735dSAndreas Gohr
264*dd87735dSAndreas Gohr        return auth_isMember($conf['remoteuser'], $INPUT->server->str('REMOTE_USER'), (array) $USERINFO['grps']);
265*dd87735dSAndreas Gohr    }
266*dd87735dSAndreas Gohr
267*dd87735dSAndreas Gohr    /**
268*dd87735dSAndreas Gohr     * Requests access
269*dd87735dSAndreas Gohr     *
270*dd87735dSAndreas Gohr     * @return void
271*dd87735dSAndreas Gohr     * @throws AccessDeniedException On denied access.
272*dd87735dSAndreas Gohr     */
273*dd87735dSAndreas Gohr    public function forceAccess()
274*dd87735dSAndreas Gohr    {
275*dd87735dSAndreas Gohr        if (!$this->hasAccess()) {
276*dd87735dSAndreas Gohr            throw new AccessDeniedException('server error. not authorized to call method', -32604);
277*dd87735dSAndreas Gohr        }
278*dd87735dSAndreas Gohr    }
279*dd87735dSAndreas Gohr
280*dd87735dSAndreas Gohr    /**
281*dd87735dSAndreas Gohr     * Collects all the methods of the enabled Remote Plugins
282*dd87735dSAndreas Gohr     *
283*dd87735dSAndreas Gohr     * @return array all plugin methods.
284*dd87735dSAndreas Gohr     * @throws RemoteException if not implemented
285*dd87735dSAndreas Gohr     */
286*dd87735dSAndreas Gohr    public function getPluginMethods()
287*dd87735dSAndreas Gohr    {
288*dd87735dSAndreas Gohr        if ($this->pluginMethods === null) {
289*dd87735dSAndreas Gohr            $this->pluginMethods = array();
290*dd87735dSAndreas Gohr            $plugins = plugin_list('remote');
291*dd87735dSAndreas Gohr
292*dd87735dSAndreas Gohr            foreach ($plugins as $pluginName) {
293*dd87735dSAndreas Gohr                /** @var DokuWiki_Remote_Plugin $plugin */
294*dd87735dSAndreas Gohr                $plugin = plugin_load('remote', $pluginName);
295*dd87735dSAndreas Gohr                if (!is_subclass_of($plugin, 'DokuWiki_Remote_Plugin')) {
296*dd87735dSAndreas Gohr                    throw new RemoteException("Plugin $pluginName does not implement DokuWiki_Remote_Plugin");
297*dd87735dSAndreas Gohr                }
298*dd87735dSAndreas Gohr
299*dd87735dSAndreas Gohr                try {
300*dd87735dSAndreas Gohr                    $methods = $plugin->_getMethods();
301*dd87735dSAndreas Gohr                } catch (\ReflectionException $e) {
302*dd87735dSAndreas Gohr                    throw new RemoteException('Automatic aggregation of available remote methods failed', 0, $e);
303*dd87735dSAndreas Gohr                }
304*dd87735dSAndreas Gohr
305*dd87735dSAndreas Gohr                foreach ($methods as $method => $meta) {
306*dd87735dSAndreas Gohr                    $this->pluginMethods["plugin.$pluginName.$method"] = $meta;
307*dd87735dSAndreas Gohr                }
308*dd87735dSAndreas Gohr            }
309*dd87735dSAndreas Gohr        }
310*dd87735dSAndreas Gohr        return $this->pluginMethods;
311*dd87735dSAndreas Gohr    }
312*dd87735dSAndreas Gohr
313*dd87735dSAndreas Gohr    /**
314*dd87735dSAndreas Gohr     * Collects all the core methods
315*dd87735dSAndreas Gohr     *
316*dd87735dSAndreas Gohr     * @param ApiCore $apiCore this parameter is used for testing. Here you can pass a non-default RemoteAPICore
317*dd87735dSAndreas Gohr     *                         instance. (for mocking)
318*dd87735dSAndreas Gohr     * @return array all core methods.
319*dd87735dSAndreas Gohr     */
320*dd87735dSAndreas Gohr    public function getCoreMethods($apiCore = null)
321*dd87735dSAndreas Gohr    {
322*dd87735dSAndreas Gohr        if ($this->coreMethods === null) {
323*dd87735dSAndreas Gohr            if ($apiCore === null) {
324*dd87735dSAndreas Gohr                $this->coreMethods = new ApiCore($this);
325*dd87735dSAndreas Gohr            } else {
326*dd87735dSAndreas Gohr                $this->coreMethods = $apiCore;
327*dd87735dSAndreas Gohr            }
328*dd87735dSAndreas Gohr        }
329*dd87735dSAndreas Gohr        return $this->coreMethods->__getRemoteInfo();
330*dd87735dSAndreas Gohr    }
331*dd87735dSAndreas Gohr
332*dd87735dSAndreas Gohr    /**
333*dd87735dSAndreas Gohr     * Transform file to xml
334*dd87735dSAndreas Gohr     *
335*dd87735dSAndreas Gohr     * @param mixed $data
336*dd87735dSAndreas Gohr     * @return mixed
337*dd87735dSAndreas Gohr     */
338*dd87735dSAndreas Gohr    public function toFile($data)
339*dd87735dSAndreas Gohr    {
340*dd87735dSAndreas Gohr        return call_user_func($this->fileTransformation, $data);
341*dd87735dSAndreas Gohr    }
342*dd87735dSAndreas Gohr
343*dd87735dSAndreas Gohr    /**
344*dd87735dSAndreas Gohr     * Transform date to xml
345*dd87735dSAndreas Gohr     *
346*dd87735dSAndreas Gohr     * @param mixed $data
347*dd87735dSAndreas Gohr     * @return mixed
348*dd87735dSAndreas Gohr     */
349*dd87735dSAndreas Gohr    public function toDate($data)
350*dd87735dSAndreas Gohr    {
351*dd87735dSAndreas Gohr        return call_user_func($this->dateTransformation, $data);
352*dd87735dSAndreas Gohr    }
353*dd87735dSAndreas Gohr
354*dd87735dSAndreas Gohr    /**
355*dd87735dSAndreas Gohr     * A simple transformation
356*dd87735dSAndreas Gohr     *
357*dd87735dSAndreas Gohr     * @param mixed $data
358*dd87735dSAndreas Gohr     * @return mixed
359*dd87735dSAndreas Gohr     */
360*dd87735dSAndreas Gohr    public function dummyTransformation($data)
361*dd87735dSAndreas Gohr    {
362*dd87735dSAndreas Gohr        return $data;
363*dd87735dSAndreas Gohr    }
364*dd87735dSAndreas Gohr
365*dd87735dSAndreas Gohr    /**
366*dd87735dSAndreas Gohr     * Set the transformer function
367*dd87735dSAndreas Gohr     *
368*dd87735dSAndreas Gohr     * @param callback $dateTransformation
369*dd87735dSAndreas Gohr     */
370*dd87735dSAndreas Gohr    public function setDateTransformation($dateTransformation)
371*dd87735dSAndreas Gohr    {
372*dd87735dSAndreas Gohr        $this->dateTransformation = $dateTransformation;
373*dd87735dSAndreas Gohr    }
374*dd87735dSAndreas Gohr
375*dd87735dSAndreas Gohr    /**
376*dd87735dSAndreas Gohr     * Set the transformer function
377*dd87735dSAndreas Gohr     *
378*dd87735dSAndreas Gohr     * @param callback $fileTransformation
379*dd87735dSAndreas Gohr     */
380*dd87735dSAndreas Gohr    public function setFileTransformation($fileTransformation)
381*dd87735dSAndreas Gohr    {
382*dd87735dSAndreas Gohr        $this->fileTransformation = $fileTransformation;
383*dd87735dSAndreas Gohr    }
384*dd87735dSAndreas Gohr}
385