[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/libraries/src/MVC/Controller/ -> ApiController.php (source)

   1  <?php
   2  
   3  /**
   4   * Joomla! Content Management System
   5   *
   6   * @copyright  (C) 2019 Open Source Matters, Inc. <https://www.joomla.org>
   7   * @license    GNU General Public License version 2 or later; see LICENSE.txt
   8   */
   9  
  10  namespace Joomla\CMS\MVC\Controller;
  11  
  12  use Joomla\CMS\Access\Exception\NotAllowed;
  13  use Joomla\CMS\Application\CMSApplication;
  14  use Joomla\CMS\Component\ComponentHelper;
  15  use Joomla\CMS\Form\Form;
  16  use Joomla\CMS\Language\Text;
  17  use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
  18  use Joomla\CMS\MVC\Model\ListModel;
  19  use Joomla\CMS\MVC\View\JsonApiView;
  20  use Joomla\CMS\Object\CMSObject;
  21  use Joomla\Input\Input;
  22  use Joomla\String\Inflector;
  23  use Tobscure\JsonApi\Exception\InvalidParameterException;
  24  
  25  // phpcs:disable PSR1.Files.SideEffects
  26  \defined('JPATH_PLATFORM') or die;
  27  // phpcs:enable PSR1.Files.SideEffects
  28  
  29  /**
  30   * Base class for a Joomla API Controller
  31   *
  32   * Controller (controllers are where you put all the actual code) Provides basic
  33   * functionality, such as rendering views (aka displaying templates).
  34   *
  35   * @since  4.0.0
  36   */
  37  class ApiController extends BaseController
  38  {
  39      /**
  40       * The content type of the item.
  41       *
  42       * @var    string
  43       * @since  4.0.0
  44       */
  45      protected $contentType;
  46  
  47      /**
  48       * The URL option for the component.
  49       *
  50       * @var    string
  51       * @since  4.0.0
  52       */
  53      protected $option;
  54  
  55      /**
  56       * The prefix to use with controller messages.
  57       *
  58       * @var    string
  59       * @since  4.0.0
  60       */
  61      protected $text_prefix;
  62  
  63      /**
  64       * The context for storing internal data, e.g. record.
  65       *
  66       * @var    string
  67       * @since  4.0.0
  68       */
  69      protected $context;
  70  
  71      /**
  72       * Items on a page
  73       *
  74       * @var  integer
  75       */
  76      protected $itemsPerPage = 20;
  77  
  78      /**
  79       * The model state to inject
  80       *
  81       * @var  CMSObject
  82       */
  83      protected $modelState;
  84  
  85      /**
  86       * Constructor.
  87       *
  88       * @param   array                $config   An optional associative array of configuration settings.
  89       *                                         Recognized key values include 'name', 'default_task', 'model_path', and
  90       *                                         'view_path' (this list is not meant to be comprehensive).
  91       * @param   MVCFactoryInterface  $factory  The factory.
  92       * @param   CMSApplication       $app      The Application for the dispatcher
  93       * @param   Input                $input    Input
  94       *
  95       * @since   4.0.0
  96       * @throws  \Exception
  97       */
  98      public function __construct($config = array(), MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
  99      {
 100          $this->modelState = new CMSObject();
 101  
 102          parent::__construct($config, $factory, $app, $input);
 103  
 104          // Guess the option as com_NameOfController
 105          if (empty($this->option)) {
 106              $this->option = ComponentHelper::getComponentName($this, $this->getName());
 107          }
 108  
 109          // Guess the \Text message prefix. Defaults to the option.
 110          if (empty($this->text_prefix)) {
 111              $this->text_prefix = strtoupper($this->option);
 112          }
 113  
 114          // Guess the context as the suffix, eg: OptionControllerContent.
 115          if (empty($this->context)) {
 116              $r = null;
 117  
 118              if (!preg_match('/(.*)Controller(.*)/i', \get_class($this), $r)) {
 119                  throw new \Exception(Text::sprintf('JLIB_APPLICATION_ERROR_GET_NAME', __METHOD__), 500);
 120              }
 121  
 122              $this->context = str_replace('\\', '', strtolower($r[2]));
 123          }
 124      }
 125  
 126      /**
 127       * Basic display of an item view
 128       *
 129       * @param   integer  $id  The primary key to display. Leave empty if you want to retrieve data from the request
 130       *
 131       * @return  static  A \JControllerLegacy object to support chaining.
 132       *
 133       * @since   4.0.0
 134       */
 135      public function displayItem($id = null)
 136      {
 137          if ($id === null) {
 138              $id = $this->input->get('id', 0, 'int');
 139          }
 140  
 141          $viewType   = $this->app->getDocument()->getType();
 142          $viewName   = $this->input->get('view', $this->default_view);
 143          $viewLayout = $this->input->get('layout', 'default', 'string');
 144  
 145          try {
 146              /** @var JsonApiView $view */
 147              $view = $this->getView(
 148                  $viewName,
 149                  $viewType,
 150                  '',
 151                  ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType]
 152              );
 153          } catch (\Exception $e) {
 154              throw new \RuntimeException($e->getMessage());
 155          }
 156  
 157          $modelName = $this->input->get('model', Inflector::singularize($this->contentType));
 158  
 159          // Create the model, ignoring request data so we can safely set the state in the request from the controller
 160          $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]);
 161  
 162          if (!$model) {
 163              throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'));
 164          }
 165  
 166          try {
 167              $modelName = $model->getName();
 168          } catch (\Exception $e) {
 169              throw new \RuntimeException($e->getMessage());
 170          }
 171  
 172          $model->setState($modelName . '.id', $id);
 173  
 174          // Push the model into the view (as default)
 175          $view->setModel($model, true);
 176  
 177          $view->document = $this->app->getDocument();
 178          $view->displayItem();
 179  
 180          return $this;
 181      }
 182  
 183      /**
 184       * Basic display of a list view
 185       *
 186       * @return  static  A \JControllerLegacy object to support chaining.
 187       *
 188       * @since   4.0.0
 189       */
 190      public function displayList()
 191      {
 192          // Assemble pagination information (using recommended JsonApi pagination notation for offset strategy)
 193          $paginationInfo = $this->input->get('page', [], 'array');
 194          $limit          = null;
 195          $offset         = null;
 196  
 197          if (\array_key_exists('offset', $paginationInfo)) {
 198              $offset = $paginationInfo['offset'];
 199              $this->modelState->set($this->context . '.limitstart', $offset);
 200          }
 201  
 202          if (\array_key_exists('limit', $paginationInfo)) {
 203              $limit = $paginationInfo['limit'];
 204              $this->modelState->set($this->context . '.list.limit', $limit);
 205          }
 206  
 207          $viewType   = $this->app->getDocument()->getType();
 208          $viewName   = $this->input->get('view', $this->default_view);
 209          $viewLayout = $this->input->get('layout', 'default', 'string');
 210  
 211          try {
 212              /** @var JsonApiView $view */
 213              $view = $this->getView(
 214                  $viewName,
 215                  $viewType,
 216                  '',
 217                  ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType]
 218              );
 219          } catch (\Exception $e) {
 220              throw new \RuntimeException($e->getMessage());
 221          }
 222  
 223          $modelName = $this->input->get('model', $this->contentType);
 224  
 225          /** @var ListModel $model */
 226          $model = $this->getModel($modelName, '', ['ignore_request' => true, 'state' => $this->modelState]);
 227  
 228          if (!$model) {
 229              throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'));
 230          }
 231  
 232          // Push the model into the view (as default)
 233          $view->setModel($model, true);
 234  
 235          if ($offset) {
 236              $model->setState('list.start', $offset);
 237          }
 238  
 239          /**
 240           * Sanity check we don't have too much data being requested as regularly in html we automatically set it back to
 241           * the last page of data. If there isn't a limit start then set
 242           */
 243          if ($limit) {
 244              $model->setState('list.limit', $limit);
 245          } else {
 246              $model->setState('list.limit', $this->itemsPerPage);
 247          }
 248  
 249          if (!is_null($offset) && $offset > $model->getTotal()) {
 250              throw new Exception\ResourceNotFound();
 251          }
 252  
 253          $view->document = $this->app->getDocument();
 254  
 255          $view->displayList();
 256  
 257          return $this;
 258      }
 259  
 260      /**
 261       * Removes an item.
 262       *
 263       * @param   integer  $id  The primary key to delete item.
 264       *
 265       * @return  void
 266       *
 267       * @since   4.0.0
 268       */
 269      public function delete($id = null)
 270      {
 271          if (!$this->app->getIdentity()->authorise('core.delete', $this->option)) {
 272              throw new NotAllowed('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED', 403);
 273          }
 274  
 275          if ($id === null) {
 276              $id = $this->input->get('id', 0, 'int');
 277          }
 278  
 279          $modelName = $this->input->get('model', Inflector::singularize($this->contentType));
 280  
 281          /** @var \Joomla\CMS\MVC\Model\AdminModel $model */
 282          $model = $this->getModel($modelName, '', ['ignore_request' => true]);
 283  
 284          if (!$model) {
 285              throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'));
 286          }
 287  
 288          // Remove the item.
 289          if (!$model->delete($id)) {
 290              if ($model->getError() !== false) {
 291                  throw new \RuntimeException($model->getError(), 500);
 292              }
 293  
 294              throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_DELETE'), 500);
 295          }
 296  
 297          $this->app->setHeader('status', 204);
 298      }
 299  
 300      /**
 301       * Method to add a new record.
 302       *
 303       * @return  void
 304       *
 305       * @since   4.0.0
 306       * @throws  NotAllowed
 307       * @throws  \RuntimeException
 308       */
 309      public function add()
 310      {
 311          // Access check.
 312          if (!$this->allowAdd()) {
 313              throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403);
 314          }
 315  
 316          $recordId = $this->save();
 317  
 318          $this->displayItem($recordId);
 319      }
 320  
 321      /**
 322       * Method to edit an existing record.
 323       *
 324       * @return  static  A \JControllerLegacy object to support chaining.
 325       *
 326       * @since   4.0.0
 327       */
 328      public function edit()
 329      {
 330          /** @var \Joomla\CMS\MVC\Model\AdminModel $model */
 331          $model = $this->getModel(Inflector::singularize($this->contentType));
 332  
 333          if (!$model) {
 334              throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'));
 335          }
 336  
 337          try {
 338              $table = $model->getTable();
 339          } catch (\Exception $e) {
 340              throw new \RuntimeException($e->getMessage());
 341          }
 342  
 343          $recordId = $this->input->getInt('id');
 344  
 345          if (!$recordId) {
 346              throw new Exception\ResourceNotFound(Text::_('JLIB_APPLICATION_ERROR_RECORD'), 404);
 347          }
 348  
 349          $key = $table->getKeyName();
 350  
 351          // Access check.
 352          if (!$this->allowEdit(array($key => $recordId), $key)) {
 353              throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403);
 354          }
 355  
 356          // Attempt to check-out the new record for editing and redirect.
 357          if ($table->hasField('checked_out') && !$model->checkout($recordId)) {
 358              // Check-out failed, display a notice but allow the user to see the record.
 359              throw new Exception\CheckinCheckout(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKOUT_FAILED', $model->getError()));
 360          }
 361  
 362          $this->save($recordId);
 363          $this->displayItem($recordId);
 364  
 365          return $this;
 366      }
 367  
 368      /**
 369       * Method to save a record.
 370       *
 371       * @param   integer  $recordKey  The primary key of the item (if exists)
 372       *
 373       * @return  integer  The record ID on success, false on failure
 374       *
 375       * @since   4.0.0
 376       */
 377      protected function save($recordKey = null)
 378      {
 379          /** @var \Joomla\CMS\MVC\Model\AdminModel $model */
 380          $model = $this->getModel(Inflector::singularize($this->contentType));
 381  
 382          if (!$model) {
 383              throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_MODEL_CREATE'));
 384          }
 385  
 386          try {
 387              $table = $model->getTable();
 388          } catch (\Exception $e) {
 389              throw new \RuntimeException($e->getMessage());
 390          }
 391  
 392          $key        = $table->getKeyName();
 393          $data       = $this->input->get('data', json_decode($this->input->json->getRaw(), true), 'array');
 394          $checkin    = property_exists($table, $table->getColumnAlias('checked_out'));
 395          $data[$key] = $recordKey;
 396  
 397          if ($this->input->getMethod() === 'PATCH') {
 398              if ($recordKey && $table->load($recordKey)) {
 399                  $fields = $table->getFields();
 400  
 401                  foreach ($fields as $field) {
 402                      if (array_key_exists($field->Field, $data)) {
 403                          continue;
 404                      }
 405  
 406                      $data[$field->Field] = $table->{$field->Field};
 407                  }
 408              }
 409          }
 410  
 411          $data = $this->preprocessSaveData($data);
 412  
 413          // @todo: Not the cleanest thing ever but it works...
 414          Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms');
 415  
 416          // Needs to be set because com_fields needs the data in jform to determine the assigned catid
 417          $this->input->set('jform', $data);
 418  
 419          // Validate the posted data.
 420          $form = $model->getForm($data, false);
 421  
 422          if (!$form) {
 423              throw new \RuntimeException(Text::_('JLIB_APPLICATION_ERROR_FORM_CREATE'));
 424          }
 425  
 426          // Test whether the data is valid.
 427          $validData = $model->validate($form, $data);
 428  
 429          // Check for validation errors.
 430          if ($validData === false) {
 431              $errors   = $model->getErrors();
 432              $messages = [];
 433  
 434              // Push up to three validation messages out to the user.
 435              for ($i = 0, $n = \count($errors); $i < $n && $i < 3; $i++) {
 436                  if ($errors[$i] instanceof \Exception) {
 437                      $messages[] = "{$errors[$i]->getMessage()}";
 438                  } else {
 439                      $messages[] = "{$errors[$i]}";
 440                  }
 441              }
 442  
 443              throw new InvalidParameterException(implode("\n", $messages));
 444          }
 445  
 446          if (!isset($validData['tags'])) {
 447              $validData['tags'] = array();
 448          }
 449  
 450          // Attempt to save the data.
 451          if (!$model->save($validData)) {
 452              throw new Exception\Save(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()));
 453          }
 454  
 455          try {
 456              $modelName = $model->getName();
 457          } catch (\Exception $e) {
 458              throw new \RuntimeException($e->getMessage());
 459          }
 460  
 461          // Ensure we have the record ID in case we created a new article
 462          $recordId = $model->getState($modelName . '.id');
 463  
 464          if ($recordId === null) {
 465              throw new Exception\CheckinCheckout(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()));
 466          }
 467  
 468          // Save succeeded, so check-in the record.
 469          if ($checkin && $model->checkin($recordId) === false) {
 470              throw new Exception\CheckinCheckout(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()));
 471          }
 472  
 473          return $recordId;
 474      }
 475  
 476      /**
 477       * Method to check if you can edit an existing record.
 478       *
 479       * Extended classes can override this if necessary.
 480       *
 481       * @param   array   $data  An array of input data.
 482       * @param   string  $key   The name of the key for the primary key; default is id.
 483       *
 484       * @return  boolean
 485       *
 486       * @since   4.0.0
 487       */
 488      protected function allowEdit($data = array(), $key = 'id')
 489      {
 490          return $this->app->getIdentity()->authorise('core.edit', $this->option);
 491      }
 492  
 493      /**
 494       * Method to check if you can add a new record.
 495       *
 496       * Extended classes can override this if necessary.
 497       *
 498       * @param   array  $data  An array of input data.
 499       *
 500       * @return  boolean
 501       *
 502       * @since   4.0.0
 503       */
 504      protected function allowAdd($data = array())
 505      {
 506          $user = $this->app->getIdentity();
 507  
 508          return $user->authorise('core.create', $this->option) || \count($user->getAuthorisedCategories($this->option, 'core.create'));
 509      }
 510  
 511      /**
 512       * Method to allow extended classes to manipulate the data to be saved for an extension.
 513       *
 514       * @param   array  $data  An array of input data.
 515       *
 516       * @return  array
 517       *
 518       * @since   4.0.0
 519       */
 520      protected function preprocessSaveData(array $data): array
 521      {
 522          return $data;
 523      }
 524  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer