33
0
mirror of https://github.com/joomla-extensions/patchtester.git synced 2024-12-22 02:49:01 +00:00

Switching to PSR-12 and updating composer dependencies

This commit is contained in:
Hannes Papenberg 2022-09-08 10:54:04 +02:00
parent 397c64c3e4
commit 8234d9a14a
35 changed files with 3717 additions and 3760 deletions

View File

@ -3,11 +3,13 @@ kind: pipeline
name: default name: default
clone: clone:
depth: 42
steps: steps:
- name: composer - name: composer
image: joomlaprojects/docker-tools:develop image: joomlaprojects/docker-images:php7.4
volumes:
- name: composer-cache
path: /tmp/composer-cache
commands: commands:
- composer validate --no-check-all --strict - composer validate --no-check-all --strict
- composer install --no-progress --no-suggest - composer install --no-progress --no-suggest
@ -16,10 +18,15 @@ steps:
image: joomlaprojects/docker-images:php7.2 image: joomlaprojects/docker-images:php7.2
commands: commands:
- echo $(date) - echo $(date)
- ./administrator/components/com_patchtester/vendor/bin/phpcs --extensions=php -p --ignore=administrator/components/com_patchtester/vendor --standard=administrator/components/com_patchtester/vendor/joomla/cms-coding-standards/lib/Joomla-CMS administrator - ./administrator/components/com_patchtester/vendor/bin/phpcs --extensions=php -p --ignore=administrator/components/com_patchtester/vendor --ignore=build/packaging --standard=PSR12 .
- echo $(date) - echo $(date)
volumes:
- name: composer-cache
host:
path: /tmp/composer-cache
--- ---
kind: signature kind: signature
hmac: c5899584898d37d46fb70cb22487532d41719c7a836be7f67ad4ac3c2267dafa hmac: 66f585c17b678699c15f48673fa52585c02eddab99c4dcfa2f1e1e85960a2a31
... ...

20
.editorconfig Normal file
View File

