diff --git a/.drone.yml b/.drone.yml
index de3758236c4e2..40130718d2ee8 100644
--- a/.drone.yml
+++ b/.drone.yml
@@ -81,6 +81,11 @@ pipeline:
commands:
- bash libraries/vendor/joomla/test-system/src/drone-run.sh "$(pwd)"
+ api-tests:
+ image: joomlaprojects/docker-systemtests:latest
+ commands:
+ - bash libraries/vendor/joomla/test-api/drone-run.sh "$(pwd)"
+
analysis3x:
image: rips/rips-cli
secrets: [rips_username, rips_password]
diff --git a/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-05.sql b/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-05.sql
new file mode 100644
index 0000000000000..dffb48720873f
--- /dev/null
+++ b/administrator/components/com_admin/sql/updates/mysql/4.0.0-2019-01-05.sql
@@ -0,0 +1,3 @@
+INSERT INTO `#__extensions` (`extension_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `custom_data`, `system_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES
+(493, 0, 'plg_api-authentication_basic', 'plugin', 'basic', 'api-authentication', 0, 1, 1, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0),
+(494, 0, 'plg_webservices_content', 'plugin', 'content', 'webservices', 0, 1, 1, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0),
diff --git a/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-05.sql b/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-05.sql
new file mode 100644
index 0000000000000..80fa35c2b51fa
--- /dev/null
+++ b/administrator/components/com_admin/sql/updates/postgresql/4.0.0-2019-01-05.sql
@@ -0,0 +1,3 @@
+INSERT INTO "#__extensions" ("extension_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "system_data", "checked_out", "checked_out_time", "ordering", "state") VALUES
+(493, 0, 'plg_api-authentication_basic', 'plugin', 'basic', 'api-authentication', 0, 1, 1, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0),
+(494, 0, 'plg_webservices_content', 'plugin', 'content', 'webservices', 0, 1, 1, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0),
diff --git a/administrator/components/com_content/Model/ArticlesModel.php b/administrator/components/com_content/Model/ArticlesModel.php
index a1ad422ecf9be..b3b9c0eaae16f 100644
--- a/administrator/components/com_content/Model/ArticlesModel.php
+++ b/administrator/components/com_content/Model/ArticlesModel.php
@@ -199,7 +199,7 @@ protected function getListQuery()
'list.select',
'DISTINCT a.id, a.title, a.alias, a.checked_out, a.checked_out_time, a.catid' .
', a.state, a.access, a.created, a.created_by, a.created_by_alias, a.modified, a.ordering, a.featured, a.language, a.hits' .
- ', a.publish_up, a.publish_down'
+ ', a.publish_up, a.publish_down, a.introtext'
)
);
$query->from('#__content AS a');
@@ -600,6 +600,18 @@ public function getItems()
}
}
+ $asset = new \Joomla\CMS\Table\Asset($this->getDbo());
+
+ foreach (array_keys($items) as $x)
+ {
+ $items[$x]->typeAlias = 'com_content.article';
+
+ $asset->loadByName('com_content.article.' . $items[$x]->id);
+
+ // Re-inject the asset id.
+ $items[$x]->asset_id = $asset->id;
+ }
+
return $items;
}
}
diff --git a/administrator/includes/defines.php b/administrator/includes/defines.php
index 1e2eb7b80a31a..e1f482a1fe8c6 100644
--- a/administrator/includes/defines.php
+++ b/administrator/includes/defines.php
@@ -23,3 +23,4 @@
define('JPATH_THEMES', JPATH_BASE . DIRECTORY_SEPARATOR . 'templates');
define('JPATH_CACHE', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'cache');
define('JPATH_MANIFESTS', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'manifests');
+define('JPATH_API', JPATH_ROOT . DIRECTORY_SEPARATOR . 'api');
diff --git a/administrator/language/en-GB/en-GB.plg_api-authentication_basic.ini b/administrator/language/en-GB/en-GB.plg_api-authentication_basic.ini
new file mode 100644
index 0000000000000..11e2ac3bbf2c0
--- /dev/null
+++ b/administrator/language/en-GB/en-GB.plg_api-authentication_basic.ini
@@ -0,0 +1,7 @@
+; Joomla! Project
+; Copyright (C) 2005 - 2017 Open Source Matters. All rights reserved.
+; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
+; Note : All ini files need to be saved as UTF-8
+
+PLG_API-AUTHENTICATION_BASIC="API Authentication - Basic Auth"
+PLG_AUTH_AUTHENTICATION_BASIC_XML_DESCRIPTION="Used to allow basic authentication to Web Services in Joomla."
diff --git a/administrator/language/en-GB/en-GB.plg_api-authentication_basic.sys.ini b/administrator/language/en-GB/en-GB.plg_api-authentication_basic.sys.ini
new file mode 100644
index 0000000000000..11e2ac3bbf2c0
--- /dev/null
+++ b/administrator/language/en-GB/en-GB.plg_api-authentication_basic.sys.ini
@@ -0,0 +1,7 @@
+; Joomla! Project
+; Copyright (C) 2005 - 2017 Open Source Matters. All rights reserved.
+; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
+; Note : All ini files need to be saved as UTF-8
+
+PLG_API-AUTHENTICATION_BASIC="API Authentication - Basic Auth"
+PLG_AUTH_AUTHENTICATION_BASIC_XML_DESCRIPTION="Used to allow basic authentication to Web Services in Joomla."
diff --git a/administrator/language/en-GB/en-GB.plg_webservices_content.ini b/administrator/language/en-GB/en-GB.plg_webservices_content.ini
new file mode 100644
index 0000000000000..9cd632a094518
--- /dev/null
+++ b/administrator/language/en-GB/en-GB.plg_webservices_content.ini
@@ -0,0 +1,7 @@
+; Joomla! Project
+; Copyright (C) 2005 - 2017 Open Source Matters. All rights reserved.
+; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
+; Note : All ini files need to be saved as UTF-8
+
+PLG_WEBSERVICES_CONTENT="Web Services - Content"
+PLG_WEBSERVICES_CONTENT_XML_DESCRIPTION="Used to add articles routes to the API for your website."
diff --git a/administrator/language/en-GB/en-GB.plg_webservices_content.sys.ini b/administrator/language/en-GB/en-GB.plg_webservices_content.sys.ini
new file mode 100644
index 0000000000000..9cd632a094518
--- /dev/null
+++ b/administrator/language/en-GB/en-GB.plg_webservices_content.sys.ini
@@ -0,0 +1,7 @@
+; Joomla! Project
+; Copyright (C) 2005 - 2017 Open Source Matters. All rights reserved.
+; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
+; Note : All ini files need to be saved as UTF-8
+
+PLG_WEBSERVICES_CONTENT="Web Services - Content"
+PLG_WEBSERVICES_CONTENT_XML_DESCRIPTION="Used to add articles routes to the API for your website."
diff --git a/api/components/com_content/Controller/ArticleController.php b/api/components/com_content/Controller/ArticleController.php
new file mode 100644
index 0000000000000..a6385fcfd76df
--- /dev/null
+++ b/api/components/com_content/Controller/ArticleController.php
@@ -0,0 +1,30 @@
+setStart($startTime, $startMem)->mark('afterLoad') : null;
+
+// Boot the DI container
+$container = \Joomla\CMS\Factory::getContainer();
+
+/*
+ * Alias the session service keys to the web session service as that is the primary session backend for this application
+ *
+ * In addition to aliasing "common" service keys, we also create aliases for the PHP classes to ensure autowiring objects
+ * is supported. This includes aliases for aliased class names, and the keys for alised class names should be considered
+ * deprecated to be removed when the class name alias is removed as well.
+ */
+$container->alias('session', 'session.cli')
+ ->alias('JSession', 'session.cli')
+ ->alias(\Joomla\CMS\Session\Session::class, 'session.cli')
+ ->alias(\Joomla\Session\Session::class, 'session.cli')
+ ->alias(\Joomla\Session\SessionInterface::class, 'session.cli');
+
+// Instantiate the application.
+$app = $container->get(\Joomla\CMS\Application\ApiApplication::class);
+
+// Set the application as global app
+\Joomla\CMS\Factory::$application = $app;
+
+// Execute the application.
+$app->execute();
diff --git a/api/includes/defines.php b/api/includes/defines.php
new file mode 100644
index 0000000000000..12c10eed0f13d
--- /dev/null
+++ b/api/includes/defines.php
@@ -0,0 +1,26 @@
+isInDevelopmentState())))
+{
+ if (file_exists(JPATH_INSTALLATION . '/index.php'))
+ {
+ header('HTTP/1.1 500 Internal Server Error');
+ echo json_encode(
+ array('error' => 'You must install Joomla to use the API')
+ );
+
+ exit();
+ }
+ else
+ {
+ header('HTTP/1.1 500 Internal Server Error');
+ echo json_encode(
+ array('error' => 'No configuration file found and no installation code available. Exiting...')
+ );
+
+ exit;
+ }
+}
+
+// Pre-Load configuration. Don't remove the Output Buffering due to BOM issues, see JCode 26026
+ob_start();
+require_once JPATH_CONFIGURATION . '/configuration.php';
+ob_end_clean();
+
+// System configuration.
+$config = new JConfig;
+
+// Set the error_reporting
+switch ($config->error_reporting)
+{
+ case 'default':
+ case '-1':
+ break;
+
+ case 'none':
+ case '0':
+ error_reporting(0);
+
+ break;
+
+ case 'simple':
+ error_reporting(E_ERROR | E_WARNING | E_PARSE);
+ ini_set('display_errors', 1);
+
+ break;
+
+ case 'maximum':
+ error_reporting(E_ALL);
+ ini_set('display_errors', 1);
+
+ break;
+
+ case 'development':
+ error_reporting(-1);
+ ini_set('display_errors', 1);
+
+ break;
+
+ default:
+ error_reporting($config->error_reporting);
+ ini_set('display_errors', 1);
+
+ break;
+}
+
+define('JDEBUG', $config->debug);
+
+unset($config);
+
+// System profiler
+if (JDEBUG)
+{
+ // @deprecated 4.0 - The $_PROFILER global will be removed
+ $_PROFILER = JProfiler::getInstance('Application');
+}
diff --git a/api/index.php b/api/index.php
new file mode 100644
index 0000000000000..cbafb7fcf4626
--- /dev/null
+++ b/api/index.php
@@ -0,0 +1,35 @@
+ sprintf('Joomla requires PHP version %s to run', JOOMLA_MINIMUM_PHP))
+ );
+
+ return;
+}
+
+/**
+ * Constant that is checked in included files to prevent direct access.
+ * define() is used rather than "const" to not error for PHP 5.2 and lower
+ */
+define('_JEXEC', 1);
+
+// Run the application - All executable code should be triggered through this file
+require_once dirname(__FILE__) . '/includes/app.php';
diff --git a/composer.json b/composer.json
index f318988479dd4..cc32c4f32ae6f 100644
--- a/composer.json
+++ b/composer.json
@@ -65,6 +65,7 @@
"joomla/oauth1": "~2.0@dev",
"joomla/oauth2": "~2.0@dev",
"joomla/registry": "~2.0@dev",
+ "joomla/router": "~2.0@dev",
"joomla/session": "~2.0@dev",
"joomla/string": "~2.0@dev",
"joomla/uri": "~2.0@dev",
@@ -86,6 +87,8 @@
"symfony/yaml": "3.4.*",
"wamania/php-stemmer": "^1.2",
"maximebf/debugbar": "^1.15",
+ "tobscure/json-api": "^0.4.1",
+ "willdurand/negotiation": "^2.3",
"ext-json": "*"
},
"require-dev": {
@@ -98,6 +101,7 @@
"joomla-projects/joomla-browser": "~4.0@dev",
"joomla-projects/robo-joomla": "dev-develop",
"joomla/test-system": "dev-4.0-dev",
- "joomla/test-unit": "dev-4.0-dev"
+ "joomla/test-unit": "dev-4.0-dev",
+ "joomla/test-api": "dev-4.0-dev"
}
}
diff --git a/composer.lock b/composer.lock
index e5bc155b2d244..5adeebcc37512 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "59485b6ff077529cb9c9ad7872b31ddb",
+ "content-hash": "ff7779b0bdd2c9a064c943f1be315f0a",
"packages": [
{
"name": "composer/ca-bundle",
@@ -1269,6 +1269,55 @@
],
"time": "2018-10-03T02:55:25+00:00"
},
+ {
+ "name": "joomla/router",
+ "version": "dev-2.0-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/joomla-framework/router.git",
+ "reference": "7558239b9c8ffa0f447068e5fad14e9b3650dbe5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/joomla-framework/router/zipball/7558239b9c8ffa0f447068e5fad14e9b3650dbe5",
+ "reference": "7558239b9c8ffa0f447068e5fad14e9b3650dbe5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "~7.0"
+ },
+ "require-dev": {
+ "jeremeamia/superclosure": "~1.0",
+ "joomla/coding-standards": "~2.0@alpha",
+ "phpunit/phpunit": "~6.3"
+ },
+ "suggest": {
+ "jeremeamia/superclosure": "If you use Closure controllers, and want to be able to cache / serialize the router, please install jeremeamia/superclosure:~1.0"
+ },
+ "type": "joomla-package",
+ "extra": {
+ "branch-alias": {
+ "dev-2.0-dev": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Joomla\\Router\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-2.0-or-later"
+ ],
+ "description": "Joomla Router Package",
+ "homepage": "https://github.com/joomla-framework/router",
+ "keywords": [
+ "framework",
+ "joomla",
+ "router"
+ ],
+ "time": "2018-10-03T01:22:25+00:00"
+ },
{
"name": "joomla/session",
"version": "dev-2.0-dev",
@@ -2749,6 +2798,56 @@
"homepage": "https://symfony.com",
"time": "2018-11-11T19:48:54+00:00"
},
+ {
+ "name": "tobscure/json-api",
+ "version": "v0.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/tobscure/json-api.git",
+ "reference": "d4ba8437977c33a5189d95d9ffa751997f13b104"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/tobscure/json-api/zipball/d4ba8437977c33a5189d95d9ffa751997f13b104",
+ "reference": "d4ba8437977c33a5189d95d9ffa751997f13b104",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.5.9 || ^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8 || ^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Tobscure\\JsonApi\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Toby Zerner",
+ "email": "toby.zerner@gmail.com"
+ }
+ ],
+ "description": "JSON-API responses in PHP",
+ "keywords": [
+ "api",
+ "json",
+ "jsonapi",
+ "standard"
+ ],
+ "time": "2017-03-05T21:16:24+00:00"
+ },
{
"name": "wamania/php-stemmer",
"version": "1.2",
@@ -2793,6 +2892,58 @@
],
"time": "2017-01-27T17:16:44+00:00"
},
+ {
+ "name": "willdurand/negotiation",
+ "version": "v2.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/willdurand/Negotiation.git",
+ "reference": "03436ededa67c6e83b9b12defac15384cb399dc9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/willdurand/Negotiation/zipball/03436ededa67c6e83b9b12defac15384cb399dc9",
+ "reference": "03436ededa67c6e83b9b12defac15384cb399dc9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.4.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~4.5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Negotiation\\": "src/Negotiation"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "William Durand",
+ "email": "will+git@drnd.me"
+ }
+ ],
+ "description": "Content Negotiation tools for PHP provided as a standalone library.",
+ "homepage": "http://williamdurand.fr/Negotiation/",
+ "keywords": [
+ "accept",
+ "content",
+ "format",
+ "header",
+ "negotiation"
+ ],
+ "time": "2017-05-14T17:21:12+00:00"
+ },
{
"name": "zendframework/zend-diactoros",
"version": "1.8.6",
@@ -4486,6 +4637,37 @@
],
"time": "2018-10-16T23:34:41+00:00"
},
+ {
+ "name": "joomla/test-api",
+ "version": "dev-4.0-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/joomla/test-api.git",
+ "reference": "40691b3e595218c61c1f7f61c0e25c2c0bb118ce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/joomla/test-api/zipball/40691b3e595218c61c1f7f61c0e25c2c0bb118ce",
+ "reference": "40691b3e595218c61c1f7f61c0e25c2c0bb118ce",
+ "shasum": ""
+ },
+ "require": {
+ "codeception/codeception": "~2.3",
+ "php": ">=7.0.0"
+ },
+ "type": "project",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-2.0+"
+ ],
+ "description": "Joomla CMS API tests",
+ "homepage": "https://github.com/joomla/joomla-cms",
+ "keywords": [
+ "cms",
+ "joomla"
+ ],
+ "time": "2018-05-19T08:17:12+00:00"
+ },
{
"name": "joomla/test-system",
"version": "dev-4.0-dev",
@@ -6861,6 +7043,7 @@
"joomla/oauth1": 20,
"joomla/oauth2": 20,
"joomla/registry": 20,
+ "joomla/router": 20,
"joomla/session": 20,
"joomla/string": 20,
"joomla/uri": 20,
@@ -6869,7 +7052,8 @@
"joomla-projects/joomla-browser": 20,
"joomla-projects/robo-joomla": 20,
"joomla/test-system": 20,
- "joomla/test-unit": 20
+ "joomla/test-unit": 20,
+ "joomla/test-api": 20
},
"prefer-stable": false,
"prefer-lowest": false,
diff --git a/includes/defines.php b/includes/defines.php
index f6a059f059dec..3ca26b0b6a8c6 100644
--- a/includes/defines.php
+++ b/includes/defines.php
@@ -22,3 +22,4 @@
define('JPATH_THEMES', JPATH_BASE . DIRECTORY_SEPARATOR . 'templates');
define('JPATH_CACHE', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'cache');
define('JPATH_MANIFESTS', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'manifests');
+define('JPATH_API', JPATH_ROOT . DIRECTORY_SEPARATOR . 'api');
diff --git a/installation/includes/defines.php b/installation/includes/defines.php
index 27f5f60cd542f..fee6911e0e15f 100644
--- a/installation/includes/defines.php
+++ b/installation/includes/defines.php
@@ -24,3 +24,5 @@
define('JPATH_THEMES', JPATH_BASE);
define('JPATH_CACHE', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'cache');
define('JPATH_MANIFESTS', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'manifests');
+define('JPATH_API', JPATH_ADMINISTRATOR . DIRECTORY_SEPARATOR . 'api');
+
diff --git a/installation/sql/mysql/joomla.sql b/installation/sql/mysql/joomla.sql
index 9cb843b5945a8..b4762f934a323 100644
--- a/installation/sql/mysql/joomla.sql
+++ b/installation/sql/mysql/joomla.sql
@@ -675,6 +675,8 @@ INSERT INTO `#__extensions` (`extension_id`, `package_id`, `name`, `type`, `elem
(490, 0, 'plg_extension_namespacemap', 'plugin', 'namespacemap', 'extension', 0, 1, 1, 1, '', '{}', 0, '0000-00-00 00:00:00', 0, 0),
(491, 0, 'plg_installer_override', 'plugin', 'override', 'installer', 0, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 4, 0),
(492, 0, 'plg_quickicon_overridecheck', 'plugin', 'overridecheck', 'quickicon', 0, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 0, 0),
+(493, 0, 'plg_api-authentication_basic', 'plugin', 'basic', 'api-authentication', 0, 1, 1, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0),
+(494, 0, 'plg_webservices_content', 'plugin', 'content', 'webservices', 0, 1, 1, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0),
(509, 0, 'atum', 'template', 'atum', '', 1, 1, 1, 0, '', '', 0, '0000-00-00 00:00:00', 0, 0),
(510, 0, 'cassiopeia', 'template', 'cassiopeia', '', 0, 1, 1, 0, '', '{"logoFile":"","fluidContainer":"0","sidebarLeftWidth":"3","sidebarRightWidth":"3"}', 0, '0000-00-00 00:00:00', 0, 0),
(600, 802, 'English (en-GB)', 'language', 'en-GB', '', 0, 1, 1, 1, '', '', 0, '0000-00-00 00:00:00', 0, 0),
diff --git a/installation/sql/postgresql/joomla.sql b/installation/sql/postgresql/joomla.sql
index 14f6aa3fadff7..5b9bad215a190 100644
--- a/installation/sql/postgresql/joomla.sql
+++ b/installation/sql/postgresql/joomla.sql
@@ -684,6 +684,8 @@ INSERT INTO "#__extensions" ("extension_id", "package_id", "name", "type", "elem
(490, 0, 'plg_extension_namespacemap', 'plugin', 'namespacemap', 'extension', 0, 1, 1, 1, '', '{}', 0, '1970-01-01 00:00:00', 0, 0),
(491, 0, 'plg_installer_override', 'plugin', 'override', 'installer', 0, 1, 1, 1, '', '', 0, '1970-01-01 00:00:00', 4, 0),
(492, 0, 'plg_quickicon_overridecheck', 'plugin', 'overridecheck', 'quickicon', 0, 1, 1, 1, '', '', 0, '1970-01-01 00:00:00', 0, 0),
+(493, 0, 'plg_api-authentication_basic', 'plugin', 'basic', 'api-authentication', 0, 1, 1, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0),
+(494, 0, 'plg_webservices_content', 'plugin', 'content', 'webservices', 0, 1, 1, 0, '', '{}', 0, '0000-00-00 00:00:00', 0, 0),
(600, 802, 'English (en-GB)', 'language', 'en-GB', '', 0, 1, 1, 1, '', '', 0, '1970-01-01 00:00:00', 0, 0),
(601, 802, 'English (en-GB)', 'language', 'en-GB', '', 1, 1, 1, 1, '', '', 0, '1970-01-01 00:00:00', 0, 0),
(700, 0, 'files_joomla', 'file', 'joomla', '', 0, 1, 1, 1, '', '', 0, '1970-01-01 00:00:00', 0, 0),
diff --git a/libraries/namespacemap.php b/libraries/namespacemap.php
index 1281881c794cc..46cb9d9ab8d4c 100644
--- a/libraries/namespacemap.php
+++ b/libraries/namespacemap.php
@@ -63,6 +63,7 @@ public function ensureMapFileExists()
public function create()
{
$extensions = $this->getNamespaces('administrator/components');
+ $extensions = array_merge($extensions, $this->getNamespaces('api/components'));
$extensions = array_merge($extensions, $this->getNamespaces('modules'));
$extensions = array_merge($extensions, $this->getNamespaces('administrator/modules'));
@@ -200,6 +201,7 @@ private function getNamespaces(string $dir): array
if (strpos($extension, 'com_') === 0)
{
$extensions[$namespace . 'Site\\\\'] = str_replace('administrator/', '', $namespacePath) . $namespaceNode->attributes()->path;
+ $extensions[$namespace . 'Api\\\\'] = str_replace('administrator/', 'api/', $namespacePath) . $namespaceNode->attributes()->path;
}
// Add the application specific segment when not a plugin
diff --git a/libraries/src/Access/Exception/AuthenticationFailed.php b/libraries/src/Access/Exception/AuthenticationFailed.php
new file mode 100644
index 0000000000000..e2d722eed081a
--- /dev/null
+++ b/libraries/src/Access/Exception/AuthenticationFailed.php
@@ -0,0 +1,20 @@
+name = 'api';
+
+ // Register the client ID
+ $this->clientId = 3;
+
+ // Execute the parent constructor
+ parent::__construct($input, $config, $client, $container);
+
+ $this->addFormatMap('application/json', 'json');
+ $this->addFormatMap('application/vnd.api+json', 'jsonapi');
+
+ // Set the root in the URI based on the application name
+ \JUri::root(null, str_ireplace('/' . $this->getName(), '', \JUri::base(true)));
+ }
+
+
+ /**
+ * Method to run the application routines.
+ *
+ * Most likely you will want to instantiate a controller and execute it, or perform some sort of task directly.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function doExecute()
+ {
+ // Initialise the application
+ $this->initialiseApp();
+
+ // Mark afterInitialise in the profiler.
+ JDEBUG ? $this->profiler->mark('afterInitialise') : null;
+
+ // Route the application
+ $this->route();
+
+ // Mark afterApiRoute in the profiler.
+ JDEBUG ? $this->profiler->mark('afterApiRoute') : null;
+
+ // Dispatch the application
+ $this->dispatch();
+
+ // Mark afterDispatch in the profiler.
+ JDEBUG ? $this->profiler->mark('afterDispatch') : null;
+ }
+
+ /**
+ * Adds a mapping from a content type to the format stored. Note the format type cannot be overwritten.
+ *
+ * @param string $contentHeader The content header
+ * @param string $format The content type format
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function addFormatMap($contentHeader, $format)
+ {
+ if (!array_key_exists($contentHeader, $this->formatMapper))
+ {
+ $this->formatMapper[$contentHeader] = $format;
+ }
+ }
+
+ /**
+ * Rendering is the process of pushing the document buffers into the template
+ * placeholders, retrieving data from the document and pushing it into
+ * the application response buffer.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ *
+ * @note Rendering should be overridden to get rid of the theme files.
+ */
+ protected function render()
+ {
+ // Render the document
+ $this->setBody($this->document->render($this->allowCache()));
+ }
+
+ /**
+ * Method to send the application response to the client. All headers will be sent prior to the main application output data.
+ *
+ * @param array $options An optional argument to enable CORS. (Temporary)
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function respond($options = array())
+ {
+ // Set the Joomla! API signature
+ $this->setHeader('X-Powered-By', 'JoomlaAPI/1.0', true);
+
+ if (array_key_exists('cors', $options))
+ {
+ // Enable CORS (Cross-origin resource sharing)
+ $this->setHeader('Access-Control-Allow-Origin', '*', true);
+ $this->setHeader('Access-Control-Allow-Headers', 'Authorization');
+ }
+
+ // Parent function can be overridden later on for debugging.
+ parent::respond();
+
+ }
+
+ /**
+ * Gets the name of the current template.
+ *
+ * @param boolean $params True to return the template parameters
+ *
+ * @return string
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getTemplate($params = false)
+ {
+ // The API application should not need to use a template
+ return 'system';
+ }
+
+ /**
+ * Route the application.
+ *
+ * Routing is the process of examining the request environment to determine which
+ * component should receive the request. The component optional parameters
+ * are then set in the request object to be processed when the application is being
+ * dispatched.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function route()
+ {
+ $router = $this->getApiRouter();
+
+ // Trigger the onBeforeApiRoute event.
+ PluginHelper::importPlugin('webservices');
+ $this->triggerEvent('onBeforeApiRoute', array(&$router, $this));
+ $caught404 = false;
+
+ try
+ {
+ $route = $router->parseApiRoute($this->input->getMethod());
+ }
+ catch (RouteNotFoundException $e)
+ {
+ $caught404 = true;
+ }
+
+ /**
+ * Now we have an API perform content negotation to ensure we have a valid header. Assume if the route doesn't
+ * tell us otherwise it uses the pain JSON API
+ */
+ $priorities = array('application/vnd.api+json');
+
+ if (!$caught404 && array_key_exists('format', $route['vars']))
+ {
+ $priorities = $route['vars']['format'];
+ }
+
+ $negotiator = new Negotiator;
+
+ try
+ {
+ $mediaType = $negotiator->getBest($this->input->server->getString('HTTP_ACCEPT'), $priorities);
+ }
+ catch (InvalidArgument $e)
+ {
+ $mediaType = null;
+ }
+
+ // If we can't find a match bail with a 406 - Not Acceptable
+ if ($mediaType === null)
+ {
+ throw new Exception\NotAcceptable('Could not match accept header', 406);
+ }
+
+ /** @var $mediaType Accept */
+ $format = $mediaType->getValue();
+
+ if (array_key_exists($mediaType->getValue(), $this->formatMapper))
+ {
+ $format = $this->formatMapper[$mediaType->getValue()];
+ }
+
+ $this->input->set('format', $format);
+
+ if ($caught404)
+ {
+ throw $e;
+ }
+
+ $this->input->set('option', $route['vars']['component']);
+ $this->input->set('controller', $route['controller']);
+ $this->input->set('task', $route['task']);
+
+ foreach ($route['vars'] as $key => $value)
+ {
+ if ($key !== 'component')
+ {
+ if ($this->input->getMethod() === 'POST')
+ {
+ $this->input->post->set($key, $value);
+ }
+ else
+ {
+ $this->input->set($key, $value);
+ }
+ }
+ }
+
+ $this->triggerEvent('onAfterApiRoute', array($this));
+
+ if (!isset($route['vars']['public']) || $route['vars']['public'] === false)
+ {
+ if (!$this->login(array('username' => ''), array('silent' => true, 'action' => 'core.login.api')))
+ {
+ throw new AuthenticationFailed;
+ }
+ }
+ }
+
+ /**
+ * Returns the application Router object.
+ *
+ * @return ApiRouter
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getApiRouter()
+ {
+ return \JFactory::getContainer()->get('ApiRouter');
+ }
+
+ /**
+ * Dispatch the application
+ *
+ * @param string $component The component which is being rendered.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function dispatch($component = null)
+ {
+ // Get the component if not set.
+ if (!$component)
+ {
+ $component = $this->input->get('option', null);
+ }
+
+ // Load the document to the API
+ $this->loadDocument();
+
+ // Set up the params
+ $document = \JFactory::getDocument();
+
+ // Register the document object with \JFactory
+ \JFactory::$document = $document;
+
+ $contents = ComponentHelper::renderComponent($component);
+ $document->setBuffer($contents, 'component');
+
+ // Trigger the onAfterDispatch event.
+ PluginHelper::importPlugin('system');
+ $this->triggerEvent('onAfterDispatch');
+ }
+}
diff --git a/libraries/src/Application/ApplicationHelper.php b/libraries/src/Application/ApplicationHelper.php
index f212819485435..e1b429c33e274 100644
--- a/libraries/src/Application/ApplicationHelper.php
+++ b/libraries/src/Application/ApplicationHelper.php
@@ -144,6 +144,12 @@ public static function getClientInfo($id = null, $byName = false)
$obj->name = 'installation';
$obj->path = JPATH_INSTALLATION;
self::$_clients[2] = clone $obj;
+
+ // Installation Client
+ $obj->id = 3;
+ $obj->name = 'api';
+ $obj->path = JPATH_API;
+ self::$_clients[3] = clone $obj;
}
// If no client id has been passed return the whole array
diff --git a/libraries/src/Application/CMSApplication.php b/libraries/src/Application/CMSApplication.php
index 5813660a23b78..e12e57779b175 100644
--- a/libraries/src/Application/CMSApplication.php
+++ b/libraries/src/Application/CMSApplication.php
@@ -116,6 +116,14 @@ abstract class CMSApplication extends WebApplication implements ContainerAwareIn
*/
protected $pathway = null;
+ /**
+ * The authentication plugin type
+ *
+ * @type string
+ * @since __DEPLOY_VERSION__
+ */
+ protected $authenticationPluginType = 'authentication';
+
/**
* Class constructor.
*
@@ -766,7 +774,7 @@ protected function loadLibraryLanguage()
public function login($credentials, $options = array())
{
// Get the global Authentication object.
- $authenticate = Authentication::getInstance();
+ $authenticate = Authentication::getInstance($this->authenticationPluginType);
$response = $authenticate->authenticate($credentials, $options);
// Import the user plugin group.
diff --git a/libraries/src/Application/Exception/NotAcceptable.php b/libraries/src/Application/Exception/NotAcceptable.php
new file mode 100644
index 0000000000000..6c6de3e1eb8cb
--- /dev/null
+++ b/libraries/src/Application/Exception/NotAcceptable.php
@@ -0,0 +1,20 @@
+setDispatcher($dispatcher);
+ $this->pluginType = $pluginType;
- $isLoaded = PluginHelper::importPlugin('authentication');
+ $isLoaded = PluginHelper::importPlugin($this->pluginType);
if (!$isLoaded)
{
@@ -104,18 +114,20 @@ public function __construct(DispatcherInterface $dispatcher = null)
* Returns the global authentication object, only creating it
* if it doesn't already exist.
*
+ * @param string $pluginType The plugin type to run authorisation and authentication on
+ *
* @return Authentication The global Authentication object
*
* @since 1.7.0
*/
- public static function getInstance()
+ public static function getInstance(string $pluginType = 'authentication')
{
- if (empty(self::$instance))
+ if (empty(self::$instance[$pluginType]))
{
- self::$instance = new static;
+ self::$instance[$pluginType] = new static($pluginType);
}
- return self::$instance;
+ return self::$instance[$pluginType];
}
/**
@@ -133,7 +145,7 @@ public static function getInstance()
public function authenticate($credentials, $options = array())
{
// Get plugins
- $plugins = PluginHelper::getPlugin('authentication');
+ $plugins = PluginHelper::getPlugin($this->pluginType);
// Create authentication response
$response = new AuthenticationResponse;
@@ -150,7 +162,7 @@ public function authenticate($credentials, $options = array())
*/
foreach ($plugins as $plugin)
{
- $className = 'plg' . $plugin->type . $plugin->name;
+ $className = 'plg' . str_replace('-', '', $plugin->type) . $plugin->name;
if (class_exists($className))
{
@@ -205,12 +217,12 @@ public function authenticate($credentials, $options = array())
* @return AuthenticationResponse[] Array of authentication response objects
*
* @since 1.7.0
+ * @throws \Exception
*/
- public static function authorise($response, $options = array())
+ public function authorise($response, $options = array())
{
// Get plugins in case they haven't been imported already
PluginHelper::importPlugin('user');
- PluginHelper::importPlugin('authentication');
$results = Factory::getApplication()->triggerEvent('onUserAuthorisation', array($response, $options));
return $results;
diff --git a/libraries/src/Component/ComponentHelper.php b/libraries/src/Component/ComponentHelper.php
index d660430c04dcc..c91904ed0825e 100644
--- a/libraries/src/Component/ComponentHelper.php
+++ b/libraries/src/Component/ComponentHelper.php
@@ -303,12 +303,15 @@ public static function filterText($text)
public static function renderComponent($option, $params = array())
{
$app = Factory::getApplication();
-
- // Load template language files.
- $template = $app->getTemplate(true)->template;
$lang = Factory::getLanguage();
- $lang->load('tpl_' . $template, JPATH_BASE, null, false, true)
+
+ if (!$app->isClient('api'))
+ {
+ // Load template language files.
+ $template = $app->getTemplate(true)->template;
+ $lang->load('tpl_' . $template, JPATH_BASE, null, false, true)
|| $lang->load('tpl_' . $template, JPATH_THEMES . "/$template", null, false, true);
+ }
if (empty($option))
{
diff --git a/libraries/src/Dispatcher/ApiDispatcher.php b/libraries/src/Dispatcher/ApiDispatcher.php
new file mode 100644
index 0000000000000..87bcfaf6f11bb
--- /dev/null
+++ b/libraries/src/Dispatcher/ApiDispatcher.php
@@ -0,0 +1,147 @@
+app = $app;
+ $this->input = $input ?: $app->input;
+ $this->option = $this->input->get('option');
+
+ $db = Factory::getDbo();
+
+ $query = $db->getQuery(true);
+
+ $query->select($db->quoteName(array('namespace')))
+ ->from($db->quoteName('#__extensions'))
+ ->where($db->quoteName('namespace') . ' IS NOT NULL AND ' . $db->quoteName('namespace') . ' != ""')
+ ->where($db->quoteName('element') . ' = ' . $db->quote($this->option));
+
+ $db->setQuery($query);
+
+ $this->namespace = $db->loadResult();
+
+ if ($this->namespace === null)
+ {
+ throw new \RuntimeException('Namespace can not be empty!');
+ }
+
+ $this->loadLanguage();
+ }
+
+ /**
+ * Load the component's administrator language
+ *
+ * @since __DEPLOY_VERSION__
+ *
+ * @return void
+ */
+ protected function loadLanguage()
+ {
+ // Load common and local language files.
+ $this->app->getLanguage()->load($this->option, JPATH_BASE, null, false, true) ||
+ $this->app->getLanguage()->load($this->option, JPATH_COMPONENT_ADMINISTRATOR, null, false, true);
+ }
+
+ /**
+ * Dispatch a controller task. Redirecting the user if appropriate.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function dispatch()
+ {
+ $command = $this->input->getCmd('task', 'display');
+ $task = $command;
+
+ // Check for a controller.task command.
+ if (strpos($command, '.') !== false)
+ {
+ // Explode the controller.task command.
+ list ($controllerName, $task) = explode('.', $command);
+
+ $this->input->set('controller', $controllerName);
+ $this->input->set('task', $task);
+ }
+
+ // Build controller config data
+ $config['option'] = $this->option;
+
+ // Set name of controller if it is passed in the request
+ if ($this->input->exists('controller'))
+ {
+ $config['name'] = strtolower($this->input->get('controller'));
+ }
+
+ // Execute the task for this component
+ $namespace = rtrim($this->namespace, '\\') . '\\';
+ $controller = new ApiController($config, new ApiMVCFactory($namespace), $this->app, $this->input);
+ $controller->execute($task);
+ $controller->redirect();
+ }
+}
diff --git a/libraries/src/Document/JsonapiDocument.php b/libraries/src/Document/JsonapiDocument.php
new file mode 100644
index 0000000000000..1003174897863
--- /dev/null
+++ b/libraries/src/Document/JsonapiDocument.php
@@ -0,0 +1,201 @@
+_mime = 'application/vnd.api+json';
+ $this->_type = 'jsonapi';
+
+ if (array_key_exists('api_document', $options) && $options['api_document'] instanceof Document)
+ {
+ $this->document = $options['api_document'];
+ }
+ else
+ {
+ $this->document = new Document;
+ }
+ }
+
+ /**
+ * Set the data object.
+ *
+ * @param ElementInterface $element Element interface.
+ *
+ * @return $this
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function setData(ElementInterface $element)
+ {
+ $this->document->setData($element);
+
+ return $this;
+ }
+
+ /**
+ * Set the errors array.
+ *
+ * @param array $errors Error array.
+ *
+ * @return $this
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function setErrors($errors)
+ {
+ $this->document->setErrors($errors);
+
+ return $this;
+ }
+
+ /**
+ * Set the JSON-API array.
+ *
+ * @param array $jsonapi JSON-API array.
+ *
+ * @return $this
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function setJsonapi($jsonapi)
+ {
+ $this->setJsonapi($jsonapi);
+
+ return $this;
+ }
+
+ /**
+ * Map everything to arrays.
+ *
+ * @return array
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function toArray()
+ {
+ return $this->document->toArray();
+ }
+
+ /**
+ * Map to string.
+ *
+ * @return string
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function __toString()
+ {
+ return json_encode($this->toArray());
+ }
+
+ /**
+ * Outputs the document.
+ *
+ * @param boolean $cache If true, cache the output.
+ * @param array $params Associative array of attributes.
+ *
+ * @return string The rendered data.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function render($cache = false, $params = array())
+ {
+ $app = Factory::getApplication();
+
+ if ($mdate = $this->getModifiedDate())
+ {
+ $app->modifiedDate = $mdate;
+ }
+
+ $app->mimeType = $this->_mime;
+ $app->charSet = $this->_charset;
+
+ return json_encode($this->document);
+ }
+
+ /**
+ * Serialize for JSON usage.
+ *
+ * @return array
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function jsonSerialize()
+ {
+ return $this->toArray();
+ }
+
+ /**
+ * Add a link to the output.
+ *
+ * @param string $key The name of the link
+ * @param string $value The link
+ *
+ * @return $this
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function addLink($key, $value)
+ {
+ $this->document->addLink($key, $value);
+
+ return $this;
+ }
+
+ /**
+ * Add a link to the output.
+ *
+ * @param string $key The name of the metadata key
+ * @param string $value The value
+ *
+ * @return $this
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function addMeta($key, $value)
+ {
+ $this->document->addMeta($key, $value);
+
+ return $this;
+ }
+}
diff --git a/libraries/src/Error/JsonApi/AuthenticationFailedExceptionHandler.php b/libraries/src/Error/JsonApi/AuthenticationFailedExceptionHandler.php
new file mode 100644
index 0000000000000..f97663901278d
--- /dev/null
+++ b/libraries/src/Error/JsonApi/AuthenticationFailedExceptionHandler.php
@@ -0,0 +1,63 @@
+ 'Forbidden'];
+
+ $code = $e->getCode();
+
+ if ($code)
+ {
+ $error['code'] = $code;
+ }
+
+ return new ResponseBag($status, [$error]);
+ }
+}
diff --git a/libraries/src/Error/JsonApi/InvalidRouteExceptionHandler.php b/libraries/src/Error/JsonApi/InvalidRouteExceptionHandler.php
new file mode 100644
index 0000000000000..bba120e12d19b
--- /dev/null
+++ b/libraries/src/Error/JsonApi/InvalidRouteExceptionHandler.php
@@ -0,0 +1,63 @@
+ 'Resource not found'];
+
+ $code = $e->getCode();
+
+ if ($code)
+ {
+ $error['code'] = $code;
+ }
+
+ return new ResponseBag($status, [$error]);
+ }
+}
diff --git a/libraries/src/Error/JsonApi/NotAcceptableExceptionHandler.php b/libraries/src/Error/JsonApi/NotAcceptableExceptionHandler.php
new file mode 100644
index 0000000000000..0c5b73c369d2f
--- /dev/null
+++ b/libraries/src/Error/JsonApi/NotAcceptableExceptionHandler.php
@@ -0,0 +1,63 @@
+ 'Not Acceptable'];
+
+ $code = $e->getCode();
+
+ if ($code)
+ {
+ $error['code'] = $code;
+ }
+
+ return new ResponseBag($status, [$error]);
+ }
+}
diff --git a/libraries/src/Error/JsonApi/NotAllowedExceptionHandler.php b/libraries/src/Error/JsonApi/NotAllowedExceptionHandler.php
new file mode 100644
index 0000000000000..bbcb0bd681a44
--- /dev/null
+++ b/libraries/src/Error/JsonApi/NotAllowedExceptionHandler.php
@@ -0,0 +1,63 @@
+ 'Access Denied'];
+
+ $code = $e->getCode();
+
+ if ($code)
+ {
+ $error['code'] = $code;
+ }
+
+ return new ResponseBag($status, [$error]);
+ }
+}
diff --git a/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php b/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php
new file mode 100644
index 0000000000000..5dad89c7413e5
--- /dev/null
+++ b/libraries/src/Error/JsonApi/ResourceNotFoundExceptionHandler.php
@@ -0,0 +1,61 @@
+ 'Resource not found'];
+
+ $code = $e->getCode();
+
+ if ($code)
+ {
+ $error['code'] = $code;
+ }
+
+ return new ResponseBag($status, [$error]);
+ }
+}
diff --git a/libraries/src/Error/Renderer/JsonapiRenderer.php b/libraries/src/Error/Renderer/JsonapiRenderer.php
new file mode 100644
index 0000000000000..f6481a983f25a
--- /dev/null
+++ b/libraries/src/Error/Renderer/JsonapiRenderer.php
@@ -0,0 +1,87 @@
+registerHandler(new InvalidRouteExceptionHandler);
+ $errors->registerHandler(new AuthenticationFailedExceptionHandler);
+ $errors->registerHandler(new NotAcceptableExceptionHandler);
+ $errors->registerHandler(new NotAllowedExceptionHandler);
+ $errors->registerHandler(new InvalidParameterExceptionHandler);
+ $errors->registerHandler(new ResourceNotFoundExceptionHandler);
+ $errors->registerHandler(new FallbackExceptionHandler(JDEBUG));
+
+ $response = $errors->handle($error);
+ }
+ else
+ {
+ $code = 500;
+ $error = ['code' => $code, 'title' => 'Internal server error'];
+
+ if (JDEBUG)
+ {
+ $error['detail'] = (string) $error;
+ }
+
+ $response = new ResponseBag($code, $error);
+ }
+
+ $this->getDocument()->setErrors($response->getErrors());
+ Factory::getApplication()->setHeader('status', $response->getStatus());
+
+ if (ob_get_contents())
+ {
+ ob_end_clean();
+ }
+
+ return $this->getDocument()->render();
+ }
+}
diff --git a/libraries/src/Extension/Service/Provider/MVCFactory.php b/libraries/src/Extension/Service/Provider/MVCFactory.php
index dd0a91c456453..5c976b020c144 100644
--- a/libraries/src/Extension/Service/Provider/MVCFactory.php
+++ b/libraries/src/Extension/Service/Provider/MVCFactory.php
@@ -11,6 +11,7 @@
defined('JPATH_PLATFORM') or die;
use Joomla\CMS\Form\FormFactoryInterface;
+use Joomla\CMS\MVC\Factory\ApiMVCFactory;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\DI\Container;
use Joomla\DI\ServiceProviderInterface;
@@ -58,7 +59,15 @@ public function register(Container $container)
MVCFactoryInterface::class,
function (Container $container)
{
- $factory = new \Joomla\CMS\MVC\Factory\MVCFactory($this->namespace);
+ if (\Joomla\CMS\Factory::getApplication()->isClient('api'))
+ {
+ $factory = new ApiMVCFactory($this->namespace);
+ }
+ else
+ {
+ $factory = new \Joomla\CMS\MVC\Factory\MVCFactory($this->namespace);
+ }
+
$factory->setFormFactory($container->get(FormFactoryInterface::class));
return $factory;
diff --git a/libraries/src/Factory.php b/libraries/src/Factory.php
index d85c9ba75a924..d96b343ecc1da 100644
--- a/libraries/src/Factory.php
+++ b/libraries/src/Factory.php
@@ -559,7 +559,8 @@ protected static function createContainer(): Container
->registerServiceProvider(new \Joomla\CMS\Service\Provider\HTMLRegistry)
->registerServiceProvider(new \Joomla\CMS\Service\Provider\Session)
->registerServiceProvider(new \Joomla\CMS\Service\Provider\Toolbar)
- ->registerServiceProvider(new \Joomla\CMS\Service\Provider\WebAsset);
+ ->registerServiceProvider(new \Joomla\CMS\Service\Provider\WebAsset)
+ ->registerServiceProvider(new \Joomla\CMS\Service\Provider\ApiRouter);
return $container;
}
diff --git a/libraries/src/Installer/Adapter/ComponentAdapter.php b/libraries/src/Installer/Adapter/ComponentAdapter.php
index 9f4c8232231e5..c23faf40ec5f8 100644
--- a/libraries/src/Installer/Adapter/ComponentAdapter.php
+++ b/libraries/src/Installer/Adapter/ComponentAdapter.php
@@ -42,6 +42,17 @@ class ComponentAdapter extends InstallerAdapter
* */
protected $oldAdminFiles = null;
+ /**
+ * The list of current files fo the Joomla! CMS API that are installed and is read
+ * from the manifest on disk in the update area to handle doing a diff
+ * and deleting files that are in the old files list and not in the new
+ * files list.
+ *
+ * @var array
+ * @since __DEPLOY_VERSION__
+ * */
+ protected $oldApiFiles = null;
+
/**
* The list of current files that are installed and is read
* from the manifest on disk in the update area to handle doing a diff
@@ -85,7 +96,9 @@ protected function checkExtensionInFilesystem()
* If the component site or admin directory already exists, then we will assume that the component is already
* installed or another component is using that directory.
*/
- if (file_exists($this->parent->getPath('extension_site')) || file_exists($this->parent->getPath('extension_administrator')))
+ if (file_exists($this->parent->getPath('extension_site'))
+ || file_exists($this->parent->getPath('extension_administrator'))
+ || file_exists($this->parent->getPath('extension_api')))
{
// Look for an update function or update tag
$updateElement = $this->getManifest()->update;
@@ -111,11 +124,22 @@ protected function checkExtensionInFilesystem()
);
}
- // If the admin exists say so
+ if (file_exists($this->parent->getPath('extension_administrator')))
+ {
+ // If the admin exists say so
+ throw new \RuntimeException(
+ Text::sprintf(
+ 'JLIB_INSTALLER_ERROR_COMP_INSTALL_DIR_ADMIN',
+ $this->parent->getPath('extension_administrator')
+ )
+ );
+ }
+
+ // If the API exists say so
throw new \RuntimeException(
Text::sprintf(
- 'JLIB_INSTALLER_ERROR_COMP_INSTALL_DIR_ADMIN',
- $this->parent->getPath('extension_administrator')
+ 'JLIB_INSTALLER_ERROR_COMP_INSTALL_DIR_API',
+ $this->parent->getPath('extension_api')
)
);
}
@@ -180,6 +204,29 @@ protected function copyBaseFiles()
}
}
+ // Copy API files
+ if ($this->getManifest()->api->files)
+ {
+ if ($this->route === 'update')
+ {
+ $result = $this->parent->parseFiles($this->getManifest()->api->files, 1, $this->oldApiFiles);
+ }
+ else
+ {
+ $result = $this->parent->parseFiles($this->getManifest()->api->files, 1);
+ }
+
+ if ($result === false)
+ {
+ throw new \RuntimeException(
+ Text::sprintf(
+ 'JLIB_INSTALLER_ABORT_COMP_FAIL_API_FILES',
+ Text::_('JLIB_INSTALLER_' . strtoupper($this->route))
+ )
+ );
+ }
+ }
+
// If there is a manifest script, let's copy it.
if ($this->manifest_script)
{
@@ -253,7 +300,7 @@ protected function createExtensionRoot()
Text::sprintf(
'JLIB_INSTALLER_ERROR_COMP_FAILED_TO_CREATE_DIRECTORY',
Text::_('JLIB_INSTALLER_' . strtoupper($this->route)),
- $this->parent->getPath('extension_site')
+ $this->parent->getPath('extension_administrator')
)
);
}
@@ -272,6 +319,37 @@ protected function createExtensionRoot()
)
);
}
+
+ // If the component API directory does not exist, let's create it
+ $created = false;
+
+ if (!file_exists($this->parent->getPath('extension_api')))
+ {
+ if (!$created = Folder::create($this->parent->getPath('extension_api')))
+ {
+ throw new \RuntimeException(
+ Text::sprintf(
+ 'JLIB_INSTALLER_ERROR_COMP_FAILED_TO_CREATE_DIRECTORY',
+ Text::_('JLIB_INSTALLER_' . strtoupper($this->route)),
+ $this->parent->getPath('extension_api')
+ )
+ );
+ }
+ }
+
+ /*
+ * Since we created the component API directory and we will want to remove it if we have to roll
+ * back the installation, let's add it to the installation step stack
+ */
+ if ($created)
+ {
+ $this->parent->pushStep(
+ array(
+ 'type' => 'folder',
+ 'path' => $this->parent->getPath('extension_api'),
+ )
+ );
+ }
}
/**
@@ -443,6 +521,16 @@ protected function finaliseUninstall(): bool
}
}
+ // Delete the component API directory
+ if (is_dir($this->parent->getPath('extension_api')))
+ {
+ if (!Folder::delete($this->parent->getPath('extension_api')))
+ {
+ Log::add(Text::_('JLIB_INSTALLER_ERROR_COMP_UNINSTALL_FAILED_REMOVE_DIRECTORY_API'), Log::WARNING, 'jerror');
+ $retval = false;
+ }
+ }
+
// Now we will no longer need the extension object, so let's delete it
$this->extension->delete($this->extension->extension_id);
@@ -488,7 +576,33 @@ public function getElement($element = null)
public function loadLanguage($path = null)
{
$source = $this->parent->getPath('source');
- $client = $this->parent->extension->client_id ? JPATH_ADMINISTRATOR : JPATH_SITE;
+
+ switch ($this->parent->extension->client_id)
+ {
+ case 0:
+ $client = JPATH_SITE;
+
+ break;
+
+ case 1:
+ $client = JPATH_ADMINISTRATOR;
+
+ break;
+
+ case 3:
+ $client = JPATH_API;
+
+ break;
+
+ default:
+ throw new \InvalidArgumentException(
+ sprintf(
+ 'Unsupported client ID %d for component %s',
+ $this->parent->extension->client_id,
+ $this->parent->extension->element
+ )
+ );
+ }
if (!$source)
{
@@ -502,6 +616,10 @@ public function loadLanguage($path = null)
{
$element = $this->getManifest()->administration->files;
}
+ elseif ($this->getManifest()->api->files)
+ {
+ $element = $this->getManifest()->api->files;
+ }
elseif ($this->getManifest()->files)
{
$element = $this->getManifest()->files;
@@ -672,6 +790,7 @@ protected function setupInstallPaths()
// Set the installation target paths
$this->parent->setPath('extension_site', Path::clean(JPATH_SITE . '/components/' . $this->element));
$this->parent->setPath('extension_administrator', Path::clean(JPATH_ADMINISTRATOR . '/components/' . $this->element));
+ $this->parent->setPath('extension_api', Path::clean(JPATH_API . '/components/' . $this->element));
// Copy the admin path as it's used as a common base
$this->parent->setPath('extension_root', $this->parent->getPath('extension_administrator'));
@@ -694,6 +813,7 @@ protected function setupUninstall()
{
// Get the admin and site paths for the component
$this->parent->setPath('extension_administrator', Path::clean(JPATH_ADMINISTRATOR . '/components/' . $this->extension->element));
+ $this->parent->setPath('extension_api', Path::clean(JPATH_API . '/components/' . $this->extension->element));
$this->parent->setPath('extension_site', Path::clean(JPATH_SITE . '/components/' . $this->extension->element));
// Copy the admin path as it's used as a common base
@@ -711,6 +831,7 @@ protected function setupUninstall()
{
// Make sure we delete the folders if no manifest exists
Folder::delete($this->parent->getPath('extension_administrator'));
+ Folder::delete($this->parent->getPath('extension_api'));
Folder::delete($this->parent->getPath('extension_site'));
// Remove the menu
@@ -758,6 +879,7 @@ protected function setupUpdates()
if ($old_manifest)
{
$this->oldAdminFiles = $old_manifest->administration->files;
+ $this->oldApiFiles = $old_manifest->api->files;
$this->oldFiles = $old_manifest->files;
}
}
@@ -1209,6 +1331,7 @@ public function discover()
$results = array();
$site_components = Folder::folders(JPATH_SITE . '/components');
$admin_components = Folder::folders(JPATH_ADMINISTRATOR . '/components');
+ $api_components = Folder::folders(JPATH_API . '/components');
foreach ($site_components as $component)
{
@@ -1251,6 +1374,26 @@ public function discover()
}
}
+ foreach ($api_components as $component)
+ {
+ if (file_exists(JPATH_API . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'))
+ {
+ $manifest_details = Installer::parseXMLInstallFile(
+ JPATH_API . '/components/' . $component . '/' . str_replace('com_', '', $component) . '.xml'
+ );
+ $extension = Table::getInstance('extension');
+ $extension->set('type', 'component');
+ $extension->set('client_id', 3);
+ $extension->set('element', $component);
+ $extension->set('folder', '');
+ $extension->set('name', $component);
+ $extension->set('state', -1);
+ $extension->set('manifest_cache', json_encode($manifest_details));
+ $extension->set('params', '{}');
+ $results[] = $extension;
+ }
+ }
+
return $results;
}
diff --git a/libraries/src/MVC/Controller/ApiController.php b/libraries/src/MVC/Controller/ApiController.php
new file mode 100644
index 0000000000000..16fee1ef7d1d0
--- /dev/null
+++ b/libraries/src/MVC/Controller/ApiController.php
@@ -0,0 +1,507 @@
+option))
+ {
+ $this->option = ComponentHelper::getComponentName($this, $this->getName());
+ }
+
+ // Guess the \JText message prefix. Defaults to the option.
+ if (empty($this->text_prefix))
+ {
+ $this->text_prefix = strtoupper($this->option);
+ }
+
+ // Guess the context as the suffix, eg: OptionControllerContent.
+ if (empty($this->context))
+ {
+ $r = null;
+
+ if (!preg_match('/(.*)Controller(.*)/i', get_class($this), $r))
+ {
+ throw new \Exception(Text::_('JLIB_APPLICATION_ERROR_CONTROLLER_GET_NAME'), 500);
+ }
+
+ $this->context = str_replace('\\', '', strtolower($r[2]));
+ }
+ }
+
+ /**
+ * Basic display of an item view
+ *
+ * @param integer $id The primary key to display. Leave empty if you want to retrieve data from the request
+ *
+ * @return static A \JControllerLegacy object to support chaining.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function displayItem($id = null)
+ {
+ if ($id === null)
+ {
+ $id = $this->input->get('id', 0, 'int');
+ }
+
+ $viewType = $this->app->getDocument()->getType();
+ $viewName = $this->input->get('view', $this->contentType);
+ $viewLayout = $this->input->get('layout', 'default', 'string');
+
+ try
+ {
+ /** @var JsonApiView $view */
+ $view = $this->getView($viewName, $viewType, '', ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType]);
+ }
+ catch (\Exception $e)
+ {
+ return $this;
+ }
+
+ // Create the model, ignoring request data so we can safely set the state in the request, without it being
+ // reinitialised on the first getState call
+ $model = $this->getModel('', '', ['ignore_request' => true]);
+
+ if (!$model)
+ {
+ throw new \RuntimeException('Unable to create the model');
+ }
+
+ try
+ {
+ $modelName = $model->getName();
+ }
+ catch (\Exception $e)
+ {
+ return $this;
+ }
+
+ $model->setState($modelName . '.id', $id);
+
+ // Push the model into the view (as default)
+ $view->setModel($model, true);
+
+ $view->document = $this->app->getDocument();
+ $view->displayItem();
+
+ return $this;
+ }
+
+ /**
+ * Basic display of a list view
+ *
+ * @return static A \JControllerLegacy object to support chaining.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function displayList()
+ {
+ // Assemble pagination information (using recommended JsonApi pagination notation for offset strategy)
+ $paginationInfo = $this->input->get('page', [], 'array');
+ $internalPaginationMapping = [];
+
+ if (array_key_exists('offset', $paginationInfo))
+ {
+ $this->input->set('limitstart', $paginationInfo['offset']);
+ }
+
+ if (array_key_exists('limit', $paginationInfo))
+ {
+ $internalPaginationMapping['limit'] = $paginationInfo['limit'];
+ }
+
+ $this->input->set('list', $internalPaginationMapping);
+
+ $viewType = $this->app->getDocument()->getType();
+ $viewName = $this->input->get('view', $this->contentType);
+ $viewLayout = $this->input->get('layout', 'default', 'string');
+
+ try
+ {
+ /** @var JsonApiView $view */
+ $view = $this->getView($viewName, $viewType, '', ['base_path' => $this->basePath, 'layout' => $viewLayout, 'contentType' => $this->contentType]);
+ }
+ catch (\Exception $e)
+ {
+ return $this;
+ }
+
+ /** @var ListModel $model */
+ $model = $this->getModel($this->contentType);
+
+ if (!$model)
+ {
+ throw new \RuntimeException('Model failed to be created', 500);
+ }
+
+ // Push the model into the view (as default)
+ $view->setModel($model, true);
+
+ /**
+ * Sanity check we don't have too much data being requested as regularly in html we automatically set it back to
+ * the last page of data. If there isn't a limit start then set
+ */
+ if (!$this->input->getInt('limit', null))
+ {
+ $model->setState('list.limit', $this->itemsPerPage);
+ }
+
+ if ($this->input->getInt('limitstart', 0) > $model->getTotal())
+ {
+ throw new Exception\ResourceNotFound;
+ }
+
+ $view->document = $this->app->getDocument();
+
+ $view->displayList();
+
+ return $this;
+ }
+
+ /**
+ * Removes an item.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function delete()
+ {
+ if (!$this->app->getIdentity()->authorise('core.delete', $this->option))
+ {
+ throw new NotAllowed('JLIB_APPLICATION_ERROR_DELETE_NOT_PERMITTED', 403);
+ }
+
+ $id = $this->input->get('id', 0, 'int');
+
+ /** @var \Joomla\CMS\MVC\Model\AdminModel $model */
+ $model = $this->getModel();
+
+ // Remove the item.
+ if (!$model->delete($id))
+ {
+ throw new \RuntimeException($model->getError(), 500);
+ }
+
+ $this->app->setHeader('status', 204);
+ }
+
+ /**
+ * Method to add a new record.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ * @throws NotAllowed
+ * @throws \RuntimeException
+ */
+ public function add()
+ {
+ // Access check.
+ if (!$this->allowAdd())
+ {
+ throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403);
+ }
+ else
+ {
+ $success = $this->save();
+
+ if (!$success)
+ {
+ throw new \RuntimeException($this->message);
+ }
+
+ $this->displayItem($success);
+ }
+ }
+
+ /**
+ * Method to edit an existing record.
+ *
+ * @return boolean True if save succeeded after access level check and checkout passes, false otherwise.
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function edit()
+ {
+ /** @var \Joomla\CMS\MVC\Model\AdminModel $model */
+ $model = $this->getModel();
+
+ try
+ {
+ $table = $model->getTable();
+ }
+ catch (\Exception $e)
+ {
+ $this->setMessage($e->getMessage());
+
+ return false;
+ }
+
+ $recordId = $this->input->getInt('id');
+
+ if (!$recordId)
+ {
+ // TODO: Lang string for exception
+ throw new Exception\ResourceNotFound('Record does not exist', 404);
+ }
+
+ $key = $table->getKeyName();
+ $checkin = property_exists($table, $table->getColumnAlias('checked_out'));
+
+ // Access check.
+ if (!$this->allowEdit(array($key => $recordId), $key))
+ {
+ throw new NotAllowed('JLIB_APPLICATION_ERROR_CREATE_RECORD_NOT_PERMITTED', 403);
+ }
+
+ // Attempt to check-out the new record for editing and redirect.
+ if ($checkin && !$model->checkout($recordId))
+ {
+ // Check-out failed, display a notice but allow the user to see the record.
+ $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKOUT_FAILED', $model->getError()), 'error');
+
+ return false;
+ }
+
+ if (!$this->save($recordId))
+ {
+ throw new \RuntimeException($this->message);
+ }
+
+ return true;
+ }
+
+ /**
+ * Method to save a record.
+ *
+ * @param int $recordKey The primary key of the item (if exists)
+ *
+ * @return int|boolean The record ID on success, false on failure
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function save($recordKey = null)
+ {
+ /** @var \Joomla\CMS\MVC\Model\AdminModel $model */
+ $model = $this->getModel();
+
+ try
+ {
+ $table = $model->getTable();
+ }
+ catch (\Exception $e)
+ {
+ $this->setMessage($e->getMessage());
+
+ return false;
+ }
+
+ $key = $table->getKeyName();
+ $data = json_decode($this->input->json->getRaw(), true);
+ $checkin = property_exists($table, $table->getColumnAlias('checked_out'));
+ $data[$key] = $recordKey;
+
+ // TODO: Not the cleanest thing ever but it works...
+ Form::addFormPath(JPATH_COMPONENT_ADMINISTRATOR . '/forms');
+
+ // Validate the posted data.
+ $form = $model->getForm($data, false);
+
+ if (!$form)
+ {
+ $this->setMessage($model->getError(), 'error');
+
+ return false;
+ }
+
+ // Test whether the data is valid.
+ $validData = $model->validate($form, $data);
+
+ // Check for validation errors.
+ if ($validData === false)
+ {
+ // Get the validation messages.
+ $errors = $model->getErrors();
+
+ // Push up to three validation messages out to the user.
+ for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++)
+ {
+ if ($errors[$i] instanceof \Exception)
+ {
+ $this->setMessage($errors[$i]->getMessage(), 'warning');
+ }
+ else
+ {
+ $this->setMessage($errors[$i], 'warning');
+ }
+ }
+
+ return false;
+ }
+
+ if (!isset($validData['tags']))
+ {
+ $validData['tags'] = array();
+ }
+
+ // Attempt to save the data.
+ if (!$model->save($validData))
+ {
+ $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error');
+
+ return false;
+ }
+
+ try
+ {
+ $modelName = $model->getName();
+ }
+ catch (\Exception $e)
+ {
+ $this->setMessage($e->getMessage());
+
+ return false;
+ }
+
+ // Ensure we have the record ID in case we created a new article
+ $recordId = $model->getState($modelName . '.id');
+
+ if ($recordId === null)
+ {
+ $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error');
+
+ return false;
+ }
+
+ // Save succeeded, so check-in the record.
+ if ($checkin && $model->checkin($recordId) === false)
+ {
+ // Check-in failed, so go back to the record and display a notice.
+ $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()), 'error');
+
+ return false;
+ }
+
+ return $recordId;
+ }
+
+ /**
+ * Method to check if you can edit an existing record.
+ *
+ * Extended classes can override this if necessary.
+ *
+ * @param array $data An array of input data.
+ * @param string $key The name of the key for the primary key; default is id.
+ *
+ * @return boolean
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function allowEdit($data = array(), $key = 'id')
+ {
+ return $this->app->getIdentity()->authorise('core.edit', $this->option);
+ }
+
+ /**
+ * Method to check if you can add a new record.
+ *
+ * Extended classes can override this if necessary.
+ *
+ * @param array $data An array of input data.
+ *
+ * @return boolean
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ protected function allowAdd($data = array())
+ {
+ $user = $this->app->getIdentity();
+
+ return $user->authorise('core.create', $this->option) || count($user->getAuthorisedCategories($this->option, 'core.create'));
+ }
+}
diff --git a/libraries/src/MVC/Controller/BaseController.php b/libraries/src/MVC/Controller/BaseController.php
index 1cc8cd1e85e2e..8b7104805703b 100644
--- a/libraries/src/MVC/Controller/BaseController.php
+++ b/libraries/src/MVC/Controller/BaseController.php
@@ -763,6 +763,13 @@ public function getModel($name = '', $prefix = '', $config = array())
// Task is a reserved state
$model->setState('task', $this->task);
+ // We don't have the concept on a menu tree in the api app, so skip setting it's information and
+ // return early
+ if ($this->app->isClient('api'))
+ {
+ return $model;
+ }
+
// Let's get the application object and set menu information if it's available
$menu = Factory::getApplication()->getMenu();
diff --git a/libraries/src/MVC/Controller/Exception/ResourceNotFound.php b/libraries/src/MVC/Controller/Exception/ResourceNotFound.php
new file mode 100644
index 0000000000000..c6273d81f4750
--- /dev/null
+++ b/libraries/src/MVC/Controller/Exception/ResourceNotFound.php
@@ -0,0 +1,20 @@
+getState('list.limit') - (int) $this->getState('list.links');
// Create the pagination object and add the object to the internal cache.
- $this->cache[$store] = new \JPagination($this->getTotal(), $this->getStart(), $limit);
+ $this->cache[$store] = new Pagination($this->getTotal(), $this->getStart(), $limit);
return $this->cache[$store];
}
@@ -337,7 +340,7 @@ public function getStart()
* @param array $data data
* @param boolean $loadData load current data
*
- * @return \JForm|boolean The \JForm object or false on error
+ * @return Form|boolean The \JForm object or false on error
*
* @since 3.2
*/
@@ -374,7 +377,7 @@ public function getFilterForm($data = array(), $loadData = true)
* @param boolean $clear Optional argument to force load a new form.
* @param string|boolean $xpath An optional xpath to search for the fields.
*
- * @return \JForm|boolean \JForm object on success, False on error.
+ * @return Form|boolean \JForm object on success, False on error.
*
* @see \JForm
* @since 3.2
@@ -394,13 +397,13 @@ protected function loadForm($name, $source = null, $options = array(), $clear =
}
// Get the form.
- \JForm::addFormPath(JPATH_COMPONENT . '/forms');
- \JForm::addFormPath(JPATH_COMPONENT . '/models/forms');
- \JForm::addFieldPath(JPATH_COMPONENT . '/models/fields');
+ Form::addFormPath(JPATH_COMPONENT . '/forms');
+ Form::addFormPath(JPATH_COMPONENT . '/models/forms');
+ Form::addFieldPath(JPATH_COMPONENT . '/models/fields');
try
{
- $form = \JForm::getInstance($name, $source, $options, false, $xpath);
+ $form = Form::getInstance($name, $source, $options, false, $xpath);
if (isset($options['load_data']) && $options['load_data'])
{
@@ -650,7 +653,7 @@ protected function populateState($ordering = null, $direction = null)
/**
* Method to allow derived classes to preprocess the form.
*
- * @param \JForm $form A \JForm object.
+ * @param Form $form A \JForm object.
* @param mixed $data The data expected for the form.
* @param string $group The name of the plugin group to import (defaults to "content").
*
@@ -659,7 +662,7 @@ protected function populateState($ordering = null, $direction = null)
* @since 3.2
* @throws \Exception if there is an error in the form event.
*/
- protected function preprocessForm(\JForm $form, $data, $group = 'content')
+ protected function preprocessForm(Form $form, $data, $group = 'content')
{
// Import the appropriate plugin group.
\JPluginHelper::importPlugin($group);
diff --git a/libraries/src/MVC/View/JsonApiView.php b/libraries/src/MVC/View/JsonApiView.php
new file mode 100644
index 0000000000000..2da46f6c5882b
--- /dev/null
+++ b/libraries/src/MVC/View/JsonApiView.php
@@ -0,0 +1,176 @@
+type = $config['contentType'];
+ }
+
+ parent::__construct($config);
+ }
+
+ /**
+ * Execute and display a template script.
+ *
+ * @return string
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function displayList()
+ {
+ /** @var \Joomla\CMS\MVC\Model\ListModel $model */
+ $model = $this->getModel();
+
+ $items = $model->getItems();
+ $pagination = $model->getPagination();
+
+ // Check for errors.
+ if (count($errors = $this->get('Errors')))
+ {
+ throw new \JViewGenericdataexception(implode("\n", $errors), 500);
+ }
+
+ if ($this->type === null)
+ {
+ throw new \RuntimeException('Content type missing');
+ }
+
+ // Set up links for pagination
+ $currentUrl = Uri::getInstance();
+ $currentPageDefaultInformation = array('offset' => $pagination->limitstart, 'limit' => $pagination->limit);
+ $currentPageQuery = $currentUrl->getVar('page', $currentPageDefaultInformation);
+ $totalPagesAvailable = ($pagination->pagesTotal * $pagination->limit);
+
+ $firstPage = clone $currentUrl;
+ $firstPageQuery = $currentPageQuery;
+ $firstPageQuery['offset'] = 0;
+ $firstPage->setVar('page', $firstPageQuery);
+
+ $nextPage = clone $currentUrl;
+ $nextPageQuery = $currentPageQuery;
+ $nextOffset = $currentPageQuery['offset'] + $pagination->limit;
+ $nextPageQuery['offset'] = ($nextOffset > ($totalPagesAvailable * $pagination->limit)) ? $totalPagesAvailable - $pagination->limit : $nextOffset;
+ $nextPage->setVar('page', $nextPageQuery);
+
+ $previousPage = clone $currentUrl;
+ $previousPageQuery = $currentPageQuery;
+ $previousOffset = $currentPageQuery['offset'] - $pagination->limit;
+ $previousPageQuery['offset'] = $previousOffset >= 0 ? $previousOffset : 0;
+ $previousPage->setVar('page', $previousPageQuery);
+
+ $lastPage = clone $currentUrl;
+ $lastPageQuery = $currentPageQuery;
+ $lastPageQuery['offset'] = $totalPagesAvailable - $pagination->limit;
+ $lastPage->setVar('page', $lastPageQuery);
+
+ $collection = (new Collection($items, new JoomlaSerializer($this->type)))
+ ->fields([$this->type => $this->fieldsToRender]);
+
+ // Set the data into the document and render it
+ $this->document->addMeta('total-pages', $pagination->pagesTotal)
+ ->setData($collection)
+ ->addLink('self', (string) $currentUrl)
+ ->addLink('first', (string) $firstPage)
+ ->addLink('next', (string) $nextPage)
+ ->addLink('previous', (string) $previousPage)
+ ->addLink('last', (string) $lastPage);
+
+ return $this->document->render();
+ }
+
+ /**
+ * Execute and display a template script.
+ *
+ * @return string
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function displayItem()
+ {
+ /** @var \Joomla\CMS\MVC\Model\AdminModel $model */
+ $model = $this->getModel();
+ $item = $model->getItem();
+
+ if ($item->id === null)
+ {
+ throw new RouteNotFoundException('Item does not exist');
+ }
+
+ // Check for errors.
+ if (count($errors = $this->get('Errors')))
+ {
+ throw new \JViewGenericdataexception(implode("\n", $errors), 500);
+ }
+
+ if ($this->type === null)
+ {
+ throw new \RuntimeException('Content type missing');
+ }
+
+ $serializer = new JoomlaSerializer($this->type);
+ $element = (new Resource($item, $serializer))
+ ->fields([$this->type => $this->fieldsToRender]);
+
+ $this->document->setData($element);
+ $this->document->addLink('self', Uri::current());
+
+ return $this->document->render();
+ }
+}
diff --git a/libraries/src/MVC/View/JsonView.php b/libraries/src/MVC/View/JsonView.php
new file mode 100644
index 0000000000000..bfd24284788d1
--- /dev/null
+++ b/libraries/src/MVC/View/JsonView.php
@@ -0,0 +1,99 @@
+_charset = $config['charset'];
+ }
+
+ // Set a base path for use by the view
+ if (array_key_exists('base_path', $config))
+ {
+ $this->_basePath = $config['base_path'];
+ }
+ else
+ {
+ $this->_basePath = JPATH_COMPONENT;
+ }
+ }
+
+ /**
+ * Execute and display a template script.
+ *
+ * @param string $tpl The name of the template file to parse; automatically searches through the template paths.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function display($tpl = null)
+ {
+ // Serializing the output
+ $result = json_encode($this->_output);
+
+ // Pushing output to the document
+ $this->document->setBuffer($result);
+ }
+}
diff --git a/libraries/src/Router/ApiRouter.php b/libraries/src/Router/ApiRouter.php
new file mode 100644
index 0000000000000..c158cc0e0f931
--- /dev/null
+++ b/libraries/src/Router/ApiRouter.php
@@ -0,0 +1,161 @@
+app = $app;
+
+ parent::__construct($maps);
+ }
+
+ /**
+ * Creates routes map for CRUD
+ *
+ * @param string $baseName The base name of the component.
+ * @param string $controller The name of the controller that contains CRUD functions.
+ * @param array $defaults An array of default values that are used when the URL is matched.
+ * @param bool $publicGets Allow the public to make GET requests.
+ *
+ * @return void
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function createCRUDRoutes($baseName, $controller, $defaults = array(), $publicGets = false)
+ {
+ $getDefaults = array_merge(array('public' => $publicGets), $defaults);
+
+ $routes = array(
+ new Route(['GET'], $baseName, $controller . '.displayList', [], $getDefaults),
+ new Route(['GET'], $baseName . '/:id', $controller . '.displayItem', ['id' => '(\d+)'], $getDefaults),
+ new Route(['POST'], $baseName, $controller . '.add', [], $defaults),
+ new Route(['PUT'], $baseName . '/:id', $controller . '.edit', ['id' => '(\d+)'], $defaults),
+ new Route(['DELETE'], $baseName . '/:id', $controller . '.delete', ['id' => '(\d+)'], $defaults),
+ );
+
+ $this->addRoutes($routes);
+ }
+
+ /**
+ * Parse the given route and return the name of a controller mapped to the given route.
+ *
+ * @param string $method Request method to match. One of GET, POST, PUT, DELETE, HEAD, OPTIONS, TRACE or PATCH
+ *
+ * @return array An array containing the controller and the matched variables.
+ *
+ * @since __DEPLOY_VERSION__
+ * @throws \InvalidArgumentException
+ */
+ public function parseApiRoute($method = 'GET')
+ {
+ $method = strtoupper($method);
+
+ $validMethods = ["GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS", "TRACE", "PATCH"];
+
+ if (!in_array($method, $validMethods))
+ {
+ throw new \InvalidArgumentException(sprintf('%s is not a valid HTTP method.', $method));
+ }
+
+ // Get the path from the route and remove and leading or trailing slash.
+ $uri = \JUri::getInstance();
+ $path = urldecode($uri->getPath());
+
+ /**
+ * In some environments (e.g. CLI we can't form a valid base URL). In this case we catch the exception thrown
+ * by URI and set an empty base URI for further work.
+ * TODO: This should probably be handled better
+ */
+ try
+ {
+ $baseUri = \JUri::base(true);
+ }
+ catch (\RuntimeException $e)
+ {
+ $baseUri = '';
+ }
+
+ // Remove the base URI path.
+ $path = substr_replace($path, '', 0, strlen($baseUri));
+
+ if (!$this->app->get('sef_rewrite'))
+ {
+ // Transform the route
+ if ($path === 'index.php')
+ {
+ $path = '';
+ }
+ else
+ {
+ $path = str_replace('index.php/', '', $path);
+ }
+ }
+
+ $query = \JUri::getInstance()->getQuery(true);
+
+ // Iterate through all of the known routes looking for a match.
+ foreach ($this->routes as $route)
+ {
+ if (in_array($method, $route->getMethods()))
+ {
+ if (preg_match($route->getRegex(), ltrim($path, '/'), $matches))
+ {
+ // If we have gotten this far then we have a positive match.
+ $vars = $route->getDefaults();
+
+ foreach ($route->getRouteVariables() as $i => $var)
+ {
+ $vars[$var] = $matches[$i + 1];
+ }
+
+ $controller = preg_split("/[.]+/", $route->getController());
+ $vars = array_merge($vars, $query);
+
+ return [
+ 'controller' => $controller[0],
+ 'task' => $controller[1],
+ 'vars' => $vars
+ ];
+ }
+ }
+ }
+
+ throw new RouteNotFoundException(sprintf('Unable to handle request for route `%s`.', $path));
+ }
+}
diff --git a/libraries/src/Serializer/JoomlaSerializer.php b/libraries/src/Serializer/JoomlaSerializer.php
new file mode 100644
index 0000000000000..54bbf51a3b7ba
--- /dev/null
+++ b/libraries/src/Serializer/JoomlaSerializer.php
@@ -0,0 +1,80 @@
+type = $type;
+ }
+
+ /**
+ * Get the attributes array.
+ *
+ * @param Table|array|\stdClass|CMSobject $post The model
+ * @param array $fields The fields can be array or null
+ *
+ * @return array
+ *
+ * @since __DEPLOY_VERSION__
+ */
+ public function getAttributes($post, array $fields = null)
+ {
+ if (!($post instanceof Table) && !($post instanceof \stdClass) && !(is_array($post))
+ && !($post instanceof CMSObject))
+ {
+ $message = sprintf(
+ 'Invalid argument for TableSerializer. Expected array or %s. Got %s',
+ Table::class,
+ gettype($post)
+ );
+
+ throw new \InvalidArgumentException($message);
+ }
+
+ // The response from a standard ListModel query
+ if ($post instanceof \stdClass)
+ {
+ $post = (array) $post;
+ }
+
+ // The response from a standard AdminModel query
+ if ($post instanceof CMSObject)
+ {
+ $post = $post->getProperties();
+ }
+
+ // TODO: Find a way to make this an instance of TableInterface instead of the concrete class
+ if ($post instanceof Table)
+ {
+ $post = $post->getProperties();
+ }
+
+ return is_array($fields) ? array_intersect_key($post, array_flip($fields)) : $post;
+ }
+}
diff --git a/libraries/src/Service/Provider/ApiRouter.php b/libraries/src/Service/Provider/ApiRouter.php
new file mode 100644
index 0000000000000..c714281f320f9
--- /dev/null
+++ b/libraries/src/Service/Provider/ApiRouter.php
@@ -0,0 +1,45 @@
+alias('ApiRouter', 'Joomla\CMS\Router\ApiRouter')
+ ->share(
+ 'Joomla\CMS\Router\ApiRouter',
+ function (Container $container)
+ {
+ return new \Joomla\CMS\Router\ApiRouter($container->get(SiteApplication::class));
+ },
+ true
+ );
+ }
+}
diff --git a/libraries/src/Service/Provider/Application.php b/libraries/src/Service/Provider/Application.php
index 11f88eba243a2..a26b7e9d7eaa1 100644
--- a/libraries/src/Service/Provider/Application.php
+++ b/libraries/src/Service/Provider/Application.php
@@ -12,6 +12,7 @@
defined('JPATH_PLATFORM') or die;
use Joomla\CMS\Application\AdministratorApplication;
+use Joomla\CMS\Application\ApiApplication;
use Joomla\CMS\Application\ConsoleApplication;
use Joomla\CMS\Application\SiteApplication;
use Joomla\CMS\Console\SessionGcCommand;
@@ -125,5 +126,26 @@ function (Container $container)
},
true
);
+
+ $container->alias(ApiApplication::class, 'JApplicationApi')
+ ->share(
+ 'JApplicationApi',
+ function (Container $container) {
+ $app = new ApiApplication(null, null, null, $container);
+
+ // The session service provider needs JFactory::$application, set it if still null
+ if (Factory::$application === null)
+ {
+ Factory::$application = $app;
+ }
+
+ $app->setDispatcher($container->get('Joomla\Event\DispatcherInterface'));
+ $app->setLogger($container->get(LoggerInterface::class));
+ $app->setSession($container->get('Joomla\Session\SessionInterface'));
+
+ return $app;
+ },
+ true
+ );
}
}
diff --git a/plugins/api-authentication/basic/basic.php b/plugins/api-authentication/basic/basic.php
new file mode 100644
index 0000000000000..1dd80ae13033d
--- /dev/null
+++ b/plugins/api-authentication/basic/basic.php
@@ -0,0 +1,119 @@
+type = 'Basic';
+
+ $username = $this->app->input->server->get('PHP_AUTH_USER');
+ $password = $this->app->input->server->get('PHP_AUTH_PW');
+
+ if (empty($password))
+ {
+ $response->status = Authentication::STATUS_FAILURE;
+ $response->error_message = Text::_('JGLOBAL_AUTH_EMPTY_PASS_NOT_ALLOWED');
+
+ return;
+ }
+
+ // Get a database object
+ $query = $this->db->getQuery(true)
+ ->select($this->db->quoteName(array('id', 'password')))
+ ->from($this->db->quoteName('#__users'))
+ ->where($this->db->quoteName('username') . '=' . $this->db->quote($username));
+
+ $this->db->setQuery($query);
+ $result = $this->db->loadObject();
+
+ if ($result)
+ {
+ $match = UserHelper::verifyPassword($password, $result->password, $result->id);
+
+ if ($match === true)
+ {
+ // Bring this in line with the rest of the system
+ $user = User::getInstance($result->id);
+ $response->email = $user->email;
+ $response->fullname = $user->name;
+ $response->username = $username;
+
+ if ($this->app->isClient('administrator'))
+ {
+ $response->language = $user->getParam('admin_language');
+ }
+
+ else
+ {
+ $response->language = $user->getParam('language');
+ }
+
+ $response->status = Authentication::STATUS_SUCCESS;
+ $response->error_message = '';
+ }
+ else
+ {
+ // Invalid password
+ $response->status = Authentication::STATUS_FAILURE;
+ $response->error_message = Text::_('JGLOBAL_AUTH_INVALID_PASS');
+ }
+ }
+ else
+ {
+ // Let's hash the entered password even if we don't have a matching user for some extra response time
+ // By doing so, we mitigate side channel user enumeration attacks
+ UserHelper::hashPassword($password);
+
+ // Invalid user
+ $response->status = Authentication::STATUS_FAILURE;
+ $response->error_message = Text::_('JGLOBAL_AUTH_NO_USER');
+ }
+ }
+}
diff --git a/plugins/api-authentication/basic/basic.xml b/plugins/api-authentication/basic/basic.xml
new file mode 100644
index 0000000000000..775cfb90a28aa
--- /dev/null
+++ b/plugins/api-authentication/basic/basic.xml
@@ -0,0 +1,19 @@
+
+
+ plg_api-authentication_basic
+ Joomla! Project
+ November 2005
+ Copyright (C) 2005 - 2017 Open Source Matters. All rights reserved.
+ GNU General Public License version 2 or later; see LICENSE.txt
+ admin@joomla.org
+ www.joomla.org
+ 4.0.0
+ PLG_AUTH_AUTHENTICATION_BASIC_XML_DESCRIPTION
+
+ basic.php
+
+
+ en-GB.plg_authentication_api_basic.ini
+ en-GB.plg_authentication_api_basic.sys.ini
+
+
diff --git a/plugins/system/debug/debug.php b/plugins/system/debug/debug.php
index 354b6818c6be3..e19330443ec4d 100644
--- a/plugins/system/debug/debug.php
+++ b/plugins/system/debug/debug.php
@@ -196,7 +196,7 @@ public function __construct(&$subject, $config)
public function onAfterDispatch()
{
// Only if debugging or language debug is enabled.
- if ((JDEBUG || $this->debugLang) && $this->isAuthorisedDisplayDebug())
+ if ((JDEBUG || $this->debugLang) && $this->isAuthorisedDisplayDebug() && strtolower($this->app->getDocument()->getType()) === 'html')
{
HTMLHelper::_('stylesheet', 'plg_system_debug/debug.css', array('version' => 'auto', 'relative' => true));
HTMLHelper::_('script', 'plg_system_debug/debug.min.js', array('version' => 'auto', 'relative' => true));
@@ -219,7 +219,7 @@ public function onAfterDispatch()
public function onAfterRespond()
{
// Do not render if debugging or language debug is not enabled.
- if (!JDEBUG && !$this->debugLang || $this->isAjax)
+ if (!JDEBUG && !$this->debugLang || $this->isAjax || strtolower($this->app->getDocument()->getType()) !== 'html')
{
return;
}
diff --git a/plugins/webservices/content/content.php b/plugins/webservices/content/content.php
new file mode 100644
index 0000000000000..dfd867886082b
--- /dev/null
+++ b/plugins/webservices/content/content.php
@@ -0,0 +1,43 @@
+createCRUDRoutes('article', 'article', ['component' => 'com_content']);
+ }
+}
diff --git a/plugins/webservices/content/content.xml b/plugins/webservices/content/content.xml
new file mode 100644
index 0000000000000..6754aa7698453
--- /dev/null
+++ b/plugins/webservices/content/content.xml
@@ -0,0 +1,19 @@
+
+
+ plg_webservices_content
+ Joomla! Project
+ August 2017
+ (C) 2005 - 2017 Open Source Matters. All rights reserved.
+ GNU General Public License version 2 or later; see LICENSE.txt
+ admin@joomla.org
+ www.joomla.org
+ 4.0.0
+ PLG_WEBSERVICES_CONTENT_XML_DESCRIPTION
+
+ content.php
+
+
+ language/en-GB/en-GB.plg_webservices_content.ini
+ language/en-GB/en-GB.plg_webservices_content.sys.ini
+
+