1 <?php
  2 
  3 /**
  4  * This is a base class for all functions which a component may provide for public use.
  5  * Those function will be called automatically by the core.
  6  * Inside an api function you might check the preconditions which have to be met (permissions, etc.)
  7  * and forward the call to an underlying service which does the actual job.
  8  *
  9  * There can only be one rex_api_function called per request, but not every request must have an api function.
 10  *
 11  * The classname of a possible implementation must start with "rex_api".
 12  *
 13  * A api function may also be called by an ajax-request.
 14  * In fact there might be ajax-requests which do nothing more than triggering an api function.
 15  *
 16  * The api functions return meaningfull error messages which the caller may display to the end-user.
 17  *
 18  * @author staabm
 19  *
 20  * @package redaxo\core
 21  */
 22 abstract class rex_api_function
 23 {
 24     use rex_factory_trait;
 25 
 26     const REQ_CALL_PARAM = 'rex-api-call';
 27     const REQ_RESULT_PARAM = 'rex-api-result';
 28 
 29     /**
 30      * Flag, indicating if this api function may be called from the frontend. False by default.
 31      *
 32      * @var bool
 33      */
 34     protected $published = false;
 35 
 36     /**
 37      * The result of the function call.
 38      *
 39      * @var rex_api_result
 40      */
 41     protected $result = null;
 42 
 43     /**
 44      * This method have to be overriden by a subclass and does all logic which the api function represents.
 45      *
 46      * In the first place this method may retrieve and validate parameters from the request.
 47      * Afterwards the actual logic should be executed.
 48      *
 49      * This function may also throw exceptions e.g. in case when permissions are missing or the provided parameters are invalid.
 50      *
 51      * @return rex_api_result The result of the api-function
 52      */
 53     abstract public function execute();
 54 
 55     /**
 56      * The api function which is bound to the current request.
 57      *
 58      * @var rex_api_function
 59      */
 60     private static $instance;
 61 
 62     /**
 63      * Returns the api function instance which is bound to the current request, or null if no api function was bound.
 64      *
 65      * @throws rex_exception
 66      *
 67      * @return self
 68      */
 69     public static function factory()
 70     {
 71         if (self::$instance) {
 72             return self::$instance;
 73         }
 74 
 75         $api = rex_request(self::REQ_CALL_PARAM, 'string');
 76 
 77         if ($api) {
 78             $apiClass = 'rex_api_' . $api;
 79             if (class_exists($apiClass)) {
 80                 $apiImpl = new $apiClass();
 81                 if ($apiImpl instanceof self) {
 82                     self::$instance = $apiImpl;
 83                     return $apiImpl;
 84                 }
 85                 throw new rex_exception('$apiClass is expected to define a subclass of rex_api_function, "'. $apiClass .'" given!');
 86             }
 87             throw new rex_exception('$apiClass "' . $apiClass . '" not found!');
 88         }
 89 
 90         return null;
 91     }
 92 
 93     /**
 94      * Returns an array containing the `rex-api-call` and `_csrf_token` params.
 95      *
 96      * The method must be called on sub classes.
 97      *
 98      * @return array
 99      */
100     public static function getUrlParams()
101     {
102         $class = static::class;
103 
104         if (self::class === $class) {
105             throw new BadMethodCallException(__FUNCTION__.' must be called on subclasses of "'.self::class.'".');
106         }
107 
108         // remove the `rex_api_` prefix
109         $name = substr($class, 8);
110 
111         return [self::REQ_CALL_PARAM => $name, rex_csrf_token::PARAM => rex_csrf_token::factory($class)->getValue()];
112     }
113 
114     /**
115      * Returns the hidden fields for `rex-api-call` and `_csrf_token`.
116      *
117      * The method must be called on sub classes.
118      *
119      * @return string
120      */
121     public static function getHiddenFields()
122     {
123         $class = static::class;
124 
125         if (self::class === $class) {
126             throw new BadMethodCallException(__FUNCTION__.' must be called on subclasses of "'.self::class.'".');
127         }
128 
129         // remove the `rex_api_` prefix
130         $name = substr($class, 8);
131 
132         return sprintf('<input type="hidden" name="%s" value="%s"/>', self::REQ_CALL_PARAM, rex_escape($name))
133             .rex_csrf_token::factory($class)->getHiddenField();
134     }
135 
136     /**
137      * checks whether an api function is bound to the current requests. If so, so the api function will be executed.
138      */
139     public static function handleCall()
140     {
141         if (static::hasFactoryClass()) {
142             return static::callFactoryClass(__FUNCTION__, func_get_args());
143         }
144 
145         $apiFunc = self::factory();
146 
147         if ($apiFunc != null) {
148             if ($apiFunc->published !== true) {
149                 if (rex::isBackend() !== true) {
150                     throw new rex_http_exception(
151                         new rex_api_exception('the api function ' . get_class($apiFunc) . ' is not published, therefore can only be called from the backend!'),
152                         rex_response::HTTP_FORBIDDEN
153                     );
154                 }
155 
156                 if (!rex::getUser()) {
157                     throw new rex_http_exception(
158                         new rex_api_exception('missing backend session to call api function ' . get_class($apiFunc) . '!'),
159                         rex_response::HTTP_UNAUTHORIZED
160                     );
161                 }
162             }
163 
164             $urlResult = rex_get(self::REQ_RESULT_PARAM, 'string');
165             if ($urlResult) {
166                 // take over result from url and do not execute the apiFunc
167                 $result = rex_api_result::fromJSON($urlResult);
168                 $apiFunc->result = $result;
169             } else {
170                 if ($apiFunc->requiresCsrfProtection() && !rex_csrf_token::factory(get_class($apiFunc))->isValid()) {
171                     $result = new rex_api_result(false, rex_i18n::msg('csrf_token_invalid'));
172                     $apiFunc->result = $result;
173 
174                     return;
175                 }
176 
177                 try {
178                     $result = $apiFunc->execute();
179 
180                     if (!($result instanceof rex_api_result)) {
181                         throw new rex_exception('Illegal result returned from api-function ' . rex_get(self::REQ_CALL_PARAM) .'. Expected a instance of rex_api_result but got "'. (is_object($result) ? get_class($result) : gettype($result)) .'".');
182                     }
183 
184                     $apiFunc->result = $result;
185                     if ($result->requiresReboot()) {
186                         $context = rex_context::fromGet();
187                         // add api call result to url
188                         $context->setParam(self::REQ_RESULT_PARAM, $result->toJSON());
189                         // and redirect to SELF for reboot
190                         rex_response::sendRedirect($context->getUrl([], false));
191                     }
192                 } catch (rex_api_exception $e) {
193                     $message = $e->getMessage();
194                     $result = new rex_api_result(false, $message);
195                     $apiFunc->result = $result;
196                 }
197             }
198         }
199     }
200 
201     public static function hasMessage()
202     {
203         $apiFunc = self::factory();
204         $result = $apiFunc->getResult();
205         return $result && null !== $result->getMessage();
206     }
207 
208     public static function getMessage($formatted = true)
209     {
210         $apiFunc = self::factory();
211         $message = '';
212         if ($apiFunc) {
213             $apiResult = $apiFunc->getResult();
214             if ($apiResult) {
215                 if ($formatted) {
216                     $message = $apiResult->getFormattedMessage();
217                 } else {
218                     $message = $apiResult->getMessage();
219                 }
220             }
221         }
222         // return a placeholder which can later be used by ajax requests to display messages
223         return '<div id="rex-message-container">' . $message . '</div>';
224     }
225 
226     protected function __construct()
227     {
228         // NOOP
229     }
230 
231     /**
232      * @return rex_api_result
233      */
234     public function getResult()
235     {
236         return $this->result;
237     }
238 
239     /**
240      * Csrf validation is disabled by default for backwards compatiblity reasons. This default will change in a future version.
241      * Prepare all your api functions to work with csrf token by using your-api-class::getUrlParams()/getHiddenFields(), otherwise they will stop work.
242      *
243      * @return bool
244      */
245     protected function requiresCsrfProtection()
246     {
247         return false;
248     }
249 }
250 
251 /**
252  * Class representing the result of a api function call.
253  *
254  * @author staabm
255  *
256  * @see rex_api_function
257  *
258  * @package redaxo\core
259  */
260 class rex_api_result
261 {
262     /**
263      * Flag indicating if the api function was executed successfully.
264      *
265      * @var bool
266      */
267     private $succeeded = false;
268 
269     /**
270      * Optional message which will be visible to the end-user.
271      *
272      * @var string
273      */
274     private $message;
275 
276     /**
277      * Flag indicating whether the result of this api call needs to be rendered in a new sub-request.
278      * This is required in rare situations, when some low-level data was changed by the api-function.
279      *
280      * @var bool
281      */
282     private $requiresReboot;
283 
284     public function __construct($succeeded, $message = null)
285     {
286         $this->succeeded = $succeeded;
287         $this->message = $message;
288     }
289 
290     public function setRequiresReboot($requiresReboot)
291     {
292         $this->requiresReboot = $requiresReboot;
293     }
294 
295     public function requiresReboot()
296     {
297         return $this->requiresReboot;
298     }
299 
300     public function getFormattedMessage()
301     {
302         if (null === $this->message) {
303             return null;
304         }
305 
306         if ($this->isSuccessfull()) {
307             return rex_view::success($this->message);
308         }
309         return rex_view::error($this->message);
310     }
311 
312     /**
313      * Returns end-user friendly statusmessage.
314      *
315      * @return string a statusmessage
316      */
317     public function getMessage()
318     {
319         return $this->message;
320     }
321 
322     /**
323      * Returns whether the api function was executed successfully.
324      *
325      * @return bool true on success, false on error
326      */
327     public function isSuccessfull()
328     {
329         return $this->succeeded;
330     }
331 
332     public function toJSON()
333     {
334         $json = new stdClass();
335         foreach ($this as $key => $value) {
336             $json->$key = $value;
337         }
338         return json_encode($json);
339     }
340 
341     public static function fromJSON($json)
342     {
343         $result = new self(true);
344         $json = json_decode($json, true);
345         foreach ($json as $key => $value) {
346             $result->$key = $value;
347         }
348         return $result;
349     }
350 }
351 
352 /**
353  * Exception-Type to indicate exceptions in an api function.
354  * The messages of this exception will be displayed to the end-user.
355  *
356  * @author staabm
357  *
358  * @see rex_api_function
359  *
360  * @package redaxo\core
361  */
362 class rex_api_exception extends rex_exception
363 {
364 }
365