@ -0,0 +1,20 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style end of lines and a blank line at the end of the file
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.php]
indent_style = space
indent_size = 4
[*.{js,json,scss,css,vue}]
indent_style = space
indent_size = 2

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -21,88 +22,81 @@ use PatchTester\Model\AbstractModel;
*/ */
abstract class AbstractController abstract class AbstractController
{ {
/** /**
* The active application * The active application
* *
* @var CMSApplication * @var CMSApplication
* @since 4.0.0 * @since 4.0.0
*/ */
protected $app; protected $app;
/**
* The object context
*
* @var string
* @since 2.0
*/
protected $context;
/**
* The default view to display
*
* @var string
* @since 2.0
*/
protected $defaultView = 'pulls';
/**
* Instantiate the controller
*
* @param CMSApplication $app The application object.
*
* @since 2.0
*/
public function __construct(CMSApplication $app)
{
$this->app = $app;
// Set the context for the controller
$this->context = 'com_patchtester.' . $this->getInput()->getCmd('view', $this->defaultView);
}
/** /**
* The object context * Get the application object.
* *
* @var string * @return CMSApplication
* @since 2.0 *
*/ * @since 4.0.0
protected $context; */
public function getApplication()
{
return $this->app;
}
/** /**
* The default view to display * Get the input object.
* *
* @var string * @return Input
* @since 2.0 *
*/ * @since 4.0.0
protected $defaultView = 'pulls'; */
public function getInput()
{
return $this->app->input;
}
/** /**
* Instantiate the controller * Sets the state for the model object
* *
* @param CMSApplication $app The application object. * @param AbstractModel $model Model object
* *
* @since 2.0 * @return Registry
*/ *
public function __construct(CMSApplication $app) * @since 2.0
{ */
$this->app = $app; protected function initializeState($model)
{
// Set the context for the controller $state = new Registry();
$this->context = 'com_patchtester.' . $this->getInput()->getCmd('view', $this->defaultView); // Load the parameters.
} $params = ComponentHelper::getParams('com_patchtester');
$state->set('github_user', $params->get('org', 'joomla'));
/** $state->set('github_repo', $params->get('repo', 'joomla-cms'));
* Get the application object. return $state;
* }
* @return CMSApplication
*
* @since 4.0.0
*/
public function getApplication()
{
return $this->app;
}
/**
* Get the input object.
*
* @return Input
*
* @since 4.0.0
*/
public function getInput()
{
return $this->app->input;
}
/**
* Sets the state for the model object
*
* @param AbstractModel $model Model object
*
* @return Registry
*
* @since 2.0
*/
protected function initializeState($model)
{
$state = new Registry;
// Load the parameters.
$params = ComponentHelper::getParams('com_patchtester');
$state->set('github_user', $params->get('org', 'joomla'));
$state->set('github_repo', $params->get('repo', 'joomla-cms'));
return $state;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -13,6 +14,11 @@ use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use PatchTester\Model\PullModel; use PatchTester\Model\PullModel;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Controller class to apply patches * Controller class to apply patches
* *
@ -20,40 +26,32 @@ use PatchTester\Model\PullModel;
*/ */
class ApplyController extends AbstractController class ApplyController extends AbstractController
{ {
/** /**
* Execute the controller. * Execute the controller.
* *
* @return void Redirects the application * @return void Redirects the application
* *
* @since 2.0 * @since 2.0
*/ */
public function execute() public function execute()
{ {
try try {
{ $model = new PullModel(null, Factory::getDbo());
$model = new PullModel(null, Factory::getDbo()); // Initialize the state for the model
$model->setState($this->initializeState($model));
if ($model->apply($this->getInput()->getUint('pull_id'))) {
$msg = Text::_('COM_PATCHTESTER_APPLY_OK');
} else {
$msg = Text::_('COM_PATCHTESTER_NO_FILES_TO_PATCH');
}
// Initialize the state for the model $type = 'message';
$model->setState($this->initializeState($model)); } catch (\Exception $e) {
$msg = $e->getMessage();
$type = 'error';
}
if ($model->apply($this->getInput()->getUint('pull_id'))) $this->getApplication()->enqueueMessage($msg, $type);
{ $this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false));
$msg = Text::_('COM_PATCHTESTER_APPLY_OK'); }
}
else
{
$msg = Text::_('COM_PATCHTESTER_NO_FILES_TO_PATCH');
}
$type = 'message';
}
catch (\Exception $e)
{
$msg = $e->getMessage();
$type = 'error';
}
$this->getApplication()->enqueueMessage($msg, $type);
$this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false));
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -13,6 +14,11 @@ use Joomla\CMS\Language\Text;
use Joomla\Registry\Registry; use Joomla\Registry\Registry;
use PatchTester\Model\AbstractModel; use PatchTester\Model\AbstractModel;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Default display controller * Default display controller
* *
@ -20,138 +26,110 @@ use PatchTester\Model\AbstractModel;
*/ */
class DisplayController extends AbstractController class DisplayController extends AbstractController
{ {
/** /**
* Default ordering value * Default ordering value
* *
* @var string * @var string
* @since 4.0.0 * @since 4.0.0
*/ */
protected $defaultFullOrdering = 'a.pull_id DESC'; protected $defaultFullOrdering = 'a.pull_id DESC';
/**
* Execute the controller.
*
* @return boolean True on success
*
* @since 2.0
* @throws \RuntimeException
*/
public function execute()
{
// Set up variables to build our classes
$view = $this->getInput()->getCmd('view', $this->defaultView);
$format = $this->getInput()->getCmd('format', 'html');
// Register the layout paths for the view
$paths = new \SplPriorityQueue();
// Add the path for template overrides
$paths->insert(JPATH_THEMES . '/' . $this->getApplication()->getTemplate() . '/html/com_patchtester/' . $view, 2);
// Add the path for the default layouts
$paths->insert(dirname(__DIR__) . '/View/' . ucfirst($view) . '/tmpl', 1);
// Build the class names for the model and view
$viewClass = '\\PatchTester\\View\\' . ucfirst($view) . '\\' . ucfirst($view) . ucfirst($format) . 'View';
$modelClass = '\\PatchTester\\Model\\' . ucfirst($view) . 'Model';
// Sanity check - Ensure our classes exist
if (!class_exists($viewClass)) {
// Try to use a default view
$viewClass = '\\PatchTester\\View\\Default' . ucfirst($format) . 'View';
if (!class_exists($viewClass)) {
throw new \RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_VIEW_NOT_FOUND', $view, $format), 500);
}
}
/** if (!class_exists($modelClass)) {
* Execute the controller. throw new \RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_MODEL_NOT_FOUND', $modelClass), 500);
* }
* @return boolean True on success
*
* @since 2.0
* @throws \RuntimeException
*/
public function execute()
{
// Set up variables to build our classes
$view = $this->getInput()->getCmd('view', $this->defaultView);
$format = $this->getInput()->getCmd('format', 'html');
// Register the layout paths for the view // Initialize the model class now; need to do it before setting the state to get required data from it
$paths = new \SplPriorityQueue; $model = new $modelClass($this->context, null, Factory::getDbo());
// Initialize the state for the model
$state = $this->initializeState($model);
foreach ($state as $key => $value) {
$model->setState($key, $value);
}
// Add the path for template overrides // Initialize the view class now
$paths->insert(JPATH_THEMES . '/' . $this->getApplication()->getTemplate() . '/html/com_patchtester/' . $view, 2); $view = new $viewClass($model, $paths);
// Echo the rendered view for the application
echo $view->render();
// Finished!
return true;
}
// Add the path for the default layouts /**
$paths->insert(dirname(__DIR__) . '/View/' . ucfirst($view) . '/tmpl', 1); * Sets the state for the model object
*
* @param AbstractModel $model Model object
*
* @return Registry
*
* @since 2.0
*/
protected function initializeState($model)
{
$state = parent::initializeState($model);
$app = $this->getApplication();
// Load the filter state.
$state->set('filter.search', $app->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', ''));
$state->set('filter.applied', $app->getUserStateFromRequest($this->context . '.filter.applied', 'filter_applied', ''));
$state->set('filter.branch', $app->getUserStateFromRequest($this->context . '.filter.branch', 'filter_branch', ''));
$state->set('filter.rtc', $app->getUserStateFromRequest($this->context . '.filter.rtc', 'filter_rtc', ''));
$state->set('filter.npm', $app->getUserStateFromRequest($this->context . '.filter.npm', 'filter_npm', ''));
$state->set('filter.label', $app->getUserStateFromRequest($this->context . '.filter.label', 'filter_label', ''));
// Pre-fill the limits.
$limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->input->get('list_limit', 20), 'uint');
$state->set('list.limit', $limit);
$fullOrdering = $app->getUserStateFromRequest($this->context . '.fullorder', 'list_fullordering', $this->defaultFullOrdering);
$orderingParts = explode(' ', $fullOrdering);
if (count($orderingParts) !== 2) {
$fullOrdering = $this->defaultFullOrdering;
$orderingParts = explode(' ', $fullOrdering);
}
// Build the class names for the model and view $state->set('list.fullordering', $fullOrdering);
$viewClass = '\\PatchTester\\View\\' . ucfirst($view) . '\\' . ucfirst($view) . ucfirst($format) . 'View'; // The 2nd part will be considered the direction
$modelClass = '\\PatchTester\\Model\\' . ucfirst($view) . 'Model'; $direction = $orderingParts[array_key_last($orderingParts)];
if (in_array(strtoupper($direction), ['ASC', 'DESC', ''])) {
$state->set('list.direction', $direction);
}
// Sanity check - Ensure our classes exist // The 1st part will be the ordering
if (!class_exists($viewClass)) $ordering = $orderingParts[array_key_first($orderingParts)];
{ if (in_array($ordering, $model->getSortFields())) {
// Try to use a default view $state->set('list.ordering', $ordering);
$viewClass = '\\PatchTester\\View\\Default' . ucfirst($format) . 'View'; }
if (!class_exists($viewClass)) $value = $app->getUserStateFromRequest($this->context . '.limitstart', 'limitstart', 0);
{ $limitstart = ($limit != 0 ? (floor($value / $limit) * $limit) : 0);
throw new \RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_VIEW_NOT_FOUND', $view, $format), 500); $state->set('list.start', $limitstart);
} return $state;
} }
if (!class_exists($modelClass))
{
throw new \RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_MODEL_NOT_FOUND', $modelClass), 500);
}
// Initialize the model class now; need to do it before setting the state to get required data from it
$model = new $modelClass($this->context, null, Factory::getDbo());
// Initialize the state for the model
$state = $this->initializeState($model);
foreach ($state as $key => $value)
{
$model->setState($key, $value);
}
// Initialize the view class now
$view = new $viewClass($model, $paths);
// Echo the rendered view for the application
echo $view->render();
// Finished!
return true;
}
/**
* Sets the state for the model object
*
* @param AbstractModel $model Model object
*
* @return Registry
*
* @since 2.0
*/
protected function initializeState($model)
{
$state = parent::initializeState($model);
$app = $this->getApplication();
// Load the filter state.
$state->set('filter.search', $app->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', ''));
$state->set('filter.applied', $app->getUserStateFromRequest($this->context . '.filter.applied', 'filter_applied', ''));
$state->set('filter.branch', $app->getUserStateFromRequest($this->context . '.filter.branch', 'filter_branch', ''));
$state->set('filter.rtc', $app->getUserStateFromRequest($this->context . '.filter.rtc', 'filter_rtc', ''));
$state->set('filter.npm', $app->getUserStateFromRequest($this->context . '.filter.npm', 'filter_npm', ''));
$state->set('filter.label', $app->getUserStateFromRequest($this->context . '.filter.label', 'filter_label', ''));
// Pre-fill the limits.
$limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->input->get('list_limit', 20), 'uint');
$state->set('list.limit', $limit);
$fullOrdering = $app->getUserStateFromRequest($this->context . '.fullorder', 'list_fullordering', $this->defaultFullOrdering);
$orderingParts = explode(' ', $fullOrdering);
if (count($orderingParts) !== 2)
{
$fullOrdering = $this->defaultFullOrdering;
$orderingParts = explode(' ', $fullOrdering);
}
$state->set('list.fullordering', $fullOrdering);
// The 2nd part will be considered the direction
$direction = $orderingParts[array_key_last($orderingParts)];
if (in_array(strtoupper($direction), ['ASC', 'DESC', '']))
{
$state->set('list.direction', $direction);
}
// The 1st part will be the ordering
$ordering = $orderingParts[array_key_first($orderingParts)];
if (in_array($ordering, $model->getSortFields()))
{
$state->set('list.ordering', $ordering);
}
$value = $app->getUserStateFromRequest($this->context . '.limitstart', 'limitstart', 0);
$limitstart = ($limit != 0 ? (floor($value / $limit) * $limit) : 0);
$state->set('list.start', $limitstart);
return $state;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -13,6 +14,10 @@ use Joomla\CMS\Language\Text;
use Joomla\CMS\Response\JsonResponse; use Joomla\CMS\Response\JsonResponse;
use PatchTester\Model\PullsModel; use PatchTester\Model\PullsModel;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Controller class to fetch remote data * Controller class to fetch remote data
* *
@ -20,84 +25,66 @@ use PatchTester\Model\PullsModel;
*/ */
class FetchController extends AbstractController class FetchController extends AbstractController
{ {
/** /**
* Execute the controller. * Execute the controller.
* *
* @return void Redirects the application * @return void Redirects the application
* *
* @since 2.0 * @since 2.0
*/ */
public function execute() public function execute()
{ {
// We don't want this request to be cached. // We don't want this request to be cached.
$this->getApplication()->setHeader('Expires', 'Mon, 1 Jan 2001 00:00:00 GMT', true); $this->getApplication()->setHeader('Expires', 'Mon, 1 Jan 2001 00:00:00 GMT', true);
$this->getApplication()->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); $this->getApplication()->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true);
$this->getApplication()->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false); $this->getApplication()->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false);
$this->getApplication()->setHeader('Pragma', 'no-cache'); $this->getApplication()->setHeader('Pragma', 'no-cache');
$this->getApplication()->setHeader('Content-Type', $this->getApplication()->mimeType . '; charset=' . $this->getApplication()->charSet); $this->getApplication()->setHeader('Content-Type', $this->getApplication()->mimeType . '; charset=' . $this->getApplication()->charSet);
$session = Factory::getSession();
try {
// Fetch our page from the session
$page = $session->get('com_patchtester_fetcher_page', 1);
$model = new PullsModel();
// Initialize the state for the model
$state = $this->initializeState($model);
foreach ($state as $key => $value) {
$model->setState($key, $value);
}
$session = Factory::getSession(); $status = $model->requestFromGithub($page);
} catch (\Exception $e) {
$response = new JsonResponse($e);
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
try // Store the last page to the session if given one
{ if (isset($status['lastPage']) && $status['lastPage'] !== false) {
// Fetch our page from the session $session->set('com_patchtester_fetcher_last_page', $status['lastPage']);
$page = $session->get('com_patchtester_fetcher_page', 1); }
$model = new PullsModel; // Update the UI and session now
if ($status['complete'] || $page === $session->get('com_patchtester_fetcher_last_page', false)) {
$status['complete'] = true;
$status['header'] = Text::_('COM_PATCHTESTER_FETCH_SUCCESSFUL', true);
$message = Text::_('COM_PATCHTESTER_FETCH_COMPLETE_CLOSE_WINDOW', true);
} elseif (isset($status['page'])) {
$session->set('com_patchtester_fetcher_page', $status['page']);
$message = Text::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', $status['page']);
// Initialize the state for the model if ($session->has('com_patchtester_fetcher_last_page')) {
$state = $this->initializeState($model); $message = Text::sprintf(
'COM_PATCHTESTER_FETCH_PAGE_NUMBER_OF_TOTAL',
$status['page'],
$session->get('com_patchtester_fetcher_last_page')
);
}
}
foreach ($state as $key => $value) $response = new JsonResponse($status, $message, false, true);
{ $this->getApplication()->sendHeaders();
$model->setState($key, $value); echo json_encode($response);
} $this->getApplication()->close();
}
$status = $model->requestFromGithub($page);
}
catch (\Exception $e)
{
$response = new JsonResponse($e);
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
// Store the last page to the session if given one
if (isset($status['lastPage']) && $status['lastPage'] !== false)
{
$session->set('com_patchtester_fetcher_last_page', $status['lastPage']);
}
// Update the UI and session now
if ($status['complete'] || $page === $session->get('com_patchtester_fetcher_last_page', false))
{
$status['complete'] = true;
$status['header'] = Text::_('COM_PATCHTESTER_FETCH_SUCCESSFUL', true);
$message = Text::_('COM_PATCHTESTER_FETCH_COMPLETE_CLOSE_WINDOW', true);
}
elseif (isset($status['page']))
{
$session->set('com_patchtester_fetcher_page', $status['page']);
$message = Text::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', $status['page']);
if ($session->has('com_patchtester_fetcher_last_page'))
{
$message = Text::sprintf(
'COM_PATCHTESTER_FETCH_PAGE_NUMBER_OF_TOTAL', $status['page'], $session->get('com_patchtester_fetcher_last_page')
);
}
}
$response = new JsonResponse($status, $message, false, true);
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close();
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -18,6 +19,10 @@ use PatchTester\Model\PullModel;
use PatchTester\Model\PullsModel; use PatchTester\Model\PullsModel;
use PatchTester\Model\TestsModel; use PatchTester\Model\TestsModel;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Controller class to reset the system state * Controller class to reset the system state
* *
@ -25,131 +30,104 @@ use PatchTester\Model\TestsModel;
*/ */
class ResetController extends AbstractController class ResetController extends AbstractController
{ {
/** /**
* Execute the controller. * Execute the controller.
* *
* @return void Redirects the application * @return void Redirects the application
* *
* @since 2.0 * @since 2.0
*/ */
public function execute(): void public function execute(): void
{ {
try try {
{ $hasErrors = false;
$hasErrors = false; $revertErrored = false;
$revertErrored = false; $pullModel = new PullModel(null, Factory::getDbo());
$pullsModel = new PullsModel($this->context, null, Factory::getDbo());
$testsModel = new TestsModel(null, Factory::getDbo());
// Check the applied patches in the database first
$appliedPatches = $testsModel->getAppliedPatches();
$params = ComponentHelper::getParams('com_patchtester');
// Decide based on repository settings whether patch will be applied through Github or CIServer
if ((bool) $params->get('ci_switch', 1)) {
// Let's try to cleanly revert all applied patches with ci
foreach ($appliedPatches as $patch) {
try {
$pullModel->revertWithCIServer($patch->id);
} catch (\RuntimeException $e) {
$revertErrored = true;
}
}
} else {
// Let's try to cleanly revert all applied patches
foreach ($appliedPatches as $patch) {
try {
$pullModel->revertWithGitHub($patch->id);
} catch (\RuntimeException $e) {
$revertErrored = true;
}
}
}
$pullModel = new PullModel(null, Factory::getDbo()); // If we errored out reverting patches, we'll need to truncate the table
$pullsModel = new PullsModel($this->context, null, Factory::getDbo()); if ($revertErrored) {
$testsModel = new TestsModel(null, Factory::getDbo()); try {
$testsModel->truncateTable();
} catch (\RuntimeException $e) {
$hasErrors = true;
// Check the applied patches in the database first $this->getApplication()->enqueueMessage(
$appliedPatches = $testsModel->getAppliedPatches(); Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_PULLS_TABLE', $e->getMessage()),
'error'
);
}
}
$params = ComponentHelper::getParams('com_patchtester'); // Now truncate the pulls table
try {
$pullsModel->truncateTable();
} catch (\RuntimeException $e) {
$hasErrors = true;
// Decide based on repository settings whether patch will be applied through Github or CIServer $this->getApplication()->enqueueMessage(
if ((bool) $params->get('ci_switch', 1)) Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_TESTS_TABLE', $e->getMessage()),
{ 'error'
// Let's try to cleanly revert all applied patches with ci );
foreach ($appliedPatches as $patch) }
{
try
{
$pullModel->revertWithCIServer($patch->id);
}
catch (\RuntimeException $e)
{
$revertErrored = true;
}
}
}
else
{
// Let's try to cleanly revert all applied patches
foreach ($appliedPatches as $patch)
{
try
{
$pullModel->revertWithGitHub($patch->id);
}
catch (\RuntimeException $e)
{
$revertErrored = true;
}
}
}
// If we errored out reverting patches, we'll need to truncate the table // Check the backups directory to see if any .txt files remain; clear them if so
if ($revertErrored) $backups = Folder::files(JPATH_COMPONENT . '/backups', '.txt');
{
try
{
$testsModel->truncateTable();
}
catch (\RuntimeException $e)
{
$hasErrors = true;
$this->getApplication()->enqueueMessage( if (count($backups)) {
Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_PULLS_TABLE', $e->getMessage()), 'error' foreach ($backups as $file) {
); if (!File::delete(JPATH_COMPONENT . '/backups/' . $file)) {
} $this->getApplication()->enqueueMessage(
} Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_COMPONENT . '/backups/' . $file),
'error'
);
$hasErrors = true;
}
}
}
// Now truncate the pulls table // Processing completed, inform the user of a success or fail
try if ($hasErrors) {
{ $msg = Text::sprintf(
$pullsModel->truncateTable(); 'COM_PATCHTESTER_RESET_HAS_ERRORS',
} JPATH_COMPONENT . '/backups',
catch (\RuntimeException $e) Factory::getDbo()->replacePrefix('#__patchtester_tests')
{ );
$hasErrors = true; $type = 'warning';
} else {
$msg = Text::_('COM_PATCHTESTER_RESET_OK');
$type = 'notice';
}
} catch (\Exception $exception) {
$msg = $exception->getMessage();
$type = 'error';
}
$this->getApplication()->enqueueMessage( $this->getApplication()->enqueueMessage($msg, $type);
Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_TESTS_TABLE', $e->getMessage()), 'error' $this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false));
); }
}
// Check the backups directory to see if any .txt files remain; clear them if so
$backups = Folder::files(JPATH_COMPONENT . '/backups', '.txt');
if (count($backups))
{
foreach ($backups as $file)
{
if (!File::delete(JPATH_COMPONENT . '/backups/' . $file))
{
$this->getApplication()->enqueueMessage(
Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_COMPONENT . '/backups/' . $file), 'error'
);
$hasErrors = true;
}
}
}
// Processing completed, inform the user of a success or fail
if ($hasErrors)
{
$msg = Text::sprintf(
'COM_PATCHTESTER_RESET_HAS_ERRORS', JPATH_COMPONENT . '/backups', Factory::getDbo()->replacePrefix('#__patchtester_tests')
);
$type = 'warning';
}
else
{
$msg = Text::_('COM_PATCHTESTER_RESET_OK');
$type = 'notice';
}
}
catch (\Exception $exception)
{
$msg = $exception->getMessage();
$type = 'error';
}
$this->getApplication()->enqueueMessage($msg, $type);
$this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false));
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -13,6 +14,10 @@ use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use PatchTester\Model\PullModel; use PatchTester\Model\PullModel;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Controller class to revert patches * Controller class to revert patches
* *
@ -20,34 +25,28 @@ use PatchTester\Model\PullModel;
*/ */
class RevertController extends AbstractController class RevertController extends AbstractController
{ {
/** /**
* Execute the controller. * Execute the controller.
* *
* @return void Redirects the application * @return void Redirects the application
* *
* @since 2.0 * @since 2.0
*/ */
public function execute() public function execute()
{ {
try try {
{ $model = new PullModel(null, Factory::getDbo());
$model = new PullModel(null, Factory::getDbo()); // Initialize the state for the model
$model->setState($this->initializeState($model));
$model->revert($this->getInput()->getUint('pull_id'));
$msg = Text::_('COM_PATCHTESTER_REVERT_OK');
$type = 'message';
} catch (\Exception $e) {
$msg = $e->getMessage();
$type = 'error';
}
// Initialize the state for the model $this->getApplication()->enqueueMessage($msg, $type);
$model->setState($this->initializeState($model)); $this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false));
}
$model->revert($this->getInput()->getUint('pull_id'));
$msg = Text::_('COM_PATCHTESTER_REVERT_OK');
$type = 'message';
}
catch (\Exception $e)
{
$msg = $e->getMessage();
$type = 'error';
}
$this->getApplication()->enqueueMessage($msg, $type);
$this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false));
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -15,6 +16,10 @@ use Joomla\CMS\Session\Session;
use PatchTester\Helper; use PatchTester\Helper;
use PatchTester\Model\TestsModel; use PatchTester\Model\TestsModel;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Controller class to start fetching remote data * Controller class to start fetching remote data
* *
@ -22,109 +27,69 @@ use PatchTester\Model\TestsModel;
*/ */
class StartfetchController extends AbstractController class StartfetchController extends AbstractController
{ {
/** /**
* Execute the controller. * Execute the controller.
* *
* @return void Redirects the application * @return void Redirects the application
* *
* @since 2.0 * @since 2.0
*/ */
public function execute() public function execute()
{ {
// We don't want this request to be cached. // We don't want this request to be cached.
$this->getApplication()->setHeader('Expires', 'Mon, 1 Jan 2001 00:00:00 GMT', true); $this->getApplication()->setHeader('Expires', 'Mon, 1 Jan 2001 00:00:00 GMT', true);
$this->getApplication()->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true); $this->getApplication()->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true);
$this->getApplication()->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false); $this->getApplication()->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false);
$this->getApplication()->setHeader('Pragma', 'no-cache'); $this->getApplication()->setHeader('Pragma', 'no-cache');
$this->getApplication()->setHeader('Content-Type', $this->getApplication()->mimeType . '; charset=' . $this->getApplication()->charSet); $this->getApplication()->setHeader('Content-Type', $this->getApplication()->mimeType . '; charset=' . $this->getApplication()->charSet);
// Check for a valid token. If invalid, send a 403 with the error message.
if (!Session::checkToken('request')) {
$response = new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403));
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
// Check for a valid token. If invalid, send a 403 with the error message. // Make sure we can fetch the data from GitHub - throw an error on < 10 available requests
if (!Session::checkToken('request')) try {
{ $rateResponse = Helper::initializeGithub()->getRateLimit();
$response = new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403)); $rate = json_decode($rateResponse->body);
} catch (\Exception $e) {
$response = new JsonResponse(new \Exception(Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e));
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
$this->getApplication()->sendHeaders(); // If over the API limit, we can't build this list
echo json_encode($response); if ($rate->resources->core->remaining < 10) {
$response = new JsonResponse(new \Exception(Text::sprintf('COM_PATCHTESTER_API_LIMIT_LIST', Factory::getDate($rate->resources->core->reset)), 429));
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
$this->getApplication()->close(1); $testsModel = new TestsModel(null, Factory::getDbo());
} try {
// Sanity check, ensure there aren't any applied patches
if (count($testsModel->getAppliedPatches()) >= 1) {
$response = new JsonResponse(new \Exception(Text::_('COM_PATCHTESTER_ERROR_APPLIED_PATCHES'), 500));
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
} catch (\Exception $e) {
$response = new JsonResponse($e);
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
// Make sure we can fetch the data from GitHub - throw an error on < 10 available requests // We're able to successfully pull data, prepare our environment
try Factory::getSession()->set('com_patchtester_fetcher_page', 1);
{ $response = new JsonResponse(array('complete' => false, 'header' => Text::_('COM_PATCHTESTER_FETCH_PROCESSING', true)), Text::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', 1), false, true);
$rateResponse = Helper::initializeGithub()->getRateLimit(); $this->getApplication()->sendHeaders();
$rate = json_decode($rateResponse->body); echo json_encode($response);
} $this->getApplication()->close();
catch (\Exception $e) }
{
$response = new JsonResponse(
new \Exception(
Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()),
$e->getCode(),
$e
)
);
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
// If over the API limit, we can't build this list
if ($rate->resources->core->remaining < 10)
{
$response = new JsonResponse(
new \Exception(
Text::sprintf('COM_PATCHTESTER_API_LIMIT_LIST', Factory::getDate($rate->resources->core->reset)),
429
)
);
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
$testsModel = new TestsModel(null, Factory::getDbo());
try
{
// Sanity check, ensure there aren't any applied patches
if (count($testsModel->getAppliedPatches()) >= 1)
{
$response = new JsonResponse(new \Exception(Text::_('COM_PATCHTESTER_ERROR_APPLIED_PATCHES'), 500));
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
}
catch (\Exception $e)
{
$response = new JsonResponse($e);
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
}
// We're able to successfully pull data, prepare our environment
Factory::getSession()->set('com_patchtester_fetcher_page', 1);
$response = new JsonResponse(
array('complete' => false, 'header' => Text::_('COM_PATCHTESTER_FETCH_PROCESSING', true)),
Text::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', 1),
false,
true
);
$this->getApplication()->sendHeaders();
echo json_encode($response);
$this->getApplication()->close();
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -11,7 +12,9 @@ namespace PatchTester\Field;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Form\Field\ListField; use Joomla\CMS\Form\Field\ListField;
defined('_JEXEC') or die; // phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* List of available branches. * List of available branches.
@ -21,34 +24,30 @@ defined('_JEXEC') or die;
*/ */
class BranchField extends ListField class BranchField extends ListField
{ {
/** /**
* Type of field * Type of field
* *
* @var string * @var string
* @since 4.1.0 * @since 4.1.0
*/ */
protected $type = 'Branch'; protected $type = 'Branch';
/**
/** * Build a list of available branches.
* Build a list of available branches. *
* * @return array List of options
* @return array List of options *
* * @since 4.1.0
* @since 4.1.0 */
*/ public function getOptions(): array
public function getOptions(): array {
{ $db = Factory::getContainer()->get('DatabaseDriver');
$db = Factory::getContainer()->get('DatabaseDriver'); $query = $db->getQuery(true);
$query = $db->getQuery(true); $query->select('DISTINCT(' . $db->quoteName('branch') . ') AS ' . $db->quoteName('text'))
->select($db->quoteName('branch', 'value'))
$query->select('DISTINCT(' . $db->quoteName('branch') . ') AS ' . $db->quoteName('text')) ->from('#__patchtester_pulls')
->select($db->quoteName('branch', 'value')) ->where($db->quoteName('branch') . ' != ' . $db->quote(''))
->from('#__patchtester_pulls') ->order($db->quoteName('branch') . ' ASC');
->where($db->quoteName('branch') . ' != ' . $db->quote('')) $options = $db->setQuery($query)->loadAssocList();
->order($db->quoteName('branch') . ' ASC'); return array_merge(parent::getOptions(), $options);
}
$options = $db->setQuery($query)->loadAssocList();
return array_merge(parent::getOptions(), $options);
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -11,7 +12,9 @@ namespace PatchTester\Field;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Form\Field\ListField; use Joomla\CMS\Form\Field\ListField;
defined('_JEXEC') or die; // phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* List of available banks. * List of available banks.
@ -21,33 +24,29 @@ defined('_JEXEC') or die;
*/ */
class LabelField extends ListField class LabelField extends ListField
{ {
/** /**
* Type of field * Type of field
* *
* @var string * @var string
* @since 4.1.0 * @since 4.1.0
*/ */
protected $type = 'Label'; protected $type = 'Label';
/**
/** * Build a list of available fields.
* Build a list of available fields. *
* * @return array List of options
* @return array List of options *
* * @since 4.1.0
* @since 4.1.0 */
*/ public function getOptions(): array
public function getOptions(): array {
{ $db = Factory::getContainer()->get('DatabaseDriver');
$db = Factory::getContainer()->get('DatabaseDriver'); $query = $db->getQuery(true);
$query = $db->getQuery(true); $query->select('DISTINCT(' . $db->quoteName('name') . ') AS ' . $db->quoteName('text'))
->select($db->quoteName('name', 'value'))
$query->select('DISTINCT(' . $db->quoteName('name') . ') AS ' . $db->quoteName('text')) ->from($db->quoteName('#__patchtester_pulls_labels'))
->select($db->quoteName('name', 'value')) ->order($db->quoteName('name') . ' ASC');
->from($db->quoteName('#__patchtester_pulls_labels')) $options = $db->setQuery($query)->loadAssocList();
->order($db->quoteName('name') . ' ASC'); return array_merge(parent::getOptions(), $options);
}
$options = $db->setQuery($query)->loadAssocList();
return array_merge(parent::getOptions(), $options);
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -17,40 +18,38 @@ use Joomla\CMS\Http\Response;
*/ */
class UnexpectedResponse extends \DomainException class UnexpectedResponse extends \DomainException
{ {
/** /**
* The Response object. * The Response object.
* *
* @var Response * @var Response
* @since 3.0.0 * @since 3.0.0
*/ */
private $response; private $response;
/**
* Constructor
*
* @param Response $response The Response object.
* @param string $message The Exception message to throw.
* @param integer $code The Exception code.
* @param \Exception $previous The previous exception used for the exception chaining.
*
* @since 3.0.0
*/
public function __construct(Response $response, $message = '', $code = 0, \Exception $previous = null)
{
parent::__construct($message, $code, $previous);
$this->response = $response;
}
/** /**
* Constructor * Get the Response object.
* *
* @param Response $response The Response object. * @return Response
* @param string $message The Exception message to throw. *
* @param integer $code The Exception code. * @since 3.0.0
* @param \Exception $previous The previous exception used for the exception chaining. */
* public function getResponse()
* @since 3.0.0 {
*/ return $this->response;
public function __construct(Response $response, $message = '', $code = 0, \Exception $previous = null) }
{
parent::__construct($message, $code, $previous);
$this->response = $response;
}
/**
* Get the Response object.
*
* @return Response
*
* @since 3.0.0
*/
public function getResponse()
{
return $this->response;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -21,332 +22,304 @@ use Joomla\Registry\Registry;
*/ */
class GitHub class GitHub
{ {
/** /**
* Options for the connector. * Options for the connector.
* *
* @var Registry * @var Registry
* @since 3.0.0 * @since 3.0.0
*/ */
protected $options; protected $options;
/**
* The HTTP client object to use in sending HTTP requests.
*
* @var Http
* @since 3.0.0
*/
protected $client;
/**
* Constructor.
*
* @param Registry $options Connector options.
* @param Http $client The HTTP client object.
*
* @since 3.0.0
*/
public function __construct(Registry $options = null, Http $client = null)
{
$this->options = $options ?: new Registry();
$this->client = $client ?: HttpFactory::getHttp($options);
}
/** /**
* The HTTP client object to use in sending HTTP requests. * Get the HTTP client for this connector.
* *
* @var Http * @return Http
* @since 3.0.0 *
*/ * @since 3.0.0
protected $client; */
public function getClient()
{
return $this->client;
}
/** /**
* Constructor. * Get the diff for a pull request.
* *
* @param Registry $options Connector options. * @param string $user The name of the owner of the GitHub repository.
* @param Http $client The HTTP client object. * @param string $repo The name of the GitHub repository.
* * @param integer $pullId The pull request number.
* @since 3.0.0 *
*/ * @return Response
public function __construct(Registry $options = null, Http $client = null) *
{ * @since 3.0.0
$this->options = $options ?: new Registry; */
$this->client = $client ?: HttpFactory::getHttp($options); public function getDiffForPullRequest($user, $repo, $pullId)
} {
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId;
// Build the request headers.
$headers = array('Accept' => 'application/vnd.github.diff');
$prepared = $this->prepareRequest($path, 0, 0, $headers);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
}
/** /**
* Get the HTTP client for this connector. * Method to build and return a full request URL for the request.
* *
* @return Http * This method will add appropriate pagination details if necessary and also prepend the API url to have a complete URL for the request.
* *
* @since 3.0.0 * @param string $path Path to process
*/ * @param integer $page Page to request
public function getClient() * @param integer $limit Number of results to return per page
{ * @param array $headers The headers to send with the request
return $this->client; *
} * @return array Associative array containing the prepared URL and request headers
*
* @since 3.0.0
*/
protected function prepareRequest(
$path,
$page = 0,
$limit = 0,
array $headers = array()
) {
$url = $this->fetchUrl($path, $page, $limit);
if ($token = $this->options->get('gh.token', false)) {
$headers['Authorization'] = "token $token";
}
/** return array('url' => $url, 'headers' => $headers);
* Get the diff for a pull request. }
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number.
*
* @return Response
*
* @since 3.0.0
*/
public function getDiffForPullRequest($user, $repo, $pullId)
{
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId;
// Build the request headers. /**
$headers = array('Accept' => 'application/vnd.github.diff'); * Build and return a full request URL.
*
* This method will add appropriate pagination details and basic authentication credentials if necessary
* and also prepend the API url to have a complete URL for the request.
*
* @param string $path URL to inflect
* @param integer $page Page to request
* @param integer $limit Number of results to return per page
*
* @return string The request URL.
*
* @since 3.0.0
*/
protected function fetchUrl($path, $page = 0, $limit = 0)
{
// Get a new Uri object using the API URL and given path.
$uri = new Uri($this->options->get('api.url') . $path);
// If we have a defined page number add it to the JUri object.
if ($page > 0) {
$uri->setVar('page', (int) $page);
}
$prepared = $this->prepareRequest($path, 0, 0, $headers); // If we have a defined items per page add it to the JUri object.
if ($limit > 0) {
$uri->setVar('per_page', (int) $limit);
}
return $this->processResponse( return (string) $uri;
$this->client->get($prepared['url'], $prepared['headers']) }
);
}
/** /**
* Method to build and return a full request URL for the request. * Process the response and return it.
* *
* This method will add appropriate pagination details if necessary and also prepend the API url to have a complete URL for the request. * @param Response $response The response.
* * @param integer $expectedCode The expected response code.
* @param string $path Path to process *
* @param integer $page Page to request * @return Response
* @param integer $limit Number of results to return per page *
* @param array $headers The headers to send with the request * @since 3.0.0
* * @throws Exception\UnexpectedResponse
* @return array Associative array containing the prepared URL and request headers */
* protected function processResponse(Response $response, $expectedCode = 200)
* @since 3.0.0 {
*/ // Validate the response code.
protected function prepareRequest($path, $page = 0, $limit = 0, if ($response->code != $expectedCode) {
array $headers = array() // Decode the error response and throw an exception.
) { $body = json_decode($response->body);
$url = $this->fetchUrl($path, $page, $limit); $error = isset($body->error) ? $body->error
: (isset($body->message) ? $body->message : 'Unknown Error');
if ($token = $this->options->get('gh.token', false)) throw new Exception\UnexpectedResponse(
{ $response,
$headers['Authorization'] = "token $token"; $error,
} $response->code
);
}
return array('url' => $url, 'headers' => $headers); return $response;
} }
/** /**
* Build and return a full request URL. * Get a file's contents from a repository.
* *
* This method will add appropriate pagination details and basic authentication credentials if necessary * @param string $user The name of the owner of the GitHub repository.
* and also prepend the API url to have a complete URL for the request. * @param string $repo The name of the GitHub repository.
* * @param string $path The content path.
* @param string $path URL to inflect * @param string $ref The name of the commit/branch/tag. Default: the repositorys default branch (usually master)
* @param integer $page Page to request *
* @param integer $limit Number of results to return per page * @return Response
* *
* @return string The request URL. * @since 3.0.0
* */
* @since 3.0.0 public function getFileContents($user, $repo, $path, $ref = null)
*/ {
protected function fetchUrl($path, $page = 0, $limit = 0) $path = "/repos/$user/$repo/contents/$path";
{ $prepared = $this->prepareRequest($path);
// Get a new Uri object using the API URL and given path. if ($ref) {
$uri = new Uri($this->options->get('api.url') . $path); $url = new Uri($prepared['url']);
$url->setVar('ref', $ref);
$prepared['url'] = (string) $url;
}
// If we have a defined page number add it to the JUri object. return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
if ($page > 0) }
{
$uri->setVar('page', (int) $page);
}
// If we have a defined items per page add it to the JUri object. /**
if ($limit > 0) * Get the list of modified files for a pull request.
{ *
$uri->setVar('per_page', (int) $limit); * @param string $user The name of the owner of the GitHub repository.
} * @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number.
*
* @return Response
*
* @since 3.0.0
*/
public function getFilesForPullRequest($user, $repo, $pullId, $page = 1)
{
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId . '/files?page=' . $page;
$prepared = $this->prepareRequest($path);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
}
return (string) $uri; /**
} * Get a list of the open issues for a repository.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $page The page number from which to get items.
* @param integer $limit The number of items on a page.
*
* @return Response
*
* @since 3.0.0
*/
public function getOpenIssues($user, $repo, $page = 0, $limit = 0)
{
$prepared = $this->prepareRequest(
"/repos/$user/$repo/issues",
$page,
$limit
);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
}
/** /**
* Process the response and return it. * Get a list of the open pull requests for a repository.
* *
* @param Response $response The response. * @param string $user The name of the owner of the GitHub repository.
* @param integer $expectedCode The expected response code. * @param string $repo The name of the GitHub repository.
* * @param integer $page The page number from which to get items.
* @return Response * @param integer $limit The number of items on a page.
* *
* @since 3.0.0 * @return Response
* @throws Exception\UnexpectedResponse *
*/ * @since 3.0.0
protected function processResponse(Response $response, $expectedCode = 200) */
{ public function getOpenPulls($user, $repo, $page = 0, $limit = 0)
// Validate the response code. {
if ($response->code != $expectedCode) $prepared = $this->prepareRequest(
{ "/repos/$user/$repo/pulls",
// Decode the error response and throw an exception. $page,
$body = json_decode($response->body); $limit
$error = isset($body->error) ? $body->error );
: (isset($body->message) ? $body->message : 'Unknown Error'); return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
}
throw new Exception\UnexpectedResponse( /**
$response, $error, $response->code * Get an option from the connector.
); *
} * @param string $key The name of the option to get.
* @param mixed $default The default value if the option is not set.
*
* @return mixed The option value.
*
* @since 3.0.0
*/
public function getOption($key, $default = null)
{
return $this->options->get($key, $default);
}
return $response; /**
} * Get a single pull request.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number.
*
* @return Response
*
* @since 3.0.0
*/
public function getPullRequest($user, $repo, $pullId)
{
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId;
$prepared = $this->prepareRequest($path);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
}
/** /**
* Get a file's contents from a repository. * Get the rate limit for the authenticated user.
* *
* @param string $user The name of the owner of the GitHub repository. * @return Response
* @param string $repo The name of the GitHub repository. *
* @param string $path The content path. * @since 3.0.0
* @param string $ref The name of the commit/branch/tag. Default: the repositorys default branch (usually master) */
* public function getRateLimit()
* @return Response {
* $prepared = $this->prepareRequest('/rate_limit');
* @since 3.0.0 return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
*/ }
public function getFileContents($user, $repo, $path, $ref = null)
{
$path = "/repos/$user/$repo/contents/$path";
$prepared = $this->prepareRequest($path); /**
* Set an option for the connector.
if ($ref) *
{ * @param string $key The name of the option to set.
$url = new Uri($prepared['url']); * @param mixed $value The option value to set.
$url->setVar('ref', $ref); *
* @return $this
$prepared['url'] = (string) $url; *
} * @since 3.0.0
*/
return $this->processResponse( public function setOption($key, $value)
$this->client->get($prepared['url'], $prepared['headers']) {
); $this->options->set($key, $value);
} return $this;
}
/**
* Get the list of modified files for a pull request.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number.
*
* @return Response
*
* @since 3.0.0
*/
public function getFilesForPullRequest($user, $repo, $pullId, $page = 1)
{
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId . '/files?page=' . $page;
$prepared = $this->prepareRequest($path);
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
}
/**
* Get a list of the open issues for a repository.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $page The page number from which to get items.
* @param integer $limit The number of items on a page.
*
* @return Response
*
* @since 3.0.0
*/
public function getOpenIssues($user, $repo, $page = 0, $limit = 0)
{
$prepared = $this->prepareRequest(
"/repos/$user/$repo/issues", $page, $limit
);
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
}
/**
* Get a list of the open pull requests for a repository.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $page The page number from which to get items.
* @param integer $limit The number of items on a page.
*
* @return Response
*
* @since 3.0.0
*/
public function getOpenPulls($user, $repo, $page = 0, $limit = 0)
{
$prepared = $this->prepareRequest(
"/repos/$user/$repo/pulls", $page, $limit
);
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
}
/**
* Get an option from the connector.
*
* @param string $key The name of the option to get.
* @param mixed $default The default value if the option is not set.
*
* @return mixed The option value.
*
* @since 3.0.0
*/
public function getOption($key, $default = null)
{
return $this->options->get($key, $default);
}
/**
* Get a single pull request.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number.
*
* @return Response
*
* @since 3.0.0
*/
public function getPullRequest($user, $repo, $pullId)
{
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId;
$prepared = $this->prepareRequest($path);
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
}
/**
* Get the rate limit for the authenticated user.
*
* @return Response
*
* @since 3.0.0
*/
public function getRateLimit()
{
$prepared = $this->prepareRequest('/rate_limit');
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
}
/**
* Set an option for the connector.
*
* @param string $key The name of the option to set.
* @param mixed $value The option value to set.
*
* @return $this
*
* @since 3.0.0
*/
public function setOption($key, $value)
{
$this->options->set($key, $value);
return $this;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -14,6 +15,10 @@ use Joomla\CMS\Language\Text;
use Joomla\Registry\Registry; use Joomla\Registry\Registry;
use PatchTester\GitHub\GitHub; use PatchTester\GitHub\GitHub;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Helper class for the patch tester component * Helper class for the patch tester component
* *
@ -21,69 +26,55 @@ use PatchTester\GitHub\GitHub;
*/ */
abstract class Helper abstract class Helper
{ {
/** /**
* Initializes the GitHub object * Initializes the GitHub object
* *
* @return GitHub * @return GitHub
* *
* @since 2.0 * @since 2.0
*/ */
public static function initializeGithub() public static function initializeGithub()
{ {
$params = ComponentHelper::getParams('com_patchtester'); $params = ComponentHelper::getParams('com_patchtester');
$options = new Registry();
// Set a user agent for the request
$options->set('userAgent', 'PatchTester/3.0');
// Set the default timeout to 120 seconds
$options->set('timeout', 120);
// Set the API URL
$options->set('api.url', 'https://api.github.com');
// If an API token is set in the params, use it for authentication
if ($params->get('gh_token', '')) {
$options->set('headers', ['Authorization' => 'token ' . $params->get('gh_token', '')]);
} else {
// Display a message about the lowered API limit without credentials
Factory::getApplication()->enqueueMessage(Text::_('COM_PATCHTESTER_NO_CREDENTIALS'), 'notice');
}
$options = new Registry; return new GitHub($options);
}
// Set a user agent for the request /**
$options->set('userAgent', 'PatchTester/3.0'); * Initializes the CI Settings
*
// Set the default timeout to 120 seconds * @return Registry
$options->set('timeout', 120); *
* @since 3.0
// Set the API URL */
$options->set('api.url', 'https://api.github.com'); public static function initializeCISettings()
{
// If an API token is set in the params, use it for authentication $params = ComponentHelper::getParams('com_patchtester');
if ($params->get('gh_token', '')) $options = new Registry();
{ // Set CI server address for the request
$options->set('headers', ['Authorization' => 'token ' . $params->get('gh_token', '')]); $options->set('server.url', $params->get('ci_server', 'https://ci.joomla.org:444'));
} // Set name of the zip archive
// Display a message about the lowered API limit without credentials $options->set('zip.name', 'build.zip');
else $options->set('zip.log.name', 'deleted_files.log');
{ // Set temp archive for extracting and downloading files
Factory::getApplication()->enqueueMessage(Text::_('COM_PATCHTESTER_NO_CREDENTIALS'), 'notice'); $options->set('folder.temp', Factory::getConfig()->get('tmp_path'));
} $options->set('folder.backups', JPATH_COMPONENT . '/backups');
// Set full url for addressing the file
return new GitHub($options); $options->set('zip.url', $options->get('server.url') . '/artifacts/joomla/joomla-cms/4.0-dev/%s/patchtester/' . $options->get('zip.name'));
} return $options;
}
/**
* Initializes the CI Settings
*
* @return Registry
*
* @since 3.0
*/
public static function initializeCISettings()
{
$params = ComponentHelper::getParams('com_patchtester');
$options = new Registry;
// Set CI server address for the request
$options->set('server.url', $params->get('ci_server', 'https://ci.joomla.org:444'));
// Set name of the zip archive
$options->set('zip.name', 'build.zip');
$options->set('zip.log.name', 'deleted_files.log');
// Set temp archive for extracting and downloading files
$options->set('folder.temp', Factory::getConfig()->get('tmp_path'));
$options->set('folder.backups', JPATH_COMPONENT . '/backups');
// Set full url for addressing the file
$options->set('zip.url', $options->get('server.url') . '/artifacts/joomla/joomla-cms/4.0-dev/%s/patchtester/' . $options->get('zip.name'));
return $options;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -18,85 +19,83 @@ use Joomla\Registry\Registry;
*/ */
abstract class AbstractModel abstract class AbstractModel
{ {
/** /**
* The database driver. * The database driver.
* *
* @var \JDatabaseDriver * @var \JDatabaseDriver
* @since 4.0.0 * @since 4.0.0
*/ */
protected $db; protected $db;
/**
* The model state.
*
* @var Registry
* @since 4.0.0
*/
protected $state;
/**
* Instantiate the model.
*
* @param Registry $state The model state.
* @param \JDatabaseDriver $db The database adpater.
*
* @since 4.0.0
*/
public function __construct(Registry $state = null, \JDatabaseDriver $db = null)
{
$this->state = $state ?: new Registry();
$this->db = $db ?: Factory::getDbo();
}
/** /**
* The model state. * Get the database driver.
* *
* @var Registry * @return \JDatabaseDriver
* @since 4.0.0 *
*/ * @since 4.0.0
protected $state; */
public function getDb()
{
return $this->db;
}
/** /**
* Instantiate the model. * Get the model state.
* *
* @param Registry $state The model state. * @return Registry
* @param \JDatabaseDriver $db The database adpater. *
* * @since 4.0.0
* @since 4.0.0 */
*/ public function getState()
public function __construct(Registry $state = null, \JDatabaseDriver $db = null) {
{ return $this->state;
$this->state = $state ?: new Registry; }
$this->db = $db ?: Factory::getDbo();
}
/** /**
* Get the database driver. * Set the database driver.
* *
* @return \JDatabaseDriver * @param \JDatabaseDriver $db The database driver.
* *
* @since 4.0.0 * @return void
*/ *
public function getDb() * @since 4.0.0
{ */
return $this->db; public function setDb(\JDatabaseDriver $db)
} {
$this->db = $db;
}
/** /**
* Get the model state. * Set the model state.
* *
* @return Registry * @param Registry $state The state object.
* *
* @since 4.0.0 * @return void
*/ *
public function getState() * @since 4.0.0
{ */
return $this->state; public function setState(Registry $state)
} {
$this->state = $state;
/** }
* Set the database driver.
*
* @param \JDatabaseDriver $db The database driver.
*
* @return void
*
* @since 4.0.0
*/
public function setDb(\JDatabaseDriver $db)
{
$this->db = $db;
}
/**
* Set the model state.
*
* @param Registry $state The state object.
*
* @return void
*
* @since 4.0.0
*/
public function setState(Registry $state)
{
$this->state = $state;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -8,6 +9,10 @@
namespace PatchTester\Model; namespace PatchTester\Model;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Model class for the fetch modal view * Model class for the fetch modal view
* *

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -15,6 +16,10 @@ use Joomla\CMS\MVC\Model\ListModel;
use PatchTester\GitHub\Exception\UnexpectedResponse; use PatchTester\GitHub\Exception\UnexpectedResponse;
use PatchTester\Helper; use PatchTester\Helper;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Model class for the pulls list view * Model class for the pulls list view
* *
@ -22,533 +27,403 @@ use PatchTester\Helper;
*/ */
class PullsModel extends ListModel class PullsModel extends ListModel
{ {
/** /**
* The object context * The object context
* *
* @var string * @var string
* @since 2.0 * @since 2.0
*/ */
protected $context; protected $context;
/**
/** * Array of fields the list can be sorted on
* Array of fields the list can be sorted on *
* * @var array
* @var array * @since 2.0
* @since 2.0 */
*/ protected $sortFields = array('pulls.pull_id', 'pulls.title');
protected $sortFields = array('pulls.pull_id', 'pulls.title'); /**
* Constructor.
/** *
* Constructor. * @param array $config An optional associative array of configuration settings.
* *
* @param array $config An optional associative array of configuration settings. * @since 4.0.0
* * @throws Exception
* @since 4.0.0 *
* @throws Exception */
* public function __construct($config = [])
*/ {
public function __construct($config = []) $config = [];
{ if (empty($config['filter_fields'])) {
$config = []; $config['filter_fields'] = [
'applied',
if (empty($config['filter_fields'])) 'rtc',
{ 'npm',
$config['filter_fields'] = [ 'draft',
'applied', 'label',
'rtc', 'branch',
'npm', ];
'draft', }
'label',
'branch', parent::__construct($config);
]; }
}
/**
parent::__construct($config); * Method to get an array of data items.
} *
* @return mixed An array of data items on success, false on failure.
/** *
* Method to get an array of data items. * @since 2.0
* */
* @return mixed An array of data items on success, false on failure. public function getItems()
* {
* @since 2.0 $store = $this->getStoreId();
*/ if (isset($this->cache[$store])) {
public function getItems() return $this->cache[$store];
{ }
$store = $this->getStoreId();
$items = $this->getList(
if (isset($this->cache[$store])) $this->getListQueryCache(),
{ $this->getStart(),
return $this->cache[$store]; $this->getState()->get('list.limit')
} );
$db = $this->getDbo();
$items = $this->getList( $query = $db->getQuery(true)
$this->getListQueryCache(), $this->getStart(), ->select($db->quoteName(['name', 'color']))
$this->getState()->get('list.limit') ->from($db->quoteName('#__patchtester_pulls_labels'));
); array_walk($items, static function ($item) use ($db, $query) {
$db = $this->getDbo(); $query->clear('where');
$query = $db->getQuery(true) $query->where($db->quoteName('pull_id') . ' = ' . $item->pull_id);
->select($db->quoteName(['name', 'color'])) $db->setQuery($query);
->from($db->quoteName('#__patchtester_pulls_labels')); $item->labels = $db->loadObjectList();
});
array_walk( $this->cache[$store] = $items;
$items, return $this->cache[$store];
static function ($item) use ($db, $query) { }
$query->clear('where');
$query->where( /**
$db->quoteName('pull_id') . ' = ' . $item->pull_id * Method to get a store id based on the model configuration state.
); *
$db->setQuery($query); * This is necessary because the model is used by the component and
* different modules that might need different sets of data or different
$item->labels = $db->loadObjectList(); * ordering requirements.
} *
); * @param string $id An identifier string to generate the store id.
*
$this->cache[$store] = $items; * @return string A store id.
*
return $this->cache[$store]; * @since 2.0
} */
protected function getStoreId($id = '')
/** {
* Method to get a store id based on the model configuration state. // Add the list state to the store id.
* $id .= ':' . $this->getState()->get('list.start');
* This is necessary because the model is used by the component and $id .= ':' . $this->getState()->get('list.limit');
* different modules that might need different sets of data or different $id .= ':' . $this->getState()->get('list.ordering');
* ordering requirements. $id .= ':' . $this->getState()->get('list.direction');
* return md5($this->context . ':' . $id);
* @param string $id An identifier string to generate the store id. }
*
* @return string A store id. /**
* * Gets an array of objects from the results of database query.
* @since 2.0 *
*/ * @param \JDatabaseQuery|string $query The query.
protected function getStoreId($id = '') * @param integer $limitstart Offset.
{ * @param integer $limit The number of records.
// Add the list state to the store id. *
$id .= ':' . $this->getState()->get('list.start'); * @return array An array of results.
$id .= ':' . $this->getState()->get('list.limit'); *
$id .= ':' . $this->getState()->get('list.ordering'); * @since 2.0
$id .= ':' . $this->getState()->get('list.direction'); * @throws RuntimeException
*/
return md5($this->context . ':' . $id); protected function getList($query, $limitstart = 0, $limit = 0)
} {
return $this->getDbo()->setQuery($query, $limitstart, $limit)
/** ->loadObjectList();
* Gets an array of objects from the results of database query. }
*
* @param \JDatabaseQuery|string $query The query. /**
* @param integer $limitstart Offset. * Method to cache the last query constructed.
* @param integer $limit The number of records. *
* * This method ensures that the query is constructed only once for a given state of the model.
* @return array An array of results. *
* * @return \JDatabaseQuery A JDatabaseQuery object
* @since 2.0 *
* @throws RuntimeException * @since 2.0
*/ */
protected function getList($query, $limitstart = 0, $limit = 0) protected function getListQueryCache()
{ {
return $this->getDbo()->setQuery($query, $limitstart, $limit) // Capture the last store id used.
->loadObjectList(); static $lastStoreId;
} // Compute the current store id.
$currentStoreId = $this->getStoreId();
/** // If the last store id is different from the current, refresh the query.
* Method to cache the last query constructed. if ($lastStoreId != $currentStoreId || empty($this->query)) {
* $lastStoreId = $currentStoreId;
* This method ensures that the query is constructed only once for a given state of the model. $this->query = $this->getListQuery();
* }
* @return \JDatabaseQuery A JDatabaseQuery object
* return $this->query;
* @since 2.0 }
*/
protected function getListQueryCache() /**
{ * Method to get a JDatabaseQuery object for retrieving the data set from a database.
// Capture the last store id used. *
static $lastStoreId; * @return \JDatabaseQuery A JDatabaseQuery object to retrieve the data set.
*
// Compute the current store id. * @since 2.0
$currentStoreId = $this->getStoreId(); */
protected function getListQuery()
// If the last store id is different from the current, refresh the query. {
if ($lastStoreId != $currentStoreId || empty($this->query)) $db = $this->getDbo();
{ $query = $db->getQuery(true);
$lastStoreId = $currentStoreId; $labelQuery = $db->getQuery(true);
$this->query = $this->getListQuery(); $query->select('pulls.*')
} ->select($db->quoteName('tests.id', 'applied'))
->from($db->quoteName('#__patchtester_pulls', 'pulls'))
return $this->query; ->leftJoin($db->quoteName('#__patchtester_tests', 'tests')
} . ' ON ' . $db->quoteName('tests.pull_id') . ' = '
. $db->quoteName('pulls.pull_id'));
/** $search = $this->getState()->get('filter.search');
* Method to get a JDatabaseQuery object for retrieving the data set from a database.
* if (!empty($search)) {
* @return \JDatabaseQuery A JDatabaseQuery object to retrieve the data set. if (stripos($search, 'id:') === 0) {
* $query->where($db->quoteName('pulls.pull_id') . ' = ' . (int) substr(
* @since 2.0 $search,
*/ 3
protected function getListQuery() ));
{ } elseif (is_numeric($search)) {
$db = $this->getDbo(); $query->where($db->quoteName('pulls.pull_id') . ' = ' . (int) $search);
$query = $db->getQuery(true); } else {
$labelQuery = $db->getQuery(true); $query->where('(' . $db->quoteName('pulls.title') . ' LIKE ' . $db->quote('%' . $db->escape($search, true) . '%') . ')');
}
$query->select('pulls.*') }
->select($db->quoteName('tests.id', 'applied'))
->from($db->quoteName('#__patchtester_pulls', 'pulls')) $applied = $this->getState()->get('filter.applied');
->leftJoin( if (!empty($applied)) {
$db->quoteName('#__patchtester_tests', 'tests') // Not applied patches have a NULL value, so build our value part of the query based on this
. ' ON ' . $db->quoteName('tests.pull_id') . ' = ' $value = $applied === 'no' ? ' IS NULL' : ' = 1';
. $db->quoteName('pulls.pull_id') $query->where($db->quoteName('applied') . $value);
); }
$search = $this->getState()->get('filter.search'); $branch = $this->getState()->get('filter.branch');
if (!empty($branch)) {
if (!empty($search)) $query->where($db->quoteName('pulls.branch') . ' IN (' . implode(',', $db->quote($branch)) . ')');
{ }
if (stripos($search, 'id:') === 0)
{ $applied = $this->getState()->get('filter.rtc');
$query->where( if (!empty($applied)) {
$db->quoteName('pulls.pull_id') . ' = ' . (int) substr( // Not applied patches have a NULL value, so build our value part of the query based on this
$search, 3 $value = $applied === 'no' ? '0' : '1';
) $query->where($db->quoteName('pulls.is_rtc') . ' = ' . $value);
); }
}
elseif (is_numeric($search)) $npm = $this->getState()->get('filter.npm');
{ if (!empty($npm)) {
$query->where( // Not applied patches have a NULL value, so build our value part of the query based on this
$db->quoteName('pulls.pull_id') . ' = ' . (int) $search $value = $npm === 'no' ? '0' : '1';
); $query->where($db->quoteName('pulls.is_npm') . ' = ' . $value);
} }
else
{ $draft = $this->getState()->get('filter.draft');
$query->where( if (!empty($draft)) {
'(' . $db->quoteName('pulls.title') . ' LIKE ' . $db->quote( // Not applied patches have a NULL value, so build our value part of the query based on this
'%' . $db->escape($search, true) . '%' $value = $draft === 'no' ? '0' : '1';
) . ')' $query->where($db->quoteName('pulls.is_draft') . ' = ' . $value);
); }
}
} $labels = $this->getState()->get('filter.label');
$applied = $this->getState()->get('filter.applied'); if (!empty($labels) && $labels[0] !== '') {
$labelQuery
if (!empty($applied)) ->select($db->quoteName('pulls_labels.pull_id'))
{ ->select('COUNT(' . $db->quoteName('pulls_labels.name') . ') AS '
// Not applied patches have a NULL value, so build our value part of the query based on this . $db->quoteName('labelCount'))
$value = $applied === 'no' ? ' IS NULL' : ' = 1'; ->from($db->quoteName(
'#__patchtester_pulls_labels',
$query->where($db->quoteName('applied') . $value); 'pulls_labels'
} ))
->where($db->quoteName('pulls_labels.name') . ' IN (' . implode(
$branch = $this->getState()->get('filter.branch'); ',',
$db->quote($labels)
if (!empty($branch)) ) . ')')
{ ->group($db->quoteName('pulls_labels.pull_id'));
$query->where( $query->leftJoin('(' . $labelQuery->__toString() . ') AS ' . $db->quoteName('pulls_labels')
$db->quoteName('pulls.branch') . ' IN (' . implode(',', $db->quote($branch)) . ')' . ' ON ' . $db->quoteName('pulls_labels.pull_id') . ' = '
); . $db->quoteName('pulls.pull_id'))
} ->where($db->quoteName('pulls_labels.labelCount') . ' = ' . count($labels));
}
$applied = $this->getState()->get('filter.rtc');
$ordering = $this->getState()->get('list.ordering', 'pulls.pull_id');
if (!empty($applied)) $direction = $this->getState()->get('list.direction', 'DESC');
{ if (!empty($ordering)) {
// Not applied patches have a NULL value, so build our value part of the query based on this $query->order($db->escape($ordering) . ' ' . $db->escape($direction));
$value = $applied === 'no' ? '0' : '1'; }
$query->where($db->quoteName('pulls.is_rtc') . ' = ' . $value); return $query;
} }
$npm = $this->getState()->get('filter.npm'); /**
* Retrieves the array of authorized sort fields
if (!empty($npm)) *
{ * @return array
// Not applied patches have a NULL value, so build our value part of the query based on this *
$value = $npm === 'no' ? '0' : '1'; * @since 2.0
*/
$query->where($db->quoteName('pulls.is_npm') . ' = ' . $value); public function getSortFields()
} {
return $this->sortFields;
$draft = $this->getState()->get('filter.draft'); }
if (!empty($draft)) /**
{ * Method to request new data from GitHub
// Not applied patches have a NULL value, so build our value part of the query based on this *
$value = $draft === 'no' ? '0' : '1'; * @param integer $page The page of the request
*
$query->where($db->quoteName('pulls.is_draft') . ' = ' . $value); * @return array
} *
* @since 2.0
$labels = $this->getState()->get('filter.label'); * @throws \RuntimeException
*/
if (!empty($labels) && $labels[0] !== '') public function requestFromGithub($page)
{ {
$labelQuery if ($page === 1) {
->select($db->quoteName('pulls_labels.pull_id')) $this->getDbo()->truncateTable('#__patchtester_pulls');
->select( $this->getDbo()->truncateTable('#__patchtester_pulls_labels');
'COUNT(' . $db->quoteName('pulls_labels.name') . ') AS ' }
. $db->quoteName('labelCount')
) try {
->from( // TODO - Option to configure the batch size
$db->quoteName( $batchSize = 100;
'#__patchtester_pulls_labels', 'pulls_labels' $pullsResponse = Helper::initializeGithub()->getOpenPulls($this->getState()->get('github_user'), $this->getState()->get('github_repo'), $page, $batchSize);
) $pulls = json_decode($pullsResponse->body);
) } catch (UnexpectedResponse $exception) {
->where( throw new \RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_GITHUB_FETCH', $exception->getMessage()), $exception->getCode(), $exception);
$db->quoteName('pulls_labels.name') . ' IN (' . implode( }
',', $db->quote($labels)
) . ')' // If this is page 1, let's check to see if we need to paginate
) if ($page === 1) {
->group($db->quoteName('pulls_labels.pull_id')); // Default this to being a single page of results
$lastPage = 1;
$query->leftJoin( if (isset($pullsResponse->headers['link'])) {
'(' . $labelQuery->__toString() . ') AS ' . $db->quoteName( $linkHeader = $pullsResponse->headers['link'];
'pulls_labels' // The `joomla/http` 2.0 package uses PSR-7 Responses which has a different format for headers, check for this
) if (is_array($linkHeader)) {
. ' ON ' . $db->quoteName('pulls_labels.pull_id') . ' = ' $linkHeader = $linkHeader[0];
. $db->quoteName('pulls.pull_id') }
)
->where( preg_match(
$db->quoteName('pulls_labels.labelCount') . ' = ' . count( '/(\?page=[0-9]{1,3}&per_page=' . $batchSize
$labels . '+>; rel=\"last\")/',
) $linkHeader,
); $matches
} );
$ordering = $this->getState()->get('list.ordering', 'pulls.pull_id'); if ($matches && isset($matches[0])) {
$direction = $this->getState()->get('list.direction', 'DESC'); $pageSegment = str_replace(
'&per_page=' . $batchSize,
if (!empty($ordering)) '',
{ $matches[0]
$query->order( );
$db->escape($ordering) . ' ' . $db->escape($direction) preg_match('/\d+/', $pageSegment, $pages);
); $lastPage = (int) $pages[0];
} }
}
return $query; }
}
// If there are no pulls to insert then bail, assume we're finished
/** if (count($pulls) === 0) {
* Retrieves the array of authorized sort fields return ['complete' => true];
* }
* @return array
* $data = [];
* @since 2.0 $labels = [];
*/ foreach ($pulls as $pull) {
public function getSortFields() // Check if this PR is RTC and has a `PR-` branch label
{ $isRTC = false;
return $this->sortFields; $isNPM = false;
} $branch = $pull->base->ref;
foreach ($pull->labels as $label) {
/** if (strtolower($label->name) === 'rtc') {
* Method to request new data from GitHub $isRTC = true;
* } elseif (
* @param integer $page The page of the request in_array(strtolower($label->name), ['npm resource changed', 'composer dependency changed'], true)
* ) {
* @return array $isNPM = true;
* }
* @since 2.0
* @throws \RuntimeException $labels[] = implode(',', [
*/ (int) $pull->number,
public function requestFromGithub($page) $this->getDbo()->quote($label->name),
{ $this->getDbo()->quote($label->color),
if ($page === 1) ]);
{ }
$this->getDbo()->truncateTable('#__patchtester_pulls');
$this->getDbo()->truncateTable('#__patchtester_pulls_labels'); // Build the data object to store in the database
} $pullData = [
(int) $pull->number,
try $this->getDbo()->quote(HTMLHelper::_('string.truncate', $pull->title, 150)),
{ $this->getDbo()->quote(HTMLHelper::_('string.truncate', $pull->body, 100)),
// TODO - Option to configure the batch size $this->getDbo()->quote($pull->html_url),
$batchSize = 100; (int) $isRTC,
(int) $isNPM,
$pullsResponse = Helper::initializeGithub()->getOpenPulls( $this->getDbo()->quote($branch),
$this->getState()->get('github_user'), ($pull->draft ? 1 : 0)
$this->getState()->get('github_repo'), ];
$page, $data[] = implode(',', $pullData);
$batchSize }
);
// If there are no pulls to insert then bail, assume we're finished
$pulls = json_decode($pullsResponse->body); if (count($data) === 0) {
} return array('complete' => true);
catch (UnexpectedResponse $exception) }
{
throw new \RuntimeException( try {
Text::sprintf( $this->getDbo()->setQuery($this->getDbo()->getQuery(true)
'COM_PATCHTESTER_ERROR_GITHUB_FETCH', ->insert('#__patchtester_pulls')
$exception->getMessage() ->columns(['pull_id', 'title', 'description', 'pull_url',
), 'is_rtc', 'is_npm', 'branch', 'is_draft'])
$exception->getCode(), ->values($data));
$exception $this->getDbo()->execute();
); } catch (\RuntimeException $exception) {
} throw new \RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_INSERT_DATABASE', $exception->getMessage()), $exception->getCode(), $exception);
}
// If this is page 1, let's check to see if we need to paginate
if ($page === 1) if ($labels) {
{ try {
// Default this to being a single page of results $this->getDbo()->setQuery($this->getDbo()->getQuery(true)
$lastPage = 1; ->insert('#__patchtester_pulls_labels')
->columns(['pull_id', 'name', 'color'])
if (isset($pullsResponse->headers['link'])) ->values($labels));
{ $this->getDbo()->execute();
$linkHeader = $pullsResponse->headers['link']; } catch (\RuntimeException $exception) {
throw new \RuntimeException(
// The `joomla/http` 2.0 package uses PSR-7 Responses which has a different format for headers, check for this Text::sprintf(
if (is_array($linkHeader)) 'COM_PATCHTESTER_ERROR_INSERT_DATABASE',
{ $exception->getMessage()
$linkHeader = $linkHeader[0]; ),
} $exception->getCode(),
$exception
preg_match( );
'/(\?page=[0-9]{1,3}&per_page=' . $batchSize }
. '+>; rel=\"last\")/', $linkHeader, $matches }
);
return [
if ($matches && isset($matches[0])) 'complete' => false,
{ 'page' => ($page + 1),
$pageSegment = str_replace( 'lastPage' => $lastPage ?? false,
'&per_page=' . $batchSize, '', $matches[0] ];
); }
preg_match('/\d+/', $pageSegment, $pages); /**
$lastPage = (int) $pages[0]; * Truncates the pulls table
} *
} * @return void
} *
* @since 2.0
// If there are no pulls to insert then bail, assume we're finished */
if (count($pulls) === 0) public function truncateTable()
{ {
return ['complete' => true]; $this->getDbo()->truncateTable('#__patchtester_pulls');
} }
$data = [];
$labels = [];
foreach ($pulls as $pull)
{
// Check if this PR is RTC and has a `PR-` branch label
$isRTC = false;
$isNPM = false;
$branch = $pull->base->ref;
foreach ($pull->labels as $label)
{
if (strtolower($label->name) === 'rtc')
{
$isRTC = true;
}
elseif (in_array(
strtolower($label->name),
['npm resource changed', 'composer dependency changed'],
true
))
{
$isNPM = true;
}
$labels[] = implode(
',',
[
(int) $pull->number,
$this->getDbo()->quote($label->name),
$this->getDbo()->quote($label->color),
]
);
}
// Build the data object to store in the database
$pullData = [
(int) $pull->number,
$this->getDbo()->quote(
HTMLHelper::_('string.truncate', $pull->title, 150)
),
$this->getDbo()->quote(
HTMLHelper::_('string.truncate', $pull->body, 100)
),
$this->getDbo()->quote($pull->html_url),
(int) $isRTC,
(int) $isNPM,
$this->getDbo()->quote($branch),
($pull->draft ? 1 : 0)
];
$data[] = implode(',', $pullData);
}
// If there are no pulls to insert then bail, assume we're finished
if (count($data) === 0)
{
return array('complete' => true);
}
try
{
$this->getDbo()->setQuery(
$this->getDbo()->getQuery(true)
->insert('#__patchtester_pulls')
->columns(
['pull_id', 'title', 'description', 'pull_url',
'is_rtc', 'is_npm', 'branch', 'is_draft']
)
->values($data)
);
$this->getDbo()->execute();
}
catch (\RuntimeException $exception)
{
throw new \RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_INSERT_DATABASE',
$exception->getMessage()
),
$exception->getCode(),
$exception
);
}
if ($labels)
{
try
{
$this->getDbo()->setQuery(
$this->getDbo()->getQuery(true)
->insert('#__patchtester_pulls_labels')
->columns(['pull_id', 'name', 'color'])
->values($labels)
);
$this->getDbo()->execute();
}
catch (\RuntimeException $exception)
{
throw new \RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_INSERT_DATABASE',
$exception->getMessage()
),
$exception->getCode(),
$exception
);
}
}
return [
'complete' => false,
'page' => ($page + 1),
'lastPage' => $lastPage ?? false,
];
}
/**
* Truncates the pulls table
*
* @return void
*
* @since 2.0
*/
public function truncateTable()
{
$this->getDbo()->truncateTable('#__patchtester_pulls');
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -8,6 +9,10 @@
namespace PatchTester\Model; namespace PatchTester\Model;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Methods supporting applied pull requests. * Methods supporting applied pull requests.
* *
@ -15,36 +20,32 @@ namespace PatchTester\Model;
*/ */
class TestsModel extends AbstractModel class TestsModel extends AbstractModel
{ {
/** /**
* Retrieves a list of applied patches * Retrieves a list of applied patches
* *
* @return array List of applied patches * @return array List of applied patches
* *
* @since 2.0 * @since 2.0
*/ */
public function getAppliedPatches(): array public function getAppliedPatches(): array
{ {
$db = $this->getDb(); $db = $this->getDb();
$db->setQuery($db->getQuery(true)
->select('*')
->from($db->quoteName('#__patchtester_tests'))
->where($db->quoteName('applied') . ' = 1'));
return $db->loadObjectList('pull_id');
}
$db->setQuery( /**
$db->getQuery(true) * Truncates the tests table
->select('*') *
->from($db->quoteName('#__patchtester_tests')) * @return void
->where($db->quoteName('applied') . ' = 1') *
); * @since 2.0
*/
return $db->loadObjectList('pull_id'); public function truncateTable(): void
} {
$this->getDb()->truncateTable('#__patchtester_tests');
/** }
* Truncates the tests table
*
* @return void
*
* @since 2.0
*/
public function truncateTable(): void
{
$this->getDb()->truncateTable('#__patchtester_tests');
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -15,55 +16,53 @@ namespace PatchTester;
*/ */
abstract class TrackerHelper abstract class TrackerHelper
{ {
/** /**
* Array containing the supported repositories integrated with the Joomla! Issue Tracker * Array containing the supported repositories integrated with the Joomla! Issue Tracker
* *
* @var array * @var array
* @since 2.0 * @since 2.0
*/ */
private static $projects = array( private static $projects = array(
'joomla-cms' => array( 'joomla-cms' => array(
'githubUser' => 'joomla', 'githubUser' => 'joomla',
'githubrepo' => 'joomla-cms', 'githubrepo' => 'joomla-cms',
'projectAlias' => 'joomla-cms', 'projectAlias' => 'joomla-cms',
), ),
'patchtester' => array( 'patchtester' => array(
'githubUser' => 'joomla-extensions', 'githubUser' => 'joomla-extensions',
'githubrepo' => 'patchtester', 'githubrepo' => 'patchtester',
'projectAlias' => 'patchtester', 'projectAlias' => 'patchtester',
), ),
'weblinks' => array( 'weblinks' => array(
'githubUser' => 'joomla-extensions', 'githubUser' => 'joomla-extensions',
'githubrepo' => 'weblinks', 'githubrepo' => 'weblinks',
'projectAlias' => 'weblinks', 'projectAlias' => 'weblinks',
), ),
); );
/** /**
* Get the issue tracker project alias for a GitHub repository * Get the issue tracker project alias for a GitHub repository
* *
* @param string $githubUser The owner of the GitHub repository (user or organization) * @param string $githubUser The owner of the GitHub repository (user or organization)
* @param string $githubRepo The GitHub repository name * @param string $githubRepo The GitHub repository name
* *
* @return string|boolean The project alias if supported or boolean false * @return string|boolean The project alias if supported or boolean false
* *
* @since 2.0 * @since 2.0
*/ */
public static function getTrackerAlias($githubUser, $githubRepo) public static function getTrackerAlias($githubUser, $githubRepo)
{ {
// If the repo isn't even listed, no point in going further // If the repo isn't even listed, no point in going further
if (!array_key_exists($githubRepo, self::$projects)) if (!array_key_exists($githubRepo, self::$projects)) {
{ return false;
return false; }
}
// Now the GitHub user must match the project (we don't support forks, sorry!) // Now the GitHub user must match the project (we don't support forks, sorry!)
if (self::$projects[$githubRepo]['githubUser'] !== $githubUser) if (self::$projects[$githubRepo]['githubUser'] !== $githubUser) {
{ return false;
return false; }
}
// This project is supported // This project is supported
return self::$projects[$githubRepo]['projectAlias']; return self::$projects[$githubRepo]['projectAlias'];
} }
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -12,6 +13,10 @@ use Joomla\CMS\Filesystem\Path;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use PatchTester\Model\AbstractModel; use PatchTester\Model\AbstractModel;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Base HTML view for the patch testing component * Base HTML view for the patch testing component
* *
@ -19,204 +24,184 @@ use PatchTester\Model\AbstractModel;
*/ */
abstract class AbstractHtmlView extends AbstractView abstract class AbstractHtmlView extends AbstractView
{ {
/** /**
* The view layout. * The view layout.
* *
* @var string * @var string
* @since 4.0.0 * @since 4.0.0
*/ */
protected $layout = 'default'; protected $layout = 'default';
/**
* The paths queue.
*
* @var SplPriorityQueue
* @since 4.0.0
*/
protected $paths;
/**
* Method to instantiate the view.
*
* @param AbstractModel $model The model object.
* @param SplPriorityQueue $paths The paths queue.
*
* @since 4.0.0
*/
public function __construct($model, \SplPriorityQueue $paths = null)
{
parent::__construct($model);
// Setup dependencies.
$this->paths = $paths ?: new \SplPriorityQueue();
}
/** /**
* The paths queue. * Method to escape output.
* *
* @var SplPriorityQueue * @param string $output The output to escape.
* @since 4.0.0 *
*/ * @return string The escaped output.
protected $paths; *
* @since 4.0.0
*/
public function escape($output)
{
// Escape the output.
return htmlspecialchars($output, ENT_COMPAT, 'UTF-8');
}
/** /**
* Method to instantiate the view. * Method to get the view layout.
* *
* @param AbstractModel $model The model object. * @return string The layout name.
* @param SplPriorityQueue $paths The paths queue. *
* * @since 4.0.0
* @since 4.0.0 */
*/ public function getLayout()
public function __construct($model, \SplPriorityQueue $paths = null) {
{ return $this->layout;
parent::__construct($model); }
// Setup dependencies. /**
$this->paths = $paths ?: new \SplPriorityQueue; * Method to get the layout path.
} *
* @param string $layout The layout name.
*
* @return mixed The layout file name if found, false otherwise.
*
* @since 4.0.0
*/
public function getPath($layout)
{
// Get the layout file name.
$file = Path::clean($layout . '.php');
// Find the layout file path.
$path = Path::find(clone $this->paths, $file);
return $path;
}
/** /**
* Method to escape output. * Method to get the view paths.
* *
* @param string $output The output to escape. * @return SplPriorityQueue The paths queue.
* *
* @return string The escaped output. * @since 4.0.0
* */
* @since 4.0.0 public function getPaths()
*/ {
public function escape($output) return $this->paths;
{ }
// Escape the output.
return htmlspecialchars($output, ENT_COMPAT, 'UTF-8');
}
/** /**
* Method to get the view layout. * Load a template file -- first look in the templates folder for an override
* *
* @return string The layout name. * @param string $tpl The name of the template source file; automatically searches the template paths and compiles as needed.
* *
* @since 4.0.0 * @return string The output of the the template script.
*/ *
public function getLayout() * @since 4.0.0
{ * @throws \RuntimeException
return $this->layout; */
} public function loadTemplate($tpl = null)
{
// Get the path to the file
$file = $this->getLayout();
if (isset($tpl)) {
$file .= '_' . $tpl;
}
/** $path = $this->getPath($file);
* Method to get the layout path. if (!$path) {
* throw new \RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_LAYOUTFILE_NOT_FOUND', $file), 500);
* @param string $layout The layout name. }
*
* @return mixed The layout file name if found, false otherwise.
*
* @since 4.0.0
*/
public function getPath($layout)
{
// Get the layout file name.
$file = Path::clean($layout . '.php');
// Find the layout file path. // Unset so as not to introduce into template scope
$path = Path::find(clone $this->paths, $file); unset($tpl);
unset($file);
// Never allow a 'this' property
if (isset($this->this)) {
unset($this->this);
}
return $path; // Start an output buffer.
} ob_start();
// Load the template.
include $path;
// Get the layout contents.
return ob_get_clean();
}
/** /**
* Method to get the view paths. * Method to render the view.
* *
* @return SplPriorityQueue The paths queue. * @return string The rendered view.
* *
* @since 4.0.0 * @since 4.0.0
*/ * @throws RuntimeException
public function getPaths() */
{ public function render()
return $this->paths; {
} // Get the layout path.
$path = $this->getPath($this->getLayout());
// Check if the layout path was found.
if (!$path) {
throw new \RuntimeException('Layout Path Not Found');
}
/** // Start an output buffer.
* Load a template file -- first look in the templates folder for an override ob_start();
* // Load the layout.
* @param string $tpl The name of the template source file; automatically searches the template paths and compiles as needed. include $path;
* // Get the layout contents.
* @return string The output of the the template script. $output = ob_get_clean();
* return $output;
* @since 4.0.0 }
* @throws \RuntimeException
*/
public function loadTemplate($tpl = null)
{
// Get the path to the file
$file = $this->getLayout();
if (isset($tpl)) /**
{ * Method to set the view layout.
$file .= '_' . $tpl; *
} * @param string $layout The layout name.
*
* @return $this
*
* @since 4.0.0
*/
public function setLayout($layout)
{
$this->layout = $layout;
return $this;
}
$path = $this->getPath($file); /**
* Method to set the view paths.
if (!$path) *
{ * @param \SplPriorityQueue $paths The paths queue.
throw new \RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_LAYOUTFILE_NOT_FOUND', $file), 500); *
} * @return $this
*
// Unset so as not to introduce into template scope * @since 4.0.0
unset($tpl); */
unset($file); public function setPaths(\SplPriorityQueue $paths)
{
// Never allow a 'this' property $this->paths = $paths;
if (isset($this->this)) return $this;
{ }
unset($this->this);
}
// Start an output buffer.
ob_start();
// Load the template.
include $path;
// Get the layout contents.
return ob_get_clean();
}
/**
* Method to render the view.
*
* @return string The rendered view.
*
* @since 4.0.0
* @throws RuntimeException
*/
public function render()
{
// Get the layout path.
$path = $this->getPath($this->getLayout());
// Check if the layout path was found.
if (!$path)
{
throw new \RuntimeException('Layout Path Not Found');
}
// Start an output buffer.
ob_start();
// Load the layout.
include $path;
// Get the layout contents.
$output = ob_get_clean();
return $output;
}
/**
* Method to set the view layout.
*
* @param string $layout The layout name.
*
* @return $this
*
* @since 4.0.0
*/
public function setLayout($layout)
{
$this->layout = $layout;
return $this;
}
/**
* Method to set the view paths.
*
* @param \SplPriorityQueue $paths The paths queue.
*
* @return $this
*
* @since 4.0.0
*/
public function setPaths(\SplPriorityQueue $paths)
{
$this->paths = $paths;
return $this;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -17,37 +18,36 @@ use PatchTester\Model\AbstractModel;
*/ */
abstract class AbstractView abstract class AbstractView
{ {
/** /**
* The model object. * The model object.
* *
* @var AbstractModel * @var AbstractModel
* @since 4.0.0 * @since 4.0.0
*/ */
protected $model; protected $model;
/**
* Method to instantiate the view.
*
* @param AbstractModel $model The model object.
*
* @since 4.0.0
*/
public function __construct($model)
{
$this->model = $model;
}
/** /**
* Method to instantiate the view. * Method to escape output.
* *
* @param AbstractModel $model The model object. * @param string $output The output to escape.
* *
* @since 4.0.0 * @return string The escaped output.
*/ *
public function __construct($model) * @since 4.0.0
{ */
$this->model = $model; public function escape($output)
} {
return $output;
/** }
* Method to escape output.
*
* @param string $output The output to escape.
*
* @return string The escaped output.
*
* @since 4.0.0
*/
public function escape($output)
{
return $output;
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -8,6 +9,10 @@
namespace PatchTester\View; namespace PatchTester\View;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Default HTML view class. * Default HTML view class.
* *

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -10,6 +11,10 @@ use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper; use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** @var \PatchTester\View\DefaultHtmlView $this */ /** @var \PatchTester\View\DefaultHtmlView $this */
HTMLHelper::_('jquery.framework'); HTMLHelper::_('jquery.framework');
@ -20,10 +25,10 @@ Text::script('COM_PATCHTESTER_FETCH_AN_ERROR_HAS_OCCURRED');
?> ?>
<div id="patchtester-container"> <div id="patchtester-container">
<h1 id="patchtester-progress-header"><?php echo Text::_('COM_PATCHTESTER_FETCH_INITIALIZING'); ?></h1> <h1 id="patchtester-progress-header"><?php echo Text::_('COM_PATCHTESTER_FETCH_INITIALIZING'); ?></h1>
<p id="patchtester-progress-message"><?php echo Text::_('COM_PATCHTESTER_FETCH_INITIALIZING_DESCRIPTION'); ?></p> <p id="patchtester-progress-message"><?php echo Text::_('COM_PATCHTESTER_FETCH_INITIALIZING_DESCRIPTION'); ?></p>
<div id="progress" class="progress"> <div id="progress" class="progress">
<div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" role="progressbar"></div> <div id="progress-bar" class="progress-bar progress-bar-striped progress-bar-animated bg-success" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" role="progressbar"></div>
</div> </div>
<input id="patchtester-token" type="hidden" name="<?php echo Factory::getSession()->getFormToken(); ?>" value="1" /> <input id="patchtester-token" type="hidden" name="<?php echo Factory::getSession()->getFormToken(); ?>" value="1" />
</div> </div>

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -18,6 +19,10 @@ use Joomla\Registry\Registry;
use PatchTester\TrackerHelper; use PatchTester\TrackerHelper;
use PatchTester\View\DefaultHtmlView; use PatchTester\View\DefaultHtmlView;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* View class for a list of pull requests. * View class for a list of pull requests.
* *
@ -27,140 +32,109 @@ use PatchTester\View\DefaultHtmlView;
*/ */
class PullsHtmlView extends DefaultHtmlView class PullsHtmlView extends DefaultHtmlView
{ {
/** /**
* Array containing environment errors * Array containing environment errors
* *
* @var array * @var array
* @since 2.0 * @since 2.0
*/ */
protected $envErrors = []; protected $envErrors = [];
/**
* Array of open pull requests
*
* @var array
* @since 2.0
*/
protected $items;
/**
* Pagination object
*
* @var Pagination
* @since 2.0
*/
protected $pagination;
/**
* Form object for search filters
*
* @var Form
* @since 4.1.0
*/
public $filterForm;
/**
* The active search filters
*
* @var array
* @since 4.1.0
*/
public $activeFilters;
/**
* The model state
*
* @var Registry
* @since 2.0.0
*/
protected $state;
/**
* The issue tracker project alias
*
* @var string|boolean
* @since 2.0
*/
protected $trackerAlias;
/**
* Method to render the view.
*
* @return string The rendered view.
*
* @since 2.0.0
* @throws Exception
*/
public function render(): string
{
if (!extension_loaded('openssl')) {
$this->envErrors[] = Text::_('COM_PATCHTESTER_REQUIREMENT_OPENSSL');
}
/** if (!in_array('https', stream_get_wrappers(), true)) {
* Array of open pull requests $this->envErrors[] = Text::_('COM_PATCHTESTER_REQUIREMENT_HTTPS');
* }
* @var array
* @since 2.0
*/
protected $items;
/** if (!count($this->envErrors)) {
* Pagination object $this->state = $this->model->getState();
* $this->items = $this->model->getItems();
* @var Pagination $this->pagination = $this->model->getPagination();
* @since 2.0 $this->filterForm = $this->model->getFilterForm();
*/ $this->activeFilters = $this->model->getActiveFilters();
protected $pagination; $this->trackerAlias = TrackerHelper::getTrackerAlias($this->state->get('github_user'), $this->state->get('github_repo'));
}
/** // Change the layout if there are environment errors
* Form object for search filters if (count($this->envErrors)) {
* $this->setLayout('errors');
* @var Form }
* @since 4.1.0
*/
public $filterForm;
/** $this->addToolbar();
* The active search filters Text::script('COM_PATCHTESTER_CONFIRM_RESET');
* return parent::render();
* @var array }
* @since 4.1.0
*/
public $activeFilters;
/** /**
* The model state * Add the page title and toolbar.
* *
* @var Registry * @return void
* @since 2.0.0 *
*/ * @since 2.0.0
protected $state; */
protected function addToolbar(): void
{
ToolbarHelper::title(Text::_('COM_PATCHTESTER'), 'patchtester fas fa-save');
if (!count($this->envErrors)) {
$toolbar = Toolbar::getInstance('toolbar');
$toolbar->appendButton('Popup', 'sync', 'COM_PATCHTESTER_TOOLBAR_FETCH_DATA', 'index.php?option=com_patchtester&view=fetch&tmpl=component', 500, 210, 0, 0, 'window.parent.location.reload()', Text::_('COM_PATCHTESTER_HEADING_FETCH_DATA'));
// Add a reset button.
$toolbar->appendButton('Standard', 'expired', 'COM_PATCHTESTER_TOOLBAR_RESET', 'reset', false);
}
/** ToolbarHelper::preferences('com_patchtester');
* The issue tracker project alias }
*
* @var string|boolean
* @since 2.0
*/
protected $trackerAlias;
/**
* Method to render the view.
*
* @return string The rendered view.
*
* @since 2.0.0
* @throws Exception
*/
public function render(): string
{
if (!extension_loaded('openssl'))
{
$this->envErrors[] = Text::_('COM_PATCHTESTER_REQUIREMENT_OPENSSL');
}
if (!in_array('https', stream_get_wrappers(), true))
{
$this->envErrors[] = Text::_('COM_PATCHTESTER_REQUIREMENT_HTTPS');
}
if (!count($this->envErrors))
{
$this->state = $this->model->getState();
$this->items = $this->model->getItems();
$this->pagination = $this->model->getPagination();
$this->filterForm = $this->model->getFilterForm();
$this->activeFilters = $this->model->getActiveFilters();
$this->trackerAlias = TrackerHelper::getTrackerAlias(
$this->state->get('github_user'),
$this->state->get('github_repo')
);
}
// Change the layout if there are environment errors
if (count($this->envErrors))
{
$this->setLayout('errors');
}
$this->addToolbar();
Text::script('COM_PATCHTESTER_CONFIRM_RESET');
return parent::render();
}
/**
* Add the page title and toolbar.
*
* @return void
*
* @since 2.0.0
*/
protected function addToolbar(): void
{
ToolbarHelper::title(Text::_('COM_PATCHTESTER'), 'patchtester fas fa-save');
if (!count($this->envErrors))
{
$toolbar = Toolbar::getInstance('toolbar');
$toolbar->appendButton(
'Popup',
'sync',
'COM_PATCHTESTER_TOOLBAR_FETCH_DATA',
'index.php?option=com_patchtester&view=fetch&tmpl=component',
500,
210,
0,
0,
'window.parent.location.reload()',
Text::_('COM_PATCHTESTER_HEADING_FETCH_DATA')
);
// Add a reset button.
$toolbar->appendButton('Standard', 'expired', 'COM_PATCHTESTER_TOOLBAR_RESET', 'reset', false);
}
ToolbarHelper::preferences('com_patchtester');
}
} }

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -11,63 +12,71 @@ use Joomla\CMS\Language\Text;
use Joomla\CMS\Layout\LayoutHelper; use Joomla\CMS\Layout\LayoutHelper;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** @var \PatchTester\View\Pulls\PullsHtmlView $this */ /** @var \PatchTester\View\Pulls\PullsHtmlView $this */
HTMLHelper::_('stylesheet', 'com_patchtester/octicons.css', ['version' => '3.5.0', 'relative' => true]); HTMLHelper::_('stylesheet', 'com_patchtester/octicons.css', ['version' => '3.5.0', 'relative' => true]);
HTMLHelper::_('script', 'com_patchtester/patchtester.js', ['version' => 'auto', 'relative' => true]); HTMLHelper::_('script', 'com_patchtester/patchtester.js', ['version' => 'auto', 'relative' => true]);
?> ?>
<form action="<?php echo Route::_('index.php?option=com_patchtester&view=pulls'); ?>" method="post" name="adminForm" id="adminForm"> <form action="<?php echo Route::_('index.php?option=com_patchtester&view=pulls'); ?>" method="post" name="adminForm" id="adminForm">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div id="j-main-container" class="j-main-container"> <div id="j-main-container" class="j-main-container">
<?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?> <?php echo LayoutHelper::render('joomla.searchtools.default', ['view' => $this]); ?>
<div id="j-main-container" class="j-main-container"> <div id="j-main-container" class="j-main-container">
<?php if (empty($this->items)) : ?> <?php if (empty($this->items)) :
<div class="alert alert-info"> ?>
<span class="fa fa-info-circle" aria-hidden="true"></span><span class="sr-only"><?php echo Text::_('INFO'); ?></span> <div class="alert alert-info">
<?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?> <span class="fa fa-info-circle" aria-hidden="true"></span><span class="sr-only"><?php echo Text::_('INFO'); ?></span>
</div> <?php echo Text::_('JGLOBAL_NO_MATCHING_RESULTS'); ?>
<?php else : ?> </div>
<table class="table"> <?php
<caption id="captionTable" class="sr-only"> else :
<?php echo Text::_('COM_PATCHTESTER_PULLS_TABLE_CAPTION'); ?>, <?php echo Text::_('JGLOBAL_SORTED_BY'); ?> ?>
</caption> <table class="table">
<thead> <caption id="captionTable" class="sr-only">
<tr> <?php echo Text::_('COM_PATCHTESTER_PULLS_TABLE_CAPTION'); ?>, <?php echo Text::_('JGLOBAL_SORTED_BY'); ?>
<th scope="col" style="width:5%" class="text-center"> </caption>
<?php echo Text::_('COM_PATCHTESTER_PULL_ID'); ?> <thead>
</th> <tr>
<th scope="col" style="min-width:100px"> <th scope="col" style="width:5%" class="text-center">
<?php echo Text::_('JGLOBAL_TITLE'); ?> <?php echo Text::_('COM_PATCHTESTER_PULL_ID'); ?>
</th> </th>
<th scope="col" style="width:8%" class="d-none d-md-table-cell text-center"> <th scope="col" style="min-width:100px">
<?php echo Text::_('COM_PATCHTESTER_BRANCH'); ?> <?php echo Text::_('JGLOBAL_TITLE'); ?>
</th> </th>
<th scope="col" style="width:8%" class="d-none d-md-table-cell text-center"> <th scope="col" style="width:8%" class="d-none d-md-table-cell text-center">
<?php echo Text::_('COM_PATCHTESTER_READY_TO_COMMIT'); ?> <?php echo Text::_('COM_PATCHTESTER_BRANCH'); ?>
</th> </th>
<th scope="col" style="width:10%" class="text-center"> <th scope="col" style="width:8%" class="d-none d-md-table-cell text-center">
<?php echo Text::_('JSTATUS'); ?> <?php echo Text::_('COM_PATCHTESTER_READY_TO_COMMIT'); ?>
</th> </th>
<th scope="col" style="width:15%" class="text-center"> <th scope="col" style="width:10%" class="text-center">
<?php echo Text::_('COM_PATCHTESTER_TEST_THIS_PATCH'); ?> <?php echo Text::_('JSTATUS'); ?>
</th> </th>
</tr> <th scope="col" style="width:15%" class="text-center">
</thead> <?php echo Text::_('COM_PATCHTESTER_TEST_THIS_PATCH'); ?>
<tbody> </th>
<?php echo $this->loadTemplate('items'); ?> </tr>
</tbody> </thead>
</table> <tbody>
<?php endif; ?> <?php echo $this->loadTemplate('items'); ?>
</tbody>
</table>
<?php
endif; ?>
<?php echo $this->pagination->getListFooter(); ?> <?php echo $this->pagination->getListFooter(); ?>
<input type="hidden" name="task" value="" /> <input type="hidden" name="task" value="" />
<input type="hidden" name="boxchecked" value="0" /> <input type="hidden" name="boxchecked" value="0" />
<input type="hidden" name="pull_id" id="pull_id" value="" /> <input type="hidden" name="pull_id" id="pull_id" value="" />
<?php echo HTMLHelper::_('form.token'); ?> <?php echo HTMLHelper::_('form.token'); ?>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</form> </form>

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -9,114 +10,141 @@
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use PatchTester\View\Pulls\PullsHtmlView; use PatchTester\View\Pulls\PullsHtmlView;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** @var PullsHtmlView $this */ /** @var PullsHtmlView $this */
foreach ($this->items as $i => $item) : foreach ($this->items as $i => $item) :
$status = ''; $status = '';
if ($item->applied) :
if ($item->applied) : $status = ' class="table-active"';
$status = ' class="table-active"'; endif;
endif; ?>
?> <tr<?php echo $status; ?>>
<tr<?php echo $status; ?>> <th scope="row" class="text-center">
<th scope="row" class="text-center"> <?php echo $item->pull_id; ?>
<?php echo $item->pull_id; ?> <?php if ($item->is_draft) :
<?php if ($item->is_draft) : ?> ?>
<span class="badge" style="color: #FFFFFF; background-color: #6e7681"> <span class="badge" style="color: #FFFFFF; background-color: #6e7681">
<?php echo Text::_('COM_PATCHTESTER_IS_DRAFT'); ?> <?php echo Text::_('COM_PATCHTESTER_IS_DRAFT'); ?>
</span> </span>
<?php endif; ?> <?php
</th> endif; ?>
<td> </th>
<span><?php echo $this->escape($item->title); ?></span> <td>
<div role="tooltip" id="tip<?php echo $i; ?>"> <span><?php echo $this->escape($item->title); ?></span>
<?php echo $this->escape($item->description); ?> <div role="tooltip" id="tip<?php echo $i; ?>">
</div> <?php echo $this->escape($item->description); ?>
<div class="row"> </div>
<div class="col-md-auto"> <div class="row">
<a class="badge bg-info" href="<?php echo $item->pull_url; ?>" target="_blank"> <div class="col-md-auto">
<?php echo Text::_('COM_PATCHTESTER_VIEW_ON_GITHUB'); ?> <a class="badge bg-info" href="<?php echo $item->pull_url; ?>" target="_blank">
</a> <?php echo Text::_('COM_PATCHTESTER_VIEW_ON_GITHUB'); ?>
</div> </a>
<?php if ($this->trackerAlias) : ?> </div>
<div class="col-md-auto"> <?php if ($this->trackerAlias) :
<a class="badge bg-info" ?>
href="https://issues.joomla.org/tracker/<?php echo $this->trackerAlias; ?>/<?php echo $item->pull_id; ?>" <div class="col-md-auto">
target="_blank"> <a class="badge bg-info"
<?php echo Text::_('COM_PATCHTESTER_VIEW_ON_JOOMLA_ISSUE_TRACKER'); ?> href="https://issues.joomla.org/tracker/<?php echo $this->trackerAlias; ?>/<?php echo $item->pull_id; ?>"
</a> target="_blank">
</div> <?php echo Text::_('COM_PATCHTESTER_VIEW_ON_JOOMLA_ISSUE_TRACKER'); ?>
<?php endif; ?> </a>
<?php if ($item->applied) : ?> </div>
<div class="col-md-auto"> <?php
<span class="badge bg-info"><?php echo Text::sprintf('COM_PATCHTESTER_APPLIED_COMMIT_SHA', substr($item->sha, 0, 10)); ?></span> endif; ?>
</div> <?php if ($item->applied) :
<?php endif; ?> ?>
</div> <div class="col-md-auto">
<?php if (count($item->labels) > 0) : ?> <span class="badge bg-info"><?php echo Text::sprintf('COM_PATCHTESTER_APPLIED_COMMIT_SHA', substr($item->sha, 0, 10)); ?></span>
</div>
<?php
endif; ?>
</div>
<?php if (count($item->labels) > 0) :
?>
<div class="row"> <div class="row">
<div class="col-md-auto"> <div class="col-md-auto">
<?php foreach ($item->labels as $label): ?> <?php foreach ($item->labels as $label) :
?>
<?php <?php
switch (strtolower($label->name)) switch (strtolower($label->name)) {
{ case 'a11y':
case 'a11y': case 'conflicting files':
case 'conflicting files': case 'documentation required':
case 'documentation required': case 'information required':
case 'information required': case 'j3 issue':
case 'j3 issue': case 'language change':
case 'language change': case 'mysql 5.7':
case 'mysql 5.7': case 'needs new owner':
case 'needs new owner': case 'no code attached yet':
case 'no code attached yet': case 'pbf':
case 'pbf': case 'pr-3.9-dev':
case 'pr-3.9-dev': case 'pr-3.10-dev':
case 'pr-3.10-dev': case 'pr-4.1-dev':
case 'pr-4.1-dev': case 'pr-i10n_4.0-dev':
case 'pr-i10n_4.0-dev': case 'pr-staging':
case 'pr-staging': case 'release blocker':
case 'release blocker': case 'rfc':
case 'rfc': case 'test instructions missing':
case 'test instructions missing': case 'updates requested':
case 'updates requested': $textColor = '000000';
$textColor = '000000';
break; break;
default: default:
$textColor = 'FFFFFF'; $textColor = 'FFFFFF';
break;
} break;
}
?> ?>
<span class="badge" style="color: #<?php echo $textColor; ?>; background-color: #<?php echo $label->color; ?>"><?php echo $label->name; ?></span> <span class="badge" style="color: #<?php echo $textColor; ?>; background-color: #<?php echo $label->color; ?>"><?php echo $label->name; ?></span>
<?php endforeach; ?> <?php
endforeach; ?>
</div> </div>
</div> </div>
<?php endif; ?> <?php
</td> endif; ?>
<td class="d-none d-md-table-cell text-center"> </td>
<?php echo $this->escape($item->branch); ?> <td class="d-none d-md-table-cell text-center">
</td> <?php echo $this->escape($item->branch); ?>
<td class="d-none d-md-table-cell text-center"> </td>
<?php if ($item->is_rtc) : ?> <td class="d-none d-md-table-cell text-center">
<span class="badge bg-success"><?php echo Text::_('JYES'); ?></span> <?php if ($item->is_rtc) :
<?php else : ?> ?>
<span class="badge bg-secondary"><?php echo Text::_('JNO'); ?></span> <span class="badge bg-success"><?php echo Text::_('JYES'); ?></span>
<?php endif; ?> <?php
</td> else :
<td class="text-center"> ?>
<?php if ($item->applied) : ?> <span class="badge bg-secondary"><?php echo Text::_('JNO'); ?></span>
<span class="badge bg-success"><?php echo Text::_('COM_PATCHTESTER_APPLIED'); ?></span> <?php
<?php else : ?> endif; ?>
<span class="badge bg-secondary"><?php echo Text::_('COM_PATCHTESTER_NOT_APPLIED'); ?></span> </td>
<?php endif; ?> <td class="text-center">
</td> <?php if ($item->applied) :
<td class="text-center"> ?>
<?php if ($item->applied) : ?> <span class="badge bg-success"><?php echo Text::_('COM_PATCHTESTER_APPLIED'); ?></span>
<button type="button" class="btn btn-sm btn-success submitPatch" <?php
data-task="revert" data-id="<?php echo (int) $item->applied; ?>"><?php echo Text::_('COM_PATCHTESTER_REVERT_PATCH'); ?></button> else :
<?php else : ?> ?>
<button type="button" class="btn btn-sm btn-primary submitPatch" <span class="badge bg-secondary"><?php echo Text::_('COM_PATCHTESTER_NOT_APPLIED'); ?></span>
data-task="apply" data-id="<?php echo (int) $item->pull_id; ?>"><?php echo Text::_('COM_PATCHTESTER_APPLY_PATCH'); ?></button> <?php
<?php endif; ?> endif; ?>
</td> </td>
</tr> <td class="text-center">
<?php endforeach; <?php if ($item->applied) :
?>
<button type="button" class="btn btn-sm btn-success submitPatch"
data-task="revert" data-id="<?php echo (int) $item->applied; ?>"><?php echo Text::_('COM_PATCHTESTER_REVERT_PATCH'); ?></button>
<?php
else :
?>
<button type="button" class="btn btn-sm btn-primary submitPatch"
data-task="apply" data-id="<?php echo (int) $item->pull_id; ?>"><?php echo Text::_('COM_PATCHTESTER_APPLY_PATCH'); ?></button>
<?php
endif; ?>
</td>
</tr>
<?php
endforeach;

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -8,12 +9,18 @@
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** @var \PatchTester\View\Pulls\PullsHtmlView $this */ /** @var \PatchTester\View\Pulls\PullsHtmlView $this */
?> ?>
<h3><?php echo Text::_('COM_PATCHTESTER_REQUIREMENTS_HEADING'); ?></h3> <h3><?php echo Text::_('COM_PATCHTESTER_REQUIREMENTS_HEADING'); ?></h3>
<p><?php echo Text::_('COM_PATCHTESTER_REQUIREMENTS_NOT_MET'); ?></p> <p><?php echo Text::_('COM_PATCHTESTER_REQUIREMENTS_NOT_MET'); ?></p>
<ul> <ul>
<?php foreach ($this->envErrors as $error) : ?> <?php foreach ($this->envErrors as $error) :
<li><?php echo $error; ?></li> ?>
<?php endforeach; ?> <li><?php echo $error; ?></li>
<?php
endforeach; ?>
</ul> </ul>

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -6,36 +7,27 @@
* @license GNU General Public License version 2 or later * @license GNU General Public License version 2 or later
*/ */
defined('_JEXEC') or die; \defined('_JEXEC') or die;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
if (!Factory::getUser()->authorise('core.manage', 'com_patchtester')) {
if (!Factory::getUser()->authorise('core.manage', 'com_patchtester')) throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
{
throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403);
} }
// Application reference // Application reference
$app = Factory::getApplication(); $app = Factory::getApplication();
// Import our Composer autoloader to load the component classes // Import our Composer autoloader to load the component classes
require_once __DIR__ . '/vendor/autoload.php'; require_once __DIR__ . '/vendor/autoload.php';
// Build the controller class name based on task // Build the controller class name based on task
$task = $app->input->getCmd('task', 'display'); $task = $app->input->getCmd('task', 'display');
// If $task is an empty string, apply our default since JInput might not // If $task is an empty string, apply our default since JInput might not
if ($task === '') if ($task === '') {
{ $task = 'display';
$task = 'display';
} }
$class = '\\PatchTester\\Controller\\' . ucfirst(strtolower($task)) . 'Controller'; $class = '\\PatchTester\\Controller\\' . ucfirst(strtolower($task)) . 'Controller';
if (!class_exists($class)) {
if (!class_exists($class)) throw new InvalidArgumentException(Text::sprintf('JLIB_APPLICATION_ERROR_INVALID_CONTROLLER_CLASS', $class), 404);
{
throw new InvalidArgumentException(Text::sprintf('JLIB_APPLICATION_ERROR_INVALID_CONTROLLER_CLASS', $class), 404);
} }
// Instantiate and execute the controller // Instantiate and execute the controller

View File

@ -1,4 +1,5 @@
<?php <?php
/** /**
* Patch testing component for the Joomla! CMS * Patch testing component for the Joomla! CMS
* *
@ -12,6 +13,10 @@ use Joomla\CMS\Installer\Adapter\ComponentAdapter;
use Joomla\CMS\Installer\InstallerScript; use Joomla\CMS\Installer\InstallerScript;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** /**
* Installation class to perform additional changes during install/uninstall/update * Installation class to perform additional changes during install/uninstall/update
* *
@ -19,103 +24,99 @@ use Joomla\CMS\Language\Text;
*/ */
class Com_PatchtesterInstallerScript extends InstallerScript class Com_PatchtesterInstallerScript extends InstallerScript
{ {
/**
* Extension script constructor.
*
* @since 3.0.0
*/
public function __construct()
{
$this->minimumJoomla = '4.0';
$this->minimumPhp = JOOMLA_MINIMUM_PHP;
$this->deleteFiles = array(
'/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default_errors.php',
);
$this->deleteFolders = array(
'/administrator/components/com_patchtester/PatchTester/Table',
'/components/com_patchtester',
);
Factory::getApplication()
->getLanguage()
->load('com_patchtester.sys', JPATH_ADMINISTRATOR, null, true);
}
/** /**
* Extension script constructor. * Show the message on install.
* *
* @since 3.0.0 * @param ComponentAdapter $parent The class calling this method
*/ *
public function __construct() * @return void
{ *
$this->minimumJoomla = '4.0'; * @since 4.0.0
$this->minimumPhp = JOOMLA_MINIMUM_PHP; */
public function install(ComponentAdapter $parent): void
{
?>
<h1>
<?php echo HTMLHelper::_('image', 'media/com_patchtester/images/icon-48-patchtester.png', Text::_('COM_PATCHTESTER')); ?>
<?php echo Text::_('COM_PATCHTESTER'); ?>
</h1>
<p><?php echo Text::_('COM_PATCHTESTER_INSTALL_INSTRUCTIONS'); ?></p>
<?php
}
$this->deleteFiles = array( /**
'/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default_errors.php', * Show the message on install.
); *
* @param ComponentAdapter $parent The class calling this method
$this->deleteFolders = array( *
'/administrator/components/com_patchtester/PatchTester/Table', * @return void
'/components/com_patchtester', *
); * @since 4.0.0
*/
Factory::getApplication() public function update(ComponentAdapter $parent): void
->getLanguage() {
->load('com_patchtester.sys', JPATH_ADMINISTRATOR, null, true); ?>
} <h1>
<?php echo HTMLHelper::_('image', 'media/com_patchtester/images/icon-48-patchtester.png', Text::_('COM_PATCHTESTER')); ?>
/** <?php echo Text::_('COM_PATCHTESTER'); ?>
* Show the message on install. </h1>
* <p><?php echo Text::_('COM_PATCHTESTER_UPDATE_INSTRUCTIONS'); ?></p>
* @param ComponentAdapter $parent The class calling this method <?php
* }
* @return void
*
* @since 4.0.0
*/
public function install(ComponentAdapter $parent): void
{
?>
<h1>
<?php echo HTMLHelper::_('image', 'media/com_patchtester/images/icon-48-patchtester.png', Text::_('COM_PATCHTESTER')); ?>
<?php echo Text::_('COM_PATCHTESTER'); ?>
</h1>
<p><?php echo Text::_('COM_PATCHTESTER_INSTALL_INSTRUCTIONS'); ?></p>
<?php
}
/**
* Show the message on install.
*
* @param ComponentAdapter $parent The class calling this method
*
* @return void
*
* @since 4.0.0
*/
public function update(ComponentAdapter $parent): void
{
?>
<h1>
<?php echo HTMLHelper::_('image', 'media/com_patchtester/images/icon-48-patchtester.png', Text::_('COM_PATCHTESTER')); ?>
<?php echo Text::_('COM_PATCHTESTER'); ?>
</h1>
<p><?php echo Text::_('COM_PATCHTESTER_UPDATE_INSTRUCTIONS'); ?></p>
<?php
}
/** /**
* Show the message on install. * Show the message on install.
* *
* @param ComponentAdapter $parent The class calling this method * @param ComponentAdapter $parent The class calling this method
* *
* @return void * @return void
* *
* @since 4.0.0 * @since 4.0.0
*/ */
public function uninstall(ComponentAdapter $parent): void public function uninstall(ComponentAdapter $parent): void
{ {
?> ?>
<h1> <h1>
<?php echo HTMLHelper::_('image', 'media/com_patchtester/images/icon-48-patchtester.png', Text::_('COM_PATCHTESTER')); ?> <?php echo HTMLHelper::_('image', 'media/com_patchtester/images/icon-48-patchtester.png', Text::_('COM_PATCHTESTER')); ?>
</h1> </h1>
<p><?php echo Text::_('COM_PATCHTESTER_UNINSTALL_THANK_YOU'); ?></p> <p><?php echo Text::_('COM_PATCHTESTER_UNINSTALL_THANK_YOU'); ?></p>
<?php <?php
} }
/** /**
* Function to perform changes during postflight * Function to perform changes during postflight
* *
* @param string $type The action being performed * @param string $type The action being performed
* @param ComponentAdapter $parent The class calling this method * @param ComponentAdapter $parent The class calling this method
* *
* @return void * @return void
* *
* @since 3.0.0 * @since 3.0.0
*/ */
public function postflight(string $type, ComponentAdapter $parent): void public function postflight(string $type, ComponentAdapter $parent): void
{ {
$this->removeFiles(); $this->removeFiles();
} }
} }

View File

@ -1,5 +1,6 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
/** /**
* Script used to generate hashes for release packages * Script used to generate hashes for release packages
* *
@ -14,16 +15,14 @@ $packageDir = dirname(__DIR__) . '/packages';
$hashes = array(); $hashes = array();
/** @var DirectoryIterator $file */ /** @var DirectoryIterator $file */
foreach (new DirectoryIterator($packageDir) as $file) foreach (new DirectoryIterator($packageDir) as $file) {
{ if ($file->isDir() || $file->isDot()) {
if ($file->isDir() || $file->isDot()) continue;
{ }
continue;
}
$hashes[$file->getFilename()] = array( $hashes[$file->getFilename()] = array(
'sha384' => hash_file('sha384', $file->getPathname()), 'sha384' => hash_file('sha384', $file->getPathname()),
); );
} }
$jsonOptions = PHP_VERSION_ID >= 50400 ? JSON_PRETTY_PRINT : 0; $jsonOptions = PHP_VERSION_ID >= 50400 ? JSON_PRETTY_PRINT : 0;

View File

@ -1,5 +1,6 @@
#!/usr/bin/env php #!/usr/bin/env php
<?php <?php
/** /**
* Script used to prepare a patchtester release * Script used to prepare a patchtester release
* *
@ -25,12 +26,12 @@ const PHP_TAB = "\t";
// Functions. // Functions.
function usage($command) function usage($command)
{ {
echo PHP_EOL; echo PHP_EOL;
echo 'Usage: php ' . $command . ' [options]' . PHP_EOL; echo 'Usage: php ' . $command . ' [options]' . PHP_EOL;
echo PHP_TAB . '[options]:'.PHP_EOL; echo PHP_TAB . '[options]:' . PHP_EOL;
echo PHP_TAB . PHP_TAB . '-v <version>:' . PHP_TAB . 'Version (ex: 3.0.0, 3.0.1-dev)' . PHP_EOL; echo PHP_TAB . PHP_TAB . '-v <version>:' . PHP_TAB . 'Version (ex: 3.0.0, 3.0.1-dev)' . PHP_EOL;
echo PHP_TAB . PHP_TAB . '--exclude-manifest: Exclude updating the update server manifest'; echo PHP_TAB . PHP_TAB . '--exclude-manifest: Exclude updating the update server manifest';
echo PHP_EOL; echo PHP_EOL;
} }
$manifestFile = '/administrator/components/com_patchtester/patchtester.xml'; $manifestFile = '/administrator/components/com_patchtester/patchtester.xml';
@ -39,31 +40,27 @@ $updateServerFile = '/manifest.xml';
// Check arguments (exit if incorrect cli arguments). // Check arguments (exit if incorrect cli arguments).
$opts = getopt('v:', array('exclude-manifest')); $opts = getopt('v:', array('exclude-manifest'));
if (empty($opts['v'])) if (empty($opts['v'])) {
{ usage($argv[0]);
usage($argv[0]); die();
die();
} }
// Check version string (exit if not correct). // Check version string (exit if not correct).
$versionParts = explode('-', $opts['v']); $versionParts = explode('-', $opts['v']);
if (!preg_match('#^[0-9]+\.[0-9]+\.[0-9]+$#', $versionParts[0])) if (!preg_match('#^[0-9]+\.[0-9]+\.[0-9]+$#', $versionParts[0])) {
{ usage($argv[0]);
usage($argv[0]); die();
die();
} }
if (isset($versionParts[1]) && !preg_match('#(dev|alpha|beta|rc)[0-9]*#', $versionParts[1])) if (isset($versionParts[1]) && !preg_match('#(dev|alpha|beta|rc)[0-9]*#', $versionParts[1])) {
{ usage($argv[0]);
usage($argv[0]); die();
die();
} }
if (isset($versionParts[2]) && $versionParts[2] !== 'dev') if (isset($versionParts[2]) && $versionParts[2] !== 'dev') {
{ usage($argv[0]);
usage($argv[0]); die();
die();
} }
// Make sure we use the correct language and timezone. // Make sure we use the correct language and timezone.
@ -77,15 +74,15 @@ umask(022);
$versionSubParts = explode('.', $versionParts[0]); $versionSubParts = explode('.', $versionParts[0]);
$version = array( $version = array(
'main' => $versionSubParts[0] . '.' . $versionSubParts[1], 'main' => $versionSubParts[0] . '.' . $versionSubParts[1],
'release' => $versionSubParts[0] . '.' . $versionSubParts[1] . '.' . $versionSubParts[2], 'release' => $versionSubParts[0] . '.' . $versionSubParts[1] . '.' . $versionSubParts[2],
'dev_devel' => $versionSubParts[2] . (!empty($versionParts[1]) ? '-' . $versionParts[1] : '') . (!empty($versionParts[2]) ? '-' . $versionParts[2] : ''), 'dev_devel' => $versionSubParts[2] . (!empty($versionParts[1]) ? '-' . $versionParts[1] : '') . (!empty($versionParts[2]) ? '-' . $versionParts[2] : ''),
'credate' => date('d-F-Y'), 'credate' => date('d-F-Y'),
); );
// Prints version information. // Prints version information.
echo PHP_EOL; echo PHP_EOL;
echo 'Version data:'. PHP_EOL; echo 'Version data:' . PHP_EOL;
echo '- Main:' . PHP_TAB . PHP_TAB . PHP_TAB . $version['main'] . PHP_EOL; echo '- Main:' . PHP_TAB . PHP_TAB . PHP_TAB . $version['main'] . PHP_EOL;
echo '- Release:' . PHP_TAB . PHP_TAB . $version['release'] . PHP_EOL; echo '- Release:' . PHP_TAB . PHP_TAB . $version['release'] . PHP_EOL;
echo '- Full:' . PHP_TAB . PHP_TAB . PHP_TAB . $version['main'] . '.' . $version['dev_devel'] . PHP_EOL; echo '- Full:' . PHP_TAB . PHP_TAB . PHP_TAB . $version['main'] . '.' . $version['dev_devel'] . PHP_EOL;
@ -96,33 +93,30 @@ echo PHP_EOL;
$rootPath = dirname(dirname(__DIR__)); $rootPath = dirname(dirname(__DIR__));
// Updates the version and creation date in the component manifest file. // Updates the version and creation date in the component manifest file.
if (file_exists($rootPath . $manifestFile)) if (file_exists($rootPath . $manifestFile)) {
{ $fileContents = file_get_contents($rootPath . $manifestFile);
$fileContents = file_get_contents($rootPath . $manifestFile); $fileContents = preg_replace('#<version>[^<]*</version>#', '<version>' . $version['main'] . '.' . $version['dev_devel'] . '</version>', $fileContents);
$fileContents = preg_replace('#<version>[^<]*</version>#', '<version>' . $version['main'] . '.' . $version['dev_devel'] . '</version>', $fileContents); $fileContents = preg_replace('#<creationDate>[^<]*</creationDate>#', '<creationDate>' . $version['credate'] . '</creationDate>', $fileContents);
$fileContents = preg_replace('#<creationDate>[^<]*</creationDate>#', '<creationDate>' . $version['credate'] . '</creationDate>', $fileContents); file_put_contents($rootPath . $manifestFile, $fileContents);
file_put_contents($rootPath . $manifestFile, $fileContents);
} }
// Replaces the `__DEPLOY_VERSION__` marker with the "release" version number // Replaces the `__DEPLOY_VERSION__` marker with the "release" version number
system('cd ' . $rootPath . ' && find administrator -name "*.php" -type f -exec sed -i "s/__DEPLOY_VERSION__/' . $version['release'] . '/g" "{}" \;'); system('cd ' . $rootPath . ' && find administrator -name "*.php" -type f -exec sed -i "s/__DEPLOY_VERSION__/' . $version['release'] . '/g" "{}" \;');
// If not instructed to exclude it, update the update server's manifest // If not instructed to exclude it, update the update server's manifest
if (!isset($opts['exclude-manifest'])) if (!isset($opts['exclude-manifest'])) {
{ if (file_exists($rootPath . $updateServerFile)) {
if (file_exists($rootPath . $updateServerFile)) $fileContents = file_get_contents($rootPath . $updateServerFile);
{ $fileContents = preg_replace('#<infourl title="Patch Tester Component">[^<]*</infourl>#', '<infourl title="Patch Tester Component">https://github.com/joomla-extensions/patchtester/releases/tag/' . $version['release'] . '</infourl>', $fileContents);
$fileContents = file_get_contents($rootPath . $updateServerFile); $fileContents = preg_replace('#<downloadurl type="full" format="zip">[^<]*</downloadurl>#', '<downloadurl type="full" format="zip">https://github.com/joomla-extensions/patchtester/releases/download/' . $version['release'] . '/com_patchtester_' . $version['release'] . '.zip</downloadurl>', $fileContents);
$fileContents = preg_replace('#<infourl title="Patch Tester Component">[^<]*</infourl>#', '<infourl title="Patch Tester Component">https://github.com/joomla-extensions/patchtester/releases/tag/' . $version['release'] . '</infourl>', $fileContents); file_put_contents($rootPath . $updateServerFile, $fileContents);
$fileContents = preg_replace('#<downloadurl type="full" format="zip">[^<]*</downloadurl>#', '<downloadurl type="full" format="zip">https://github.com/joomla-extensions/patchtester/releases/download/' . $version['release'] . '/com_patchtester_' . $version['release'] . '.zip</downloadurl>', $fileContents);
file_put_contents($rootPath . $updateServerFile, $fileContents);
echo '*************' . PHP_EOL; echo '*************' . PHP_EOL;
echo '* IMPORTANT *' . PHP_EOL; echo '* IMPORTANT *' . PHP_EOL;
echo '*************' . PHP_EOL; echo '*************' . PHP_EOL;
echo '' . PHP_EOL; echo '' . PHP_EOL;
echo 'Ensure you regenerate the SHA384 package hash before publishing the updated manifest!!!' . PHP_EOL; echo 'Ensure you regenerate the SHA384 package hash before publishing the updated manifest!!!' . PHP_EOL;
} }
} }
echo 'Version bump complete!' . PHP_EOL . PHP_EOL; echo 'Version bump complete!' . PHP_EOL . PHP_EOL;

View File

@ -14,8 +14,6 @@
}, },
"require-dev": { "require-dev": {
"joomla/crowdin-sync": "dev-master", "joomla/crowdin-sync": "dev-master",
"joomla/cms-coding-standards": "~2.0.0-alpha2@dev",
"joomla/coding-standards": "~3.0@dev",
"squizlabs/php_codesniffer": "~3.0" "squizlabs/php_codesniffer": "~3.0"
}, },
"autoload": { "autoload": {

980
composer.lock generated

File diff suppressed because it is too large Load Diff

32
ruleset.xml Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0"?>
<ruleset name="Joomla-CMS">
<description>The Joomla CMS PSR-12 exceptions.</description>
<!-- Exclude folders not containing production code -->
<exclude-pattern type="relative">build/packag*</exclude-pattern>
<exclude-pattern type="relative">administrator/components/com_patchtester/vendor/*</exclude-pattern>
<rule ref="PSR12">
<exclude name="Generic.Files.LineEndings"/>
</rule>
<!-- temporary extend the line length -->
<rule ref="Generic.Files.LineLength">
<properties>
<property name="lineLimit" value="560"/>
<property name="absoluteLineLimit" value="560"/>
</properties>
</rule>
<rule ref="PSR1.Files.SideEffects">
<exclude-pattern type="relative">build/patchtester/release\.php</exclude-pattern>
</rule>
<rule ref="PSR1.Classes.ClassDeclaration">
<exclude-pattern type="relative">administrator/components/com_patchtester/script\.php</exclude-pattern>
</rule>
<rule ref="Squiz.Classes.ValidClassName">
<exclude-pattern type="relative">administrator/components/com_patchtester/script\.php</exclude-pattern>
</rule>
</ruleset>