Clean up controllers and models

Signed-off-by: Roland Dalmulder <contact@rolandd.com>
This commit is contained in:
Roland Dalmulder 2023-08-20 15:54:48 +02:00
parent 63510fe4f9
commit 96b8480c51
No known key found for this signature in database
GPG Key ID: 6D30CD38749A5B9E
23 changed files with 823 additions and 603 deletions

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<form addfieldprefix="PatchTester\Field"> <form addfieldprefix="Joomla\Component\Patchtester\Administrator\Field">
<fields name="filter"> <fields name="filter">
<field <field
name="search" name="search"
@ -87,10 +87,10 @@
type="list" type="list"
onchange="this.form.submit();" onchange="this.form.submit();"
> >
<option value="a.title ASC">JGLOBAL_TITLE_ASC</option> <option value="pulls.title ASC">JGLOBAL_TITLE_ASC</option>
<option value="a.title DESC">JGLOBAL_TITLE_DESC</option> <option value="pulls.title DESC">JGLOBAL_TITLE_DESC</option>
<option value="a.pull_id ASC">COM_PATCHTESTER_PULL_ID_ASC</option> <option value="pulls.pull_id ASC">COM_PATCHTESTER_PULL_ID_ASC</option>
<option value="a.pull_id DESC">COM_PATCHTESTER_PULL_ID_DESC</option> <option value="pulls.pull_id DESC">COM_PATCHTESTER_PULL_ID_DESC</option>
</field> </field>
<field <field

View File

@ -1,104 +0,0 @@
<?php
/**
* Patch testing component for the Joomla! CMS
*
* @copyright Copyright (C) 2011 - 2012 Ian MacLennan, Copyright (C) 2013 - 2018 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later
*/
namespace Joomla\Component\Patchtester\Administrator\Controller;
use Joomla\CMS\Application\CMSApplication;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\Component\Patchtester\Administrator\Model\AbstractModel;
use Joomla\Input\Input;
use Joomla\Registry\Registry;
/**
* Base controller for the patch testing component
*
* @since 2.0
*/
abstract class AbstractController extends BaseController
{
/**
* The active application
*
* @var CMSApplication
* @since 4.0.0
*/
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($config = array(), MVCFactoryInterface $factory = null, ?CMSApplication $app = null, ?Input $input = null)
{
$this->app = $app;
// Set the context for the controller
$this->context = 'com_patchtester.' . $this->getInput()->getCmd('view', $this->defaultView);
}
/**
* Get the application object.
*
* @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

@ -11,6 +11,7 @@ namespace Joomla\Component\Patchtester\Administrator\Controller;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use Joomla\Component\Patchtester\Administrator\Model\PullModel; use Joomla\Component\Patchtester\Administrator\Model\PullModel;
@ -24,7 +25,7 @@ use Joomla\Component\Patchtester\Administrator\Model\PullModel;
* *
* @since 2.0 * @since 2.0
*/ */
class ApplyController extends AbstractController class ApplyController extends BaseController
{ {
/** /**
* Execute the controller. * Execute the controller.
@ -33,16 +34,15 @@ class ApplyController extends AbstractController
* *
* @since 2.0 * @since 2.0
*/ */
public function execute() public function execute($task): void
{ {
try { try {
$model = new PullModel(null, Factory::getDbo()); /** @var PullModel $model */
// Initialize the state for the model $model = Factory::getApplication()->bootComponent('com_patchtester')->getMVCFactory()->createModel('Pull', 'Administrator', ['ignore_request' => true]);
$model->setState($this->initializeState($model)); $msg = Text::_('COM_PATCHTESTER_NO_FILES_TO_PATCH');
if ($model->apply($this->getInput()->getUint('pull_id'))) {
if ($model->apply($this->input->getUint('pull_id'))) {
$msg = Text::_('COM_PATCHTESTER_APPLY_OK'); $msg = Text::_('COM_PATCHTESTER_APPLY_OK');
} else {
$msg = Text::_('COM_PATCHTESTER_NO_FILES_TO_PATCH');
} }
$type = 'message'; $type = 'message';
@ -51,7 +51,7 @@ class ApplyController extends AbstractController
$type = 'error'; $type = 'error';
} }
$this->getApplication()->enqueueMessage($msg, $type); $this->app->enqueueMessage($msg, $type);
$this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false)); $this->app->redirect(Route::_('index.php?option=com_patchtester', false));
} }
} }

View File

@ -9,10 +9,7 @@
namespace Joomla\Component\Patchtester\Administrator\Controller; namespace Joomla\Component\Patchtester\Administrator\Controller;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController; use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects // phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die; \defined('_JEXEC') or die;
@ -33,111 +30,4 @@ class DisplayController extends BaseController
* @since 1.6 * @since 1.6
*/ */
protected $default_view = 'pulls'; protected $default_view = 'pulls';
/**
* Default ordering value
*
* @var string
* @since 4.0.0
*/
protected $defaultFullOrdering = 'a.pull_id DESC';
/**
* Execute the controller.
*
* @return boolean True on success
*
* @since 2.0
* @throws \RuntimeException
*/
public function executeOld()
{
// 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)) {
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

@ -9,8 +9,10 @@
namespace Joomla\Component\Patchtester\Administrator\Controller; namespace Joomla\Component\Patchtester\Administrator\Controller;
use Exception;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Response\JsonResponse; use Joomla\CMS\Response\JsonResponse;
use Joomla\Component\Patchtester\Administrator\Model\PullsModel; use Joomla\Component\Patchtester\Administrator\Model\PullsModel;
@ -23,48 +25,42 @@ use Joomla\Component\Patchtester\Administrator\Model\PullsModel;
* *
* @since 2.0 * @since 2.0
*/ */
class FetchController extends AbstractController class FetchController extends BaseController
{ {
/** /**
* Execute the controller. * Execute the controller.
* *
* @return void Redirects the application * @return void Redirects the application
* *
* @throws Exception
* @since 2.0 * @since 2.0
*/ */
public function execute() public function execute($task)
{ {
// We don't want this request to be cached. $this->app->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->app->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->app->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->app->setHeader('Pragma', 'no-cache');
$this->getApplication()->setHeader('Pragma', 'no-cache'); $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet);
$this->getApplication()->setHeader('Content-Type', $this->getApplication()->mimeType . '; charset=' . $this->getApplication()->charSet); $session = Factory::getApplication()->getSession();
$session = Factory::getSession();
try { try {
// Fetch our page from the session
$page = $session->get('com_patchtester_fetcher_page', 1); $page = $session->get('com_patchtester_fetcher_page', 1);
$model = new PullsModel(); /** @var PullsModel $model */
// Initialize the state for the model $model = $this->app->bootComponent('com_patchtester')->getMVCFactory()->createModel('Pulls', 'Administrator', ['ignore_request' => true]);
$state = $this->initializeState($model);
foreach ($state as $key => $value) {
$model->setState($key, $value);
}
$status = $model->requestFromGithub($page); $status = $model->requestFromGithub($page);
} catch (\Exception $e) { } catch (Exception $e) {
$response = new JsonResponse($e); $response = new JsonResponse($e);
$this->getApplication()->sendHeaders(); $this->app->sendHeaders();
echo json_encode($response); echo json_encode($response);
$this->getApplication()->close(1); $this->app->close(1);
} }
// Store the last page to the session if given one
if (isset($status['lastPage']) && $status['lastPage'] !== false) { if (isset($status['lastPage']) && $status['lastPage'] !== false) {
$session->set('com_patchtester_fetcher_last_page', $status['lastPage']); $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)) { if ($status['complete'] || $page === $session->get('com_patchtester_fetcher_last_page', false)) {
$status['complete'] = true; $status['complete'] = true;
$status['header'] = Text::_('COM_PATCHTESTER_FETCH_SUCCESSFUL', true); $status['header'] = Text::_('COM_PATCHTESTER_FETCH_SUCCESSFUL', true);
@ -83,8 +79,8 @@ class FetchController extends AbstractController
} }
$response = new JsonResponse($status, $message, false, true); $response = new JsonResponse($status, $message, false, true);
$this->getApplication()->sendHeaders(); $this->app->sendHeaders();
echo json_encode($response); echo json_encode($response);
$this->getApplication()->close(); $this->app->close();
} }
} }

View File

@ -13,6 +13,7 @@ use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Folder; use Joomla\CMS\Filesystem\Folder;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use Joomla\Component\Patchtester\Administrator\Model\PullModel; use Joomla\Component\Patchtester\Administrator\Model\PullModel;
use Joomla\Component\Patchtester\Administrator\Model\PullsModel; use Joomla\Component\Patchtester\Administrator\Model\PullsModel;
@ -28,7 +29,7 @@ use Joomla\Filesystem\File;
* *
* @since 2.0 * @since 2.0
*/ */
class ResetController extends AbstractController class ResetController extends BaseController
{ {
/** /**
* Execute the controller. * Execute the controller.
@ -37,20 +38,25 @@ class ResetController extends AbstractController
* *
* @since 2.0 * @since 2.0
*/ */
public function execute(): void public function execute($task): void
{ {
try { try {
$hasErrors = false; $hasErrors = false;
$revertErrored = false; $revertErrored = false;
$pullModel = new PullModel(null, Factory::getDbo()); $mvcFactory = Factory::getApplication()->bootComponent('com_patchtester')->getMVCFactory();
$pullsModel = new PullsModel($this->context, null, Factory::getDbo()); /** @var PullModel $pullModel */
$testsModel = new TestsModel(null, Factory::getDbo()); $pullModel = $mvcFactory->createModel('Pull', 'Administrator', ['ignore_request' => true]);
// Check the applied patches in the database first /** @var PullsModel $pullModel */
$pullsModel = $mvcFactory->createModel('Pulls', 'Administrator', ['ignore_request' => true]);
/** @var TestsModel $pullModel */
$testsModel = $mvcFactory->createModel('Tests', 'Administrator', ['ignore_request' => true]);
// Check the applied patches in the database first
$appliedPatches = $testsModel->getAppliedPatches(); $appliedPatches = $testsModel->getAppliedPatches();
$params = ComponentHelper::getParams('com_patchtester'); $params = ComponentHelper::getParams('com_patchtester');
// Decide based on repository settings whether patch will be applied through Github or CIServer // Decide based on repository settings whether patch will be applied through Github or CIServer
if ((bool) $params->get('ci_switch', 1)) { if ((bool) $params->get('ci_switch', 1)) {
// Let's try to cleanly revert all applied patches with ci // Let's try to cleanly revert all applied patches with ci
foreach ($appliedPatches as $patch) { foreach ($appliedPatches as $patch) {
try { try {
$pullModel->revertWithCIServer($patch->id); $pullModel->revertWithCIServer($patch->id);
@ -76,7 +82,7 @@ class ResetController extends AbstractController
} catch (\RuntimeException $e) { } catch (\RuntimeException $e) {
$hasErrors = true; $hasErrors = true;
$this->getApplication()->enqueueMessage( $this->app->enqueueMessage(
Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_PULLS_TABLE', $e->getMessage()), Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_PULLS_TABLE', $e->getMessage()),
'error' 'error'
); );
@ -89,7 +95,7 @@ class ResetController extends AbstractController
} catch (\RuntimeException $e) { } catch (\RuntimeException $e) {
$hasErrors = true; $hasErrors = true;
$this->getApplication()->enqueueMessage( $this->app->enqueueMessage(
Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_TESTS_TABLE', $e->getMessage()), Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_TESTS_TABLE', $e->getMessage()),
'error' 'error'
); );
@ -101,7 +107,7 @@ class ResetController extends AbstractController
if (count($backups)) { if (count($backups)) {
foreach ($backups as $file) { foreach ($backups as $file) {
if (!File::delete(JPATH_COMPONENT . '/backups/' . $file)) { if (!File::delete(JPATH_COMPONENT . '/backups/' . $file)) {
$this->getApplication()->enqueueMessage( $this->app->enqueueMessage(
Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_COMPONENT . '/backups/' . $file), Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_COMPONENT . '/backups/' . $file),
'error' 'error'
); );
@ -115,7 +121,7 @@ class ResetController extends AbstractController
$msg = Text::sprintf( $msg = Text::sprintf(
'COM_PATCHTESTER_RESET_HAS_ERRORS', 'COM_PATCHTESTER_RESET_HAS_ERRORS',
JPATH_COMPONENT . '/backups', JPATH_COMPONENT . '/backups',
Factory::getDbo()->replacePrefix('#__patchtester_tests') Factory::getApplication()->get('DatabaseDriver')->replacePrefix('#__patchtester_tests')
); );
$type = 'warning'; $type = 'warning';
} else { } else {
@ -127,7 +133,7 @@ class ResetController extends AbstractController
$type = 'error'; $type = 'error';
} }
$this->getApplication()->enqueueMessage($msg, $type); $this->app->enqueueMessage($msg, $type);
$this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false)); $this->app->redirect(Route::_('index.php?option=com_patchtester', false));
} }
} }

View File

@ -9,8 +9,8 @@
namespace Joomla\Component\Patchtester\Administrator\Controller; namespace Joomla\Component\Patchtester\Administrator\Controller;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route; use Joomla\CMS\Router\Route;
use Joomla\Component\Patchtester\Administrator\Model\PullModel; use Joomla\Component\Patchtester\Administrator\Model\PullModel;
@ -23,7 +23,7 @@ use Joomla\Component\Patchtester\Administrator\Model\PullModel;
* *
* @since 2.0 * @since 2.0
*/ */
class RevertController extends AbstractController class RevertController extends BaseController
{ {
/** /**
* Execute the controller. * Execute the controller.
@ -32,13 +32,12 @@ class RevertController extends AbstractController
* *
* @since 2.0 * @since 2.0
*/ */
public function execute() public function execute($task)
{ {
try { try {
$model = new PullModel(null, Factory::getDbo()); /** @var PullModel $model */
// Initialize the state for the model $model = $this->app->bootComponent('com_patchtester')->getMVCFactory()->createModel('Pull', 'Administrator', ['ignore_request' => true]);
$model->setState($this->initializeState($model)); $model->revert($this->input->getUint('pull_id'));
$model->revert($this->getInput()->getUint('pull_id'));
$msg = Text::_('COM_PATCHTESTER_REVERT_OK'); $msg = Text::_('COM_PATCHTESTER_REVERT_OK');
$type = 'message'; $type = 'message';
} catch (\Exception $e) { } catch (\Exception $e) {
@ -46,7 +45,7 @@ class RevertController extends AbstractController
$type = 'error'; $type = 'error';
} }
$this->getApplication()->enqueueMessage($msg, $type); $this->app->enqueueMessage($msg, $type);
$this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false)); $this->app->redirect(Route::_('index.php?option=com_patchtester', false));
} }
} }

View File

@ -11,10 +11,10 @@ namespace Joomla\Component\Patchtester\Administrator\Controller;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Response\JsonResponse; use Joomla\CMS\Response\JsonResponse;
use Joomla\CMS\Session\Session; use Joomla\CMS\Session\Session;
use Joomla\Component\Patchtester\Administrator\Helper\Helper; use Joomla\Component\Patchtester\Administrator\Helper\Helper;
use Joomla\Component\Patchtester\Administrator\Model\TestsModel;
// phpcs:disable PSR1.Files.SideEffects // phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die; \defined('_JEXEC') or die;
@ -25,7 +25,7 @@ use Joomla\Component\Patchtester\Administrator\Model\TestsModel;
* *
* @since 2.0 * @since 2.0
*/ */
class StartfetchController extends AbstractController class StartfetchController extends BaseController
{ {
/** /**
* Execute the controller. * Execute the controller.
@ -34,20 +34,19 @@ class StartfetchController extends AbstractController
* *
* @since 2.0 * @since 2.0
*/ */
public function execute() public function execute($task): void
{ {
// We don't want this request to be cached. $this->app->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->app->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->app->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->app->setHeader('Pragma', 'no-cache');
$this->getApplication()->setHeader('Pragma', 'no-cache'); $this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->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')) { if (!Session::checkToken('request')) {
$response = new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403)); $response = new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403));
$this->getApplication()->sendHeaders(); $this->app->sendHeaders();
echo json_encode($response); echo json_encode($response);
$this->getApplication()->close(1); $this->app->close(1);
} }
// Make sure we can fetch the data from GitHub - throw an error on < 10 available requests // Make sure we can fetch the data from GitHub - throw an error on < 10 available requests
@ -56,40 +55,48 @@ class StartfetchController extends AbstractController
$rate = json_decode($rateResponse->body); $rate = json_decode($rateResponse->body);
} catch (\Exception $e) { } catch (\Exception $e) {
$response = new JsonResponse(new \Exception(Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e)); $response = new JsonResponse(new \Exception(Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e));
$this->getApplication()->sendHeaders(); $this->app->sendHeaders();
echo json_encode($response); echo json_encode($response);
$this->getApplication()->close(1); $this->app->close(1);
} }
// If over the API limit, we can't build this list // If over the API limit, we can't build this list
if ($rate->resources->core->remaining < 10) { 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)); $response = new JsonResponse(new \Exception(Text::sprintf('COM_PATCHTESTER_API_LIMIT_LIST', Factory::getDate($rate->resources->core->reset)), 429));
$this->getApplication()->sendHeaders(); $this->app->sendHeaders();
echo json_encode($response); echo json_encode($response);
$this->getApplication()->close(1); $this->app->close(1);
} }
$testsModel = new TestsModel(null, Factory::getDbo()); $testsModel = Factory::getApplication()->bootComponent('com_patchtester')->getMVCFactory()->createModel('Tests', 'Administrator', ['ignore_request' => true]);
try { try {
// Sanity check, ensure there aren't any applied patches // Sanity check, ensure there aren't any applied patches
if (count($testsModel->getAppliedPatches()) >= 1) { if (count($testsModel->getAppliedPatches()) >= 1) {
$response = new JsonResponse(new \Exception(Text::_('COM_PATCHTESTER_ERROR_APPLIED_PATCHES'), 500)); $response = new JsonResponse(new \Exception(Text::_('COM_PATCHTESTER_ERROR_APPLIED_PATCHES'), 500));
$this->getApplication()->sendHeaders(); $this->app->sendHeaders();
echo json_encode($response); echo json_encode($response);
$this->getApplication()->close(1); $this->app->close(1);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$response = new JsonResponse($e); $response = new JsonResponse($e);
$this->getApplication()->sendHeaders(); $this->app->sendHeaders();
echo json_encode($response); echo json_encode($response);
$this->getApplication()->close(1); $this->app->close(1);
} }
// We're able to successfully pull data, prepare our environment // We're able to successfully pull data, prepare our environment
Factory::getSession()->set('com_patchtester_fetcher_page', 1); Factory::getApplication()->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); $response = new JsonResponse(
$this->getApplication()->sendHeaders(); [
'complete' => false,
'header' => Text::_('COM_PATCHTESTER_FETCH_PROCESSING', true)
],
Text::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', 1),
false,
true
);
$this->app->sendHeaders();
echo json_encode($response); echo json_encode($response);
$this->getApplication()->close(); $this->app->close();
} }
} }

View File

@ -7,16 +7,18 @@
* @license GNU General Public License version 2 or later * @license GNU General Public License version 2 or later
*/ */
namespace Joomla\Component\Patchtester\Administrator\Github\Exception; namespace Joomla\Component\Patchtester\Administrator\Exception;
use Joomla\CMS\Http\Response; use DomainException;
use Exception;
use Joomla\Http\Response;
/** /**
* Exception representing an unexpected response * Exception representing an unexpected response
* *
* @since 3.0.0 * @since 3.0.0
*/ */
class UnexpectedResponse extends \DomainException class UnexpectedResponse extends DomainException
{ {
/** /**
* The Response object. * The Response object.
@ -25,18 +27,23 @@ class UnexpectedResponse extends \DomainException
* @since 3.0.0 * @since 3.0.0
*/ */
private $response; private $response;
/**
/**
* Constructor * Constructor
* *
* @param Response $response The Response object. * @param Response $response The Response object.
* @param string $message The Exception message to throw. * @param string $message The Exception message to throw.
* @param integer $code The Exception code. * @param int $code The Exception code.
* @param \Exception $previous The previous exception used for the exception chaining. * @param Exception|null $previous The previous exception used for the exception chaining.
* *
* @since 3.0.0 * @since 3.0.0
*/ */
public function __construct(Response $response, $message = '', $code = 0, \Exception $previous = null) public function __construct(
{ Response $response,
$message = '',
$code = 0,
Exception $previous = null
) {
parent::__construct($message, $code, $previous); parent::__construct($message, $code, $previous);
$this->response = $response; $this->response = $response;
} }
@ -48,7 +55,7 @@ class UnexpectedResponse extends \DomainException
* *
* @since 3.0.0 * @since 3.0.0
*/ */
public function getResponse() public function getResponse(): Response
{ {
return $this->response; return $this->response;
} }

View File

@ -31,7 +31,8 @@ class BranchField extends ListField
* @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
@ -40,14 +41,19 @@ class BranchField extends ListField
*/ */
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')) $query->select(
'DISTINCT(' . $db->quoteName('branch') . ') AS ' . $db->quoteName(
'text'
)
)
->select($db->quoteName('branch', 'value')) ->select($db->quoteName('branch', 'value'))
->from('#__patchtester_pulls') ->from('#__patchtester_pulls')
->where($db->quoteName('branch') . ' != ' . $db->quote('')) ->where($db->quoteName('branch') . ' != ' . $db->quote(''))
->order($db->quoteName('branch') . ' ASC'); ->order($db->quoteName('branch') . ' ASC');
$options = $db->setQuery($query)->loadAssocList(); $options = $db->setQuery($query)->loadAssocList();
return array_merge(parent::getOptions(), $options); return array_merge(parent::getOptions(), $options);
} }
} }

View File

@ -31,7 +31,8 @@ class LabelField extends ListField
* @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
@ -40,13 +41,18 @@ class LabelField extends ListField
*/ */
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')) $query->select(
'DISTINCT(' . $db->quoteName('name') . ') AS ' . $db->quoteName(
'text'
)
)
->select($db->quoteName('name', 'value')) ->select($db->quoteName('name', 'value'))
->from($db->quoteName('#__patchtester_pulls_labels')) ->from($db->quoteName('#__patchtester_pulls_labels'))
->order($db->quoteName('name') . ' ASC'); ->order($db->quoteName('name') . ' ASC');
$options = $db->setQuery($query)->loadAssocList(); $options = $db->setQuery($query)->loadAssocList();
return array_merge(parent::getOptions(), $options); return array_merge(parent::getOptions(), $options);
} }
} }

View File

@ -11,9 +11,9 @@ namespace Joomla\Component\Patchtester\Administrator\GitHub;
use Joomla\CMS\Http\Http; use Joomla\CMS\Http\Http;
use Joomla\CMS\Http\HttpFactory; use Joomla\CMS\Http\HttpFactory;
use Joomla\CMS\Http\Response;
use Joomla\CMS\Uri\Uri; use Joomla\CMS\Uri\Uri;
use Joomla\Component\Patchtester\Administrator\Github\Exception\UnexpectedResponse; use Joomla\Component\Patchtester\Administrator\Exception\UnexpectedResponse;
use Joomla\Http\Response;
use Joomla\Registry\Registry; use Joomla\Registry\Registry;
/** /**
@ -30,18 +30,19 @@ class GitHub
* @since 3.0.0 * @since 3.0.0
*/ */
protected $options; protected $options;
/** /**
* The HTTP client object to use in sending HTTP requests. * The HTTP client object to use in sending HTTP requests.
* *
* @var Http * @var Http
* @since 3.0.0 * @since 3.0.0
*/ */
protected $client; protected $client;
/**
/**
* Constructor. * Constructor.
* *
* @param Registry $options Connector options. * @param Registry|null $options Connector options.
* @param Http $client The HTTP client object. * @param Http|null $client The HTTP client object.
* *
* @since 3.0.0 * @since 3.0.0
*/ */
@ -58,7 +59,7 @@ class GitHub
* *
* @since 3.0.0 * @since 3.0.0
*/ */
public function getClient() public function getClient(): Http
{ {
return $this->client; return $this->client;
} }
@ -66,22 +67,27 @@ class GitHub
/** /**
* Get the diff for a pull request. * Get the diff for a pull request.
* *
* @param string $user The name of the owner of the GitHub repository. * @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository. * @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number. * @param int $pullId The pull request number.
* *
* @return Response * @return Response
* *
* @since 3.0.0 * @since 3.0.0
*/ */
public function getDiffForPullRequest($user, $repo, $pullId) public function getDiffForPullRequest(
{ string $user,
// Build the request path. string $repo,
$path = "/repos/$user/$repo/pulls/" . (int) $pullId; int $pullId
// Build the request headers. ): Response {
$headers = array('Accept' => 'application/vnd.github.diff'); $path = "/repos/$user/$repo/pulls/" . $pullId;
$headers = ['Accept' => 'application/vnd.github.diff'];
$prepared = $this->prepareRequest($path, 0, 0, $headers); $prepared = $this->prepareRequest($path, 0, 0, $headers);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
} }
/** /**
@ -89,27 +95,28 @@ class GitHub
* *
* This method will add appropriate pagination details if necessary and also prepend the API url to have a complete URL for the request. * This method will add appropriate pagination details if necessary and also prepend the API url to have a complete URL for the request.
* *
* @param string $path Path to process * @param string $path Path to process
* @param integer $page Page to request * @param int $page Page to request
* @param integer $limit Number of results to return per page * @param int $limit Number of results to return per page
* @param array $headers The headers to send with the request * @param array $headers The headers to send with the request
* *
* @return array Associative array containing the prepared URL and request headers * @return array Associative array containing the prepared URL and request headers
* *
* @since 3.0.0 * @since 3.0.0
*/ */
protected function prepareRequest( protected function prepareRequest(
$path, string $path,
$page = 0, int $page = 0,
$limit = 0, int $limit = 0,
array $headers = array() array $headers = []
) { ): array {
$url = $this->fetchUrl($path, $page, $limit); $url = $this->fetchUrl($path, $page, $limit);
if ($token = $this->options->get('gh.token', false)) { if ($token = $this->options->get('gh.token', false)) {
$headers['Authorization'] = "token $token"; $headers['Authorization'] = "token $token";
} }
return array('url' => $url, 'headers' => $headers); return ['url' => $url, 'headers' => $headers];
} }
/** /**
@ -118,9 +125,9 @@ class GitHub
* This method will add appropriate pagination details and basic authentication credentials if necessary * 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. * and also prepend the API url to have a complete URL for the request.
* *
* @param string $path URL to inflect * @param string $path URL to inflect
* @param integer $page Page to request * @param int $page Page to request
* @param integer $limit Number of results to return per page * @param int $limit Number of results to return per page
* *
* @return string The request URL. * @return string The request URL.
* *
@ -130,38 +137,39 @@ class GitHub
{ {
// Get a new Uri object using the API URL and given path. // Get a new Uri object using the API URL and given path.
$uri = new Uri($this->options->get('api.url') . $path); $uri = new Uri($this->options->get('api.url') . $path);
// If we have a defined page number add it to the JUri object. // If we have a defined page number add it to the JUri object.
if ($page > 0) { if ($page > 0) {
$uri->setVar('page', (int) $page); $uri->setVar('page', (int)$page);
} }
// If we have a defined items per page add it to the JUri object. // If we have a defined items per page add it to the JUri object.
if ($limit > 0) { if ($limit > 0) {
$uri->setVar('per_page', (int) $limit); $uri->setVar('per_page', (int)$limit);
} }
return (string) $uri; return (string)$uri;
} }
/** /**
* Process the response and return it. * Process the response and return it.
* *
* @param Response $response The response. * @param Response $response The response.
* @param integer $expectedCode The expected response code. * @param int $expectedCode The expected response code.
* *
* @return Response * @return Response
* *
* @throws UnexpectedResponse * @throws UnexpectedResponse
*@since 3.0.0 * @since 3.0.0
*/ */
protected function processResponse(Response $response, $expectedCode = 200) protected function processResponse(
{ Response $response,
int $expectedCode = 200
): Response {
// Validate the response code. // Validate the response code.
if ($response->code != $expectedCode) { if ($response->code != $expectedCode) {
// Decode the error response and throw an exception. // Decode the error response and throw an exception.
$body = json_decode($response->body); $body = json_decode($response->body);
$error = isset($body->error) ? $body->error $error = $body->error ?? ($body->message ?? 'Unknown Error');
: (isset($body->message) ? $body->message : 'Unknown Error');
throw new UnexpectedResponse( throw new UnexpectedResponse(
$response, $response,
@ -187,15 +195,17 @@ class GitHub
*/ */
public function getFileContents($user, $repo, $path, $ref = null) public function getFileContents($user, $repo, $path, $ref = null)
{ {
$path = "/repos/$user/$repo/contents/$path"; $path = "/repos/$user/$repo/contents/$path";
$prepared = $this->prepareRequest($path); $prepared = $this->prepareRequest($path);
if ($ref) { if ($ref) {
$url = new Uri($prepared['url']); $url = new Uri($prepared['url']);
$url->setVar('ref', $ref); $url->setVar('ref', $ref);
$prepared['url'] = (string) $url; $prepared['url'] = (string)$url;
} }
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers'])); return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
} }
/** /**
@ -212,18 +222,22 @@ class GitHub
public function getFilesForPullRequest($user, $repo, $pullId, $page = 1) public function getFilesForPullRequest($user, $repo, $pullId, $page = 1)
{ {
// Build the request path. // Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId . '/files?page=' . $page; $path = "/repos/$user/$repo/pulls/" . (int)$pullId . '/files?page='
. $page;
$prepared = $this->prepareRequest($path); $prepared = $this->prepareRequest($path);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
} }
/** /**
* Get a list of the open issues for a repository. * Get a list of the open issues for a repository.
* *
* @param string $user The name of the owner of the GitHub repository. * @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name 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 int $page The page number from which to get items.
* @param integer $limit The number of items on a page. * @param int $limit The number of items on a page.
* *
* @return Response * @return Response
* *
@ -236,16 +250,19 @@ class GitHub
$page, $page,
$limit $limit
); );
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
} }
/** /**
* Get a list of the open pull requests for a repository. * 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 $user The name of the owner of the GitHub repository.
* @param string $repo The name 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 int $page The page number from which to get items.
* @param integer $limit The number of items on a page. * @param int $limit The number of items on a page.
* *
* @return Response * @return Response
* *
@ -258,7 +275,10 @@ class GitHub
$page, $page,
$limit $limit
); );
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
} }
/** /**
@ -279,9 +299,9 @@ class GitHub
/** /**
* Get a single pull request. * Get a single pull request.
* *
* @param string $user The name of the owner of the GitHub repository. * @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository. * @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number. * @param int $pullId The pull request number.
* *
* @return Response * @return Response
* *
@ -290,9 +310,12 @@ class GitHub
public function getPullRequest($user, $repo, $pullId) public function getPullRequest($user, $repo, $pullId)
{ {
// Build the request path. // Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId; $path = "/repos/$user/$repo/pulls/" . (int)$pullId;
$prepared = $this->prepareRequest($path); $prepared = $this->prepareRequest($path);
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
} }
/** /**
@ -305,7 +328,10 @@ class GitHub
public function getRateLimit() public function getRateLimit()
{ {
$prepared = $this->prepareRequest('/rate_limit'); $prepared = $this->prepareRequest('/rate_limit');
return $this->processResponse($this->client->get($prepared['url'], $prepared['headers']));
return $this->processResponse(
$this->client->get($prepared['url'], $prepared['headers'])
);
} }
/** /**
@ -321,6 +347,7 @@ class GitHub
public function setOption($key, $value) public function setOption($key, $value)
{ {
$this->options->set($key, $value); $this->options->set($key, $value);
return $this; return $this;
} }
} }

View File

@ -0,0 +1,25 @@
<?php
namespace Joomla\Component\Patchtester\Administrator;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\Registry\Registry;
/**
* @package ${NAMESPACE}
* @subpackage
*
* @copyright A copyright
* @license A "Slug" license name e.g. GPL2
*/
trait GithubCredentialsTrait
{
protected function getCredentials() {
$state = new Registry();
$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

@ -12,8 +12,8 @@ namespace Joomla\Component\Patchtester\Administrator\Helper;
use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory; use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use Joomla\Component\Patchtester\Administrator\GitHub\GitHub;
use Joomla\Registry\Registry; use Joomla\Registry\Registry;
use src\GitHub\GitHub;
// phpcs:disable PSR1.Files.SideEffects // phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die; \defined('_JEXEC') or die;

View File

@ -10,7 +10,6 @@
namespace Joomla\Component\Patchtester\Administrator\Model; namespace Joomla\Component\Patchtester\Administrator\Model;
// phpcs:disable PSR1.Files.SideEffects // phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die; \defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects // phpcs:enable PSR1.Files.SideEffects

View File

@ -15,15 +15,17 @@ use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Path; use Joomla\CMS\Filesystem\Path;
use Joomla\CMS\Http\HttpFactory; use Joomla\CMS\Http\HttpFactory;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Factory\MVCFactoryInterface;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Version; use Joomla\CMS\Version;
use Joomla\Component\Patchtester\Administrator\Github\Exception\UnexpectedResponse;
use Joomla\Component\Patchtester\Administrator\GitHub\GitHub;
use Joomla\Component\Patchtester\Administrator\GithubCredentialsTrait;
use Joomla\Component\Patchtester\Administrator\Helper\Helper; use Joomla\Component\Patchtester\Administrator\Helper\Helper;
use Joomla\Database\DatabaseDriver;
use Joomla\Filesystem\File; use Joomla\Filesystem\File;
use Joomla\Filesystem\Folder; use Joomla\Filesystem\Folder;
use Joomla\Registry\Registry; use Joomla\Registry\Registry;
use RuntimeException; use RuntimeException;
use src\GitHub\Exception\UnexpectedResponse;
use src\GitHub\GitHub;
use stdClass; use stdClass;
// phpcs:disable PSR1.Files.SideEffects // phpcs:disable PSR1.Files.SideEffects
@ -35,8 +37,9 @@ use stdClass;
* *
* @since 2.0 * @since 2.0
*/ */
class PullModel extends AbstractModel class PullModel extends BaseDatabaseModel
{ {
use GithubCredentialsTrait;
/** /**
* Array containing top level non-production folders * Array containing top level non-production folders
* *
@ -44,21 +47,21 @@ class PullModel extends AbstractModel
* @since 2.0 * @since 2.0
*/ */
protected $nonProductionFolders protected $nonProductionFolders
= array( = [
'build', 'build',
'docs', 'docs',
'installation', 'installation',
'tests', 'tests',
'.github', '.github',
); ];
/** /**
* Array containing non-production files * Array containing non-production files
* *
* @var array * @var array
* @since 2.0 * @since 2.0
*/ */
protected $nonProductionFiles protected $nonProductionFiles
= array( = [
'.drone.yml', '.drone.yml',
'.gitignore', '.gitignore',
'.php_cs', '.php_cs',
@ -77,25 +80,27 @@ class PullModel extends AbstractModel
'jorobo.dist.ini', 'jorobo.dist.ini',
'manifest.xml', 'manifest.xml',
'crowdin.yaml', 'crowdin.yaml',
); ];
/**
/**
* The namespace mapper * The namespace mapper
* *
* @var \JNamespacePsr4Map * @var \JNamespacePsr4Map
* @since 4.0.0 * @since 4.0.0
*/ */
protected $namespaceMapper; protected $namespaceMapper;
/**
* Instantiate the model. /**
* * @inheritDoc
* @param Registry $state The model state.
* @param DatabaseDriver $db The database adpater.
*
* @since 4.0.0
*/ */
public function __construct(Registry $state = null, DatabaseDriver $db = null) public function __construct(
{ $config = [],
parent::__construct($state, $db); MVCFactoryInterface $factory = null
) {
parent::__construct($config, $factory);
$this->state = $this->getCredentials();
$this->namespaceMapper = new \JNamespacePsr4Map(); $this->namespaceMapper = new \JNamespacePsr4Map();
} }
@ -103,19 +108,19 @@ class PullModel extends AbstractModel
* Patches the code with the supplied pull request * Patches the code with the supplied pull request
* However uses different methods for different repositories. * However uses different methods for different repositories.
* *
* @param integer $id ID of the pull request to apply * @param int $id ID of the pull request to apply
* *
* @return boolean * @return bool
*
* @since 3.0
* *
* @throws RuntimeException * @throws RuntimeException
* @since 3.0
*
*/ */
public function apply(int $id): bool public function apply(int $id): bool
{ {
$params = ComponentHelper::getParams('com_patchtester'); $params = ComponentHelper::getParams('com_patchtester');
// Decide based on repository settings whether patch will be applied through Github or CIServer // Decide based on repository settings whether patch will be applied through Github or CIServer
if ((bool) $params->get('ci_switch', 0)) { if ((bool)$params->get('ci_switch', 0)) {
return $this->applyWithCIServer($id); return $this->applyWithCIServer($id);
} }
@ -129,9 +134,9 @@ class PullModel extends AbstractModel
* *
* @return boolean * @return boolean
* *
* @throws RuntimeException
* @since 3.0 * @since 3.0
* *
* @throws RuntimeException
*/ */
private function applyWithCIServer(int $id): bool private function applyWithCIServer(int $id): bool
{ {
@ -151,10 +156,10 @@ class PullModel extends AbstractModel
Folder::create($ciSettings->get('folder.temp')); Folder::create($ciSettings->get('folder.temp'));
} }
$tempPath = $ciSettings->get('folder.temp') . '/' . $id; $tempPath = $ciSettings->get('folder.temp') . '/' . $id;
$backupsPath = $ciSettings->get('folder.backups') . '/' . $id; $backupsPath = $ciSettings->get('folder.backups') . '/' . $id;
$delLogPath = $tempPath . '/' . $ciSettings->get('zip.log.name'); $delLogPath = $tempPath . '/' . $ciSettings->get('zip.log.name');
$zipPath = $tempPath . '/' . $ciSettings->get('zip.name'); $zipPath = $tempPath . '/' . $ciSettings->get('zip.name');
$serverZipPath = sprintf($ciSettings->get('zip.url'), $id); $serverZipPath = sprintf($ciSettings->get('zip.url'), $id);
// Patch has already been applied // Patch has already been applied
if (file_exists($backupsPath)) { if (file_exists($backupsPath)) {
@ -163,7 +168,10 @@ class PullModel extends AbstractModel
$version = new Version(); $version = new Version();
$httpOption = new Registry(); $httpOption = new Registry();
$httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); $httpOption->set(
'userAgent',
$version->getUserAgent('Joomla', true, false)
);
// Try to download the zip file // Try to download the zip file
try { try {
$http = HttpFactory::getHttp($httpOption); $http = HttpFactory::getHttp($httpOption);
@ -172,24 +180,33 @@ class PullModel extends AbstractModel
$result = null; $result = null;
} }
if ($result === null || ($result->getStatusCode() !== 200 && $result->getStatusCode() !== 310)) { if ($result === null
throw new RuntimeException(Text::_('COM_PATCHTESTER_SERVER_RESPONDED_NOT_200')); || ($result->getStatusCode() !== 200
&& $result->getStatusCode() !== 310)
) {
throw new RuntimeException(
Text::_('COM_PATCHTESTER_SERVER_RESPONDED_NOT_200')
);
} }
// Assign to variable to avlod PHP notice "Indirect modification of overloaded property" // Assign to variable to avlod PHP notice "Indirect modification of overloaded property"
$content = (string) $result->getBody(); $content = (string)$result->getBody();
// Write the file to disk // Write the file to disk
File::write($zipPath, $content); File::write($zipPath, $content);
// Check if zip folder could have been downloaded // Check if zip folder could have been downloaded
if (!file_exists($zipPath)) { if (!file_exists($zipPath)) {
throw new RuntimeException(Text::_('COM_PATCHTESTER_ZIP_DOES_NOT_EXIST')); throw new RuntimeException(
Text::_('COM_PATCHTESTER_ZIP_DOES_NOT_EXIST')
);
} }
Folder::create($tempPath); Folder::create($tempPath);
$zip = new Zip(); $zip = new Zip();
if (!$zip->extract($zipPath, $tempPath)) { if (!$zip->extract($zipPath, $tempPath)) {
Folder::delete($tempPath); Folder::delete($tempPath);
throw new RuntimeException(Text::_('COM_PATCHTESTER_ZIP_EXTRACT_FAILED')); throw new RuntimeException(
Text::_('COM_PATCHTESTER_ZIP_EXTRACT_FAILED')
);
} }
// Remove zip to avoid get listing afterwards // Remove zip to avoid get listing afterwards
@ -198,14 +215,16 @@ class PullModel extends AbstractModel
if ($this->verifyAutoloader($tempPath) === false) { if ($this->verifyAutoloader($tempPath) === false) {
// There is something broken in the autoloader, clean up and go back // There is something broken in the autoloader, clean up and go back
Folder::delete($tempPath); Folder::delete($tempPath);
throw new RuntimeException(Text::_('COM_PATCHTESTER_PATCH_BREAKS_SITE')); throw new RuntimeException(
Text::_('COM_PATCHTESTER_PATCH_BREAKS_SITE')
);
} }
// Get files from deleted_logs // Get files from deleted_logs
$deletedFiles = (file_exists($delLogPath) ? file($delLogPath) : []); $deletedFiles = (file_exists($delLogPath) ? file($delLogPath) : []);
$deletedFiles = array_map('trim', $deletedFiles); $deletedFiles = array_map('trim', $deletedFiles);
if (file_exists($delLogPath)) { if (file_exists($delLogPath)) {
// Remove deleted_logs to avoid get listing afterwards // Remove deleted_logs to avoid get listing afterwards
File::delete($delLogPath); File::delete($delLogPath);
} }
@ -220,7 +239,7 @@ class PullModel extends AbstractModel
$filePath = explode(DIRECTORY_SEPARATOR, Path::clean($file)); $filePath = explode(DIRECTORY_SEPARATOR, Path::clean($file));
array_pop($filePath); array_pop($filePath);
$filePath = implode(DIRECTORY_SEPARATOR, $filePath); $filePath = implode(DIRECTORY_SEPARATOR, $filePath);
// Deleted_logs returns files as well as folder, if value is folder, unset and skip // Deleted_logs returns files as well as folder, if value is folder, unset and skip
if (is_dir(JPATH_ROOT . '/' . $file)) { if (is_dir(JPATH_ROOT . '/' . $file)) {
unset($files[$key]); unset($files[$key]);
continue; continue;
@ -232,21 +251,37 @@ class PullModel extends AbstractModel
Folder::create($backupsPath . '/' . $filePath); Folder::create($backupsPath . '/' . $filePath);
} }
File::move(JPATH_ROOT . '/' . $file, $backupsPath . '/' . $file); File::move(
JPATH_ROOT . '/' . $file,
$backupsPath . '/' . $file
);
} }
if (file_exists($tempPath . '/' . $file)) { if (file_exists($tempPath . '/' . $file)) {
// Create directories if they don't exist until file // Create directories if they don't exist until file
if (!file_exists(JPATH_ROOT . '/' . $filePath) || !is_dir(JPATH_ROOT . '/' . $filePath)) { if (!file_exists(JPATH_ROOT . '/' . $filePath)
|| !is_dir(
JPATH_ROOT . '/' . $filePath
)
) {
Folder::create(JPATH_ROOT . '/' . $filePath); Folder::create(JPATH_ROOT . '/' . $filePath);
} }
File::copy($tempPath . '/' . $file, JPATH_ROOT . '/' . $file); File::copy(
$tempPath . '/' . $file,
JPATH_ROOT . '/' . $file
);
} }
} catch (RuntimeException $exception) { } catch (RuntimeException $exception) {
Folder::delete($tempPath); Folder::delete($tempPath);
Folder::move($backupsPath, $backupsPath . '_failed'); Folder::move($backupsPath, $backupsPath . '_failed');
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_FAILED_APPLYING_PATCH', $file, $exception->getMessage())); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_FAILED_APPLYING_PATCH',
$file,
$exception->getMessage()
)
);
} }
} }
@ -258,6 +293,7 @@ class PullModel extends AbstractModel
// Change the media version // Change the media version
$version = new Version(); $version = new Version();
$version->refreshMediaVersion(); $version->refreshMediaVersion();
return true; return true;
} }
@ -269,9 +305,9 @@ class PullModel extends AbstractModel
* *
* @return stdClass The pull request data * @return stdClass The pull request data
* *
* @throws RuntimeException
* @since 2.0 * @since 2.0
* *
* @throws RuntimeException
*/ */
private function retrieveGitHubData(GitHub $github, int $id): stdClass private function retrieveGitHubData(GitHub $github, int $id): stdClass
{ {
@ -279,19 +315,38 @@ class PullModel extends AbstractModel
$rateResponse = $github->getRateLimit(); $rateResponse = $github->getRateLimit();
$rate = json_decode($rateResponse->body, false); $rate = json_decode($rateResponse->body, false);
} catch (UnexpectedResponse $exception) { } catch (UnexpectedResponse $exception) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $exception->getMessage()), $exception->getCode(), $exception); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB',
$exception->getMessage()
), $exception->getCode(), $exception
);
} }
// If over the API limit, we can't build this list // If over the API limit, we can't build this list
if ((int) $rate->resources->core->remaining === 0) { if ((int)$rate->resources->core->remaining === 0) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_API_LIMIT_LIST', Factory::getDate($rate->resources->core->reset))); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_API_LIMIT_LIST',
Factory::getDate($rate->resources->core->reset)
)
);
} }
try { try {
$pullResponse = $github->getPullRequest($this->getState()->get('github_user'), $this->getState()->get('github_repo'), $id); $pullResponse = $github->getPullRequest(
$this->getState()->get('github_user'),
$this->getState()->get('github_repo'),
$id
);
$pull = json_decode($pullResponse->body, false); $pull = json_decode($pullResponse->body, false);
} catch (UnexpectedResponse $exception) { } catch (UnexpectedResponse $exception) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $exception->getMessage()), $exception->getCode(), $exception); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB',
$exception->getMessage()
), $exception->getCode(), $exception
);
} }
return $pull; return $pull;
@ -309,7 +364,7 @@ class PullModel extends AbstractModel
private function verifyAutoloader(string $path): bool private function verifyAutoloader(string $path): bool
{ {
$result = false; $result = false;
// Check if we have an autoload file // Check if we have an autoload file
if (!file_exists($path . '/libraries/vendor/autoload.php')) { if (!file_exists($path . '/libraries/vendor/autoload.php')) {
return $result; return $result;
} }
@ -322,35 +377,50 @@ class PullModel extends AbstractModel
'autoload_psr4.php', 'autoload_psr4.php',
]; ];
$filesToCheck = []; $filesToCheck = [];
array_walk($composerFiles, static function ($composerFile) use (&$filesToCheck, $path) { array_walk(
$composerFiles,
if (file_exists($path . '/libraries/vendor/composer/' . $composerFile) === false) { static function ($composerFile) use (&$filesToCheck, $path) {
return; if (file_exists(
} $path . '/libraries/vendor/composer/' . $composerFile
) === false
if ($composerFile === 'autoload_static.php') { ) {
// Get the generated token
$autoload = file_get_contents($path . '/libraries/vendor/autoload.php');
$resultMatch = preg_match('/ComposerAutoloaderInit(.*)::/', $autoload, $match);
if (!$resultMatch) {
return; return;
} }
// Check if the class is already defined if ($composerFile === 'autoload_static.php') {
$autoloadClass = '\Composer\Autoload\ComposerStaticInit' . $match[1]; // Get the generated token
if (class_exists($autoloadClass)) { $autoload = file_get_contents(
return; $path . '/libraries/vendor/autoload.php'
} );
$resultMatch = preg_match(
'/ComposerAutoloaderInit(.*)::/',
$autoload,
$match
);
if (!$resultMatch) {
return;
}
require_once $path . '/libraries/vendor/composer/autoload_static.php'; // Check if the class is already defined
// Get all the files $autoloadClass = '\Composer\Autoload\ComposerStaticInit'
$files = $autoloadClass::$files; . $match[1];
$filesToCheck = array_merge($filesToCheck, $files); if (class_exists($autoloadClass)) {
} else { return;
$files = require $path . '/libraries/vendor/composer/' . $composerFile; }
$filesToCheck = array_merge($filesToCheck, $files);
require_once $path
. '/libraries/vendor/composer/autoload_static.php';
// Get all the files
$files = $autoloadClass::$files;
$filesToCheck = array_merge($filesToCheck, $files);
} else {
$files = require $path
. '/libraries/vendor/composer/' . $composerFile;
$filesToCheck = array_merge($filesToCheck, $files);
}
} }
}); );
return $this->checkFilesExist($filesToCheck, $path); return $this->checkFilesExist($filesToCheck, $path);
} }
@ -393,24 +463,29 @@ class PullModel extends AbstractModel
* *
* @since 3.0 * @since 3.0
*/ */
private function saveAppliedPatch(int $id, array $fileList, string $sha = null): int private function saveAppliedPatch(
{ int $id,
$record = (object) array( array $fileList,
string $sha = null
): int {
$record = (object)[
'pull_id' => $id, 'pull_id' => $id,
'data' => json_encode($fileList), 'data' => json_encode($fileList),
'patched_by' => Factory::getUser()->id, 'patched_by' => Factory::getUser()->id,
'applied' => 1, 'applied' => 1,
'applied_version' => JVERSION, 'applied_version' => JVERSION,
); ];
$db = $this->getDb(); $db = $this->getDatabase();
$db->insertObject('#__patchtester_tests', $record); $db->insertObject('#__patchtester_tests', $record);
$insertId = $db->insertid(); $insertId = $db->insertid();
if ($sha !== null) { if ($sha !== null) {
// Insert the retrieved commit SHA into the pulls table for this item // Insert the retrieved commit SHA into the pulls table for this item
$db->setQuery($db->getQuery(true) $db->setQuery(
$db->getQuery(true)
->update('#__patchtester_pulls') ->update('#__patchtester_pulls')
->set('sha = ' . $db->quote($sha)) ->set('sha = ' . $db->quote($sha))
->where($db->quoteName('pull_id') . ' = ' . $id))->execute(); ->where($db->quoteName('pull_id') . ' = ' . $id)
)->execute();
} }
return $insertId; return $insertId;
@ -423,14 +498,14 @@ class PullModel extends AbstractModel
* *
* @return boolean * @return boolean
* *
* @throws RuntimeException
* @since 2.0 * @since 2.0
* *
* @throws RuntimeException
*/ */
private function applyWithGitHub(int $id): bool private function applyWithGitHub(int $id): bool
{ {
$github = Helper::initializeGithub(); $github = Helper::initializeGithub();
$pull = $this->retrieveGitHubData($github, $id); $pull = $this->retrieveGitHubData($github, $id);
if ($pull->head->repo === null) { if ($pull->head->repo === null) {
throw new RuntimeException(Text::_('COM_PATCHTESTER_REPO_IS_GONE')); throw new RuntimeException(Text::_('COM_PATCHTESTER_REPO_IS_GONE'));
} }
@ -438,7 +513,12 @@ class PullModel extends AbstractModel
try { try {
$files = $this->getFiles($id, 1); $files = $this->getFiles($id, 1);
} catch (UnexpectedResponse $exception) { } catch (UnexpectedResponse $exception) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $exception->getMessage()), $exception->getCode(), $exception); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB',
$exception->getMessage()
), $exception->getCode(), $exception
);
} }
if (!count($files)) { if (!count($files)) {
@ -454,7 +534,12 @@ class PullModel extends AbstractModel
switch ($file->action) { switch ($file->action) {
case 'deleted': case 'deleted':
if (!file_exists(JPATH_ROOT . '/' . $file->filename)) { if (!file_exists(JPATH_ROOT . '/' . $file->filename)) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_FILE_DELETED_DOES_NOT_EXIST_S', $file->filename)); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_FILE_DELETED_DOES_NOT_EXIST_S',
$file->filename
)
);
} }
@ -463,17 +548,43 @@ class PullModel extends AbstractModel
case 'modified': case 'modified':
case 'renamed': case 'renamed':
// If the backup file already exists, we can't apply the patch // If the backup file already exists, we can't apply the patch
if (file_exists(JPATH_COMPONENT . '/backups/' . md5($file->filename) . '.txt')) { if (file_exists(
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_CONFLICT_S', $file->filename)); JPATH_COMPONENT . '/backups/' . md5($file->filename)
. '.txt'
)
) {
throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_CONFLICT_S',
$file->filename
)
);
} }
if ($file->action === 'modified' && !file_exists(JPATH_ROOT . '/' . $file->filename)) { if ($file->action === 'modified'
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_FILE_MODIFIED_DOES_NOT_EXIST_S', $file->filename)); && !file_exists(
JPATH_ROOT . '/' . $file->filename
)
) {
throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_FILE_MODIFIED_DOES_NOT_EXIST_S',
$file->filename
)
);
} }
try { try {
$contentsResponse = $github->getFileContents($pull->head->user->login, $pull->head->repo->name, $file->repofilename, urlencode($pull->head->ref)); $contentsResponse = $github->getFileContents(
$contents = json_decode($contentsResponse->body, false); $pull->head->user->login,
$pull->head->repo->name,
$file->repofilename,
urlencode($pull->head->ref)
);
$contents = json_decode(
$contentsResponse->body,
false
);
// In case encoding type ever changes // In case encoding type ever changes
switch ($contents->encoding) { switch ($contents->encoding) {
case 'base64': case 'base64':
@ -481,10 +592,19 @@ class PullModel extends AbstractModel
break; break;
default: default:
throw new RuntimeException(Text::_('COM_PATCHTESTER_ERROR_UNSUPPORTED_ENCODING')); throw new RuntimeException(
Text::_(
'COM_PATCHTESTER_ERROR_UNSUPPORTED_ENCODING'
)
);
} }
} catch (UnexpectedResponse $exception) { } catch (UnexpectedResponse $exception) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $exception->getMessage()), $exception->getCode(), $exception); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB',
$exception->getMessage()
), $exception->getCode(), $exception
);
} }
break; break;
@ -496,40 +616,84 @@ class PullModel extends AbstractModel
// We only create a backup if the file already exists // We only create a backup if the file already exists
if ( if (
$file->action === 'deleted' $file->action === 'deleted'
|| (file_exists(JPATH_ROOT . '/' . $file->filename) && $file->action === 'modified') || (file_exists(JPATH_ROOT . '/' . $file->filename)
|| (file_exists(JPATH_ROOT . '/' . $file->originalFile) && $file->action === 'renamed') && $file->action === 'modified')
|| (file_exists(JPATH_ROOT . '/' . $file->originalFile)
&& $file->action === 'renamed')
) { ) {
$filename = $file->action === 'renamed' ? $file->originalFile : $file->filename; $filename = $file->action === 'renamed' ? $file->originalFile
: $file->filename;
$src = JPATH_ROOT . '/' . $filename; $src = JPATH_ROOT . '/' . $filename;
$dest = JPATH_COMPONENT . '/backups/' . md5($filename) . '.txt'; $dest = JPATH_COMPONENT . '/backups/' . md5($filename)
. '.txt';
if (!File::copy(Path::clean($src), $dest)) { if (!File::copy(Path::clean($src), $dest)) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_COPY_FILE', $src, $dest)); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_COPY_FILE',
$src,
$dest
)
);
} }
} }
switch ($file->action) { switch ($file->action) {
case 'modified': case 'modified':
case 'added': case 'added':
if (!File::write(Path::clean(JPATH_ROOT . '/' . $file->filename), $file->body)) { if (!File::write(
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_WRITE_FILE', JPATH_ROOT . '/' . $file->filename)); Path::clean(JPATH_ROOT . '/' . $file->filename),
$file->body
)
) {
throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_WRITE_FILE',
JPATH_ROOT . '/' . $file->filename
)
);
} }
break; break;
case 'deleted': case 'deleted':
if (!File::delete(Path::clean(JPATH_ROOT . '/' . $file->filename))) { if (!File::delete(
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_ROOT . '/' . $file->filename)); Path::clean(JPATH_ROOT . '/' . $file->filename)
)
) {
throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE',
JPATH_ROOT . '/' . $file->filename
)
);
} }
break; break;
case 'renamed': case 'renamed':
if (!File::delete(Path::clean(JPATH_ROOT . '/' . $file->originalFile))) { if (!File::delete(
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_ROOT . '/' . $file->originalFile)); Path::clean(JPATH_ROOT . '/' . $file->originalFile)
)
) {
throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE',
JPATH_ROOT . '/' . $file->originalFile
)
);
} }
if (!File::write(Path::clean(JPATH_ROOT . '/' . $file->filename), $file->body)) { if (!File::write(
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_WRITE_FILE', JPATH_ROOT . '/' . $file->filename)); Path::clean(JPATH_ROOT . '/' . $file->filename),
$file->body
)
) {
throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_WRITE_FILE',
JPATH_ROOT . '/' . $file->filename
)
);
} }
break; break;
@ -545,6 +709,7 @@ class PullModel extends AbstractModel
// Change the media version // Change the media version
$version = new Version(); $version = new Version();
$version->refreshMediaVersion(); $version->refreshMediaVersion();
return true; return true;
} }
@ -561,11 +726,19 @@ class PullModel extends AbstractModel
*/ */
private function getFiles(int $id, int $page, array $files = []): array private function getFiles(int $id, int $page, array $files = []): array
{ {
$github = Helper::initializeGithub(); $github = Helper::initializeGithub();
$filesResponse = $github->getFilesForPullRequest($this->getState()->get('github_user'), $this->getState()->get('github_repo'), $id, $page); $filesResponse = $github->getFilesForPullRequest(
$files = array_merge($files, json_decode($filesResponse->getBody(), false)); $this->getState()->get('github_user'),
$lastPage = 1; $this->getState()->get('github_repo'),
$headers = $filesResponse->getHeaders(); $id,
$page
);
$files = array_merge(
$files,
json_decode($filesResponse->getBody(), false)
);
$lastPage = 1;
$headers = $filesResponse->getHeaders();
if (!isset($headers['link'])) { if (!isset($headers['link'])) {
return $files; return $files;
@ -578,7 +751,7 @@ class PullModel extends AbstractModel
); );
if ($matches && isset($matches[0])) { if ($matches && isset($matches[0])) {
preg_match('/\d+/', $matches[0], $pages); preg_match('/\d+/', $matches[0], $pages);
$lastPage = (int) $pages[0]; $lastPage = (int)$pages[0];
} }
if ($page <= $lastPage) { if ($page <= $lastPage) {
@ -600,10 +773,10 @@ class PullModel extends AbstractModel
private function parseFileList(array $files): array private function parseFileList(array $files): array
{ {
$parsedFiles = array(); $parsedFiles = array();
/* /*
* Check if the patch tester is running in a development environment * Check if the patch tester is running in a development environment
* If we are not in development, we'll need to check the exclusion lists * If we are not in development, we'll need to check the exclusion lists
*/ */
$isDev = file_exists(JPATH_INSTALLATION . '/index.php'); $isDev = file_exists(JPATH_INSTALLATION . '/index.php');
foreach ($files as $file) { foreach ($files as $file) {
if (!$isDev) { if (!$isDev) {
@ -621,20 +794,24 @@ class PullModel extends AbstractModel
$prodFileName = $file->filename; $prodFileName = $file->filename;
$prodRenamedFileName = $file->previous_filename ?? false; $prodRenamedFileName = $file->previous_filename ?? false;
$filePath = explode('/', $prodFileName); $filePath = explode('/', $prodFileName);
// Remove the `src` here to match the CMS paths if needed // Remove the `src` here to match the CMS paths if needed
if ($filePath[0] === 'src') { if ($filePath[0] === 'src') {
$prodFileName = str_replace('src/', '', $prodFileName); $prodFileName = str_replace('src/', '', $prodFileName);
} }
if ($prodRenamedFileName) { if ($prodRenamedFileName) {
$filePath = explode('/', $prodRenamedFileName); $filePath = explode('/', $prodRenamedFileName);
// Remove the `src` here to match the CMS paths if needed // Remove the `src` here to match the CMS paths if needed
if ($filePath[0] === 'src') { if ($filePath[0] === 'src') {
$prodRenamedFileName = str_replace('src/', '', $prodRenamedFileName); $prodRenamedFileName = str_replace(
'src/',
'',
$prodRenamedFileName
);
} }
} }
$parsedFiles[] = (object) array( $parsedFiles[] = (object)array(
'action' => $file->status, 'action' => $file->status,
'filename' => $prodFileName, 'filename' => $prodFileName,
'repofilename' => $file->filename, 'repofilename' => $file->filename,
@ -654,14 +831,14 @@ class PullModel extends AbstractModel
* *
* @return boolean * @return boolean
* *
* @since 3.0
* @throws RuntimeException * @throws RuntimeException
* @since 3.0
*/ */
public function revert(int $id): bool public function revert(int $id): bool
{ {
$params = ComponentHelper::getParams('com_patchtester'); $params = ComponentHelper::getParams('com_patchtester');
// Decide based on repository settings whether patch will be applied through Github or CIServer // Decide based on repository settings whether patch will be applied through Github or CIServer
if ((bool) $params->get('ci_switch', 0)) { if ((bool)$params->get('ci_switch', 0)) {
return $this->revertWithCIServer($id); return $this->revertWithCIServer($id);
} }
@ -675,8 +852,8 @@ class PullModel extends AbstractModel
* *
* @return boolean * @return boolean
* *
* @since 3.0
* @throws RuntimeException * @throws RuntimeException
* @since 3.0
*/ */
public function revertWithCIServer(int $id): bool public function revertWithCIServer(int $id): bool
{ {
@ -690,10 +867,17 @@ class PullModel extends AbstractModel
$files = json_decode($testRecord->data, false); $files = json_decode($testRecord->data, false);
if (!$files) { if (!$files) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_READING_DATABASE_TABLE', __METHOD__, htmlentities($testRecord->data))); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_READING_DATABASE_TABLE',
__METHOD__,
htmlentities($testRecord->data)
)
);
} }
$backupsPath = $ciSettings->get('folder.backups') . "/$testRecord->pull_id"; $backupsPath = $ciSettings->get('folder.backups')
. "/$testRecord->pull_id";
foreach ($files as $file) { foreach ($files as $file) {
try { try {
$filePath = explode("\\", $file); $filePath = explode("\\", $file);
@ -704,11 +888,16 @@ class PullModel extends AbstractModel
File::delete(JPATH_ROOT . "/$file"); File::delete(JPATH_ROOT . "/$file");
// Move from backup, if it exists there // Move from backup, if it exists there
if (file_exists($backupsPath . '/' . $file)) { if (file_exists($backupsPath . '/' . $file)) {
File::move($backupsPath . '/' . $file, JPATH_ROOT . '/' . $file); File::move(
$backupsPath . '/' . $file,
JPATH_ROOT . '/' . $file
);
} }
// If folder is empty, remove it as well // If folder is empty, remove it as well
if (count(glob(JPATH_ROOT . '/' . $filePath . '/*')) === 0) { if (count(glob(JPATH_ROOT . '/' . $filePath . '/*'))
=== 0
) {
Folder::delete(JPATH_ROOT . '/' . $filePath); Folder::delete(JPATH_ROOT . '/' . $filePath);
} }
} elseif (file_exists($backupsPath . '/' . $file)) { } elseif (file_exists($backupsPath . '/' . $file)) {
@ -717,10 +906,19 @@ class PullModel extends AbstractModel
Folder::create(JPATH_ROOT . '/' . $filePath); Folder::create(JPATH_ROOT . '/' . $filePath);
} }
File::move($backupsPath . '/' . $file, JPATH_ROOT . '/' . $file); File::move(
$backupsPath . '/' . $file,
JPATH_ROOT . '/' . $file
);
} }
} catch (RuntimeException $exception) { } catch (RuntimeException $exception) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_FAILED_REVERT_PATCH', $file, $exception->getMessage())); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_FAILED_REVERT_PATCH',
$file,
$exception->getMessage()
)
);
} }
} }
@ -730,6 +928,7 @@ class PullModel extends AbstractModel
// Change the media version // Change the media version
$version = new Version(); $version = new Version();
$version->refreshMediaVersion(); $version->refreshMediaVersion();
return $this->removeTest($testRecord); return $this->removeTest($testRecord);
} }
@ -744,11 +943,14 @@ class PullModel extends AbstractModel
*/ */
private function getTestRecord(int $id): stdClass private function getTestRecord(int $id): stdClass
{ {
$db = $this->getDb(); $db = $this->getDatabase();
return $db->setQuery($db->getQuery(true)
return $db->setQuery(
$db->getQuery(true)
->select('*') ->select('*')
->from('#__patchtester_tests') ->from('#__patchtester_tests')
->where('id = ' . (int) $id))->loadObject(); ->where('id = ' . (int)$id)
)->loadObject();
} }
/** /**
@ -762,16 +964,21 @@ class PullModel extends AbstractModel
*/ */
private function removeTest(stdClass $testRecord): bool private function removeTest(stdClass $testRecord): bool
{ {
$db = $this->getDb(); $db = $this->getDatabase();
// Remove the retrieved commit SHA from the pulls table for this item // Remove the retrieved commit SHA from the pulls table for this item
$db->setQuery($db->getQuery(true) $db->setQuery(
$db->getQuery(true)
->update('#__patchtester_pulls') ->update('#__patchtester_pulls')
->set('sha = ' . $db->quote('')) ->set('sha = ' . $db->quote(''))
->where($db->quoteName('id') . ' = ' . (int) $testRecord->id))->execute(); ->where($db->quoteName('id') . ' = ' . (int)$testRecord->id)
// And delete the record from the tests table )->execute();
$db->setQuery($db->getQuery(true) // And delete the record from the tests table
$db->setQuery(
$db->getQuery(true)
->delete('#__patchtester_tests') ->delete('#__patchtester_tests')
->where('id = ' . (int) $testRecord->id))->execute(); ->where('id = ' . (int)$testRecord->id)
)->execute();
return true; return true;
} }
@ -782,8 +989,8 @@ class PullModel extends AbstractModel
* *
* @return boolean * @return boolean
* *
* @since 2.0
* @throws RuntimeException * @throws RuntimeException
* @since 2.0
*/ */
public function revertWithGitHub(int $id): bool public function revertWithGitHub(int $id): bool
{ {
@ -795,22 +1002,40 @@ class PullModel extends AbstractModel
$files = json_decode($testRecord->data, false); $files = json_decode($testRecord->data, false);
if (!$files) { if (!$files) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_READING_DATABASE_TABLE', __METHOD__, htmlentities($testRecord->data))); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_READING_DATABASE_TABLE',
__METHOD__,
htmlentities($testRecord->data)
)
);
} }
foreach ($files as $file) { foreach ($files as $file) {
switch ($file->action) { switch ($file->action) {
case 'deleted': case 'deleted':
case 'modified': case 'modified':
$src = JPATH_COMPONENT . '/backups/' . md5($file->filename) . '.txt'; $src = JPATH_COMPONENT . '/backups/' . md5($file->filename)
. '.txt';
$dest = JPATH_ROOT . '/' . $file->filename; $dest = JPATH_ROOT . '/' . $file->filename;
if (!File::copy($src, $dest)) { if (!File::copy($src, $dest)) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_COPY_FILE', $src, $dest)); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_COPY_FILE',
$src,
$dest
)
);
} }
if (file_exists($src)) { if (file_exists($src)) {
if (!File::delete($src)) { if (!File::delete($src)) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', $src)); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE',
$src
)
);
} }
} }
@ -820,28 +1045,51 @@ class PullModel extends AbstractModel
$src = JPATH_ROOT . '/' . $file->filename; $src = JPATH_ROOT . '/' . $file->filename;
if (file_exists($src)) { if (file_exists($src)) {
if (!File::delete($src)) { if (!File::delete($src)) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', $src)); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE',
$src
)
);
} }
} }
break; break;
case 'renamed': case 'renamed':
$originalSrc = JPATH_COMPONENT . '/backups/' . md5($file->originalFile) . '.txt'; $originalSrc = JPATH_COMPONENT . '/backups/' . md5(
$file->originalFile
) . '.txt';
$newSrc = JPATH_ROOT . '/' . $file->filename; $newSrc = JPATH_ROOT . '/' . $file->filename;
$dest = JPATH_ROOT . '/' . $file->originalFile; $dest = JPATH_ROOT . '/' . $file->originalFile;
if (!File::copy($originalSrc, $dest)) { if (!File::copy($originalSrc, $dest)) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_COPY_FILE', $originalSrc, $dest)); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_COPY_FILE',
$originalSrc,
$dest
)
);
} }
if (file_exists($originalSrc)) { if (file_exists($originalSrc)) {
if (!File::delete($originalSrc)) { if (!File::delete($originalSrc)) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', $originalSrc)); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE',
$originalSrc
)
);
} }
} }
if (file_exists($newSrc)) { if (file_exists($newSrc)) {
if (!File::delete($newSrc)) { if (!File::delete($newSrc)) {
throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', $newSrc)); throw new RuntimeException(
Text::sprintf(
'COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE',
$newSrc
)
);
} }
} }
@ -855,6 +1103,7 @@ class PullModel extends AbstractModel
// Change the media version // Change the media version
$version = new Version(); $version = new Version();
$version->refreshMediaVersion(); $version->refreshMediaVersion();
return $this->removeTest($testRecord); return $this->removeTest($testRecord);
} }
} }

View File

@ -14,6 +14,7 @@ use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text; use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Model\ListModel; use Joomla\CMS\MVC\Model\ListModel;
use Joomla\Component\Patchtester\Administrator\Github\Exception\UnexpectedResponse; use Joomla\Component\Patchtester\Administrator\Github\Exception\UnexpectedResponse;
use Joomla\Component\Patchtester\Administrator\GithubCredentialsTrait;
use Joomla\Component\Patchtester\Administrator\Helper\Helper; use Joomla\Component\Patchtester\Administrator\Helper\Helper;
use Joomla\Database\DatabaseQuery; use Joomla\Database\DatabaseQuery;
use RuntimeException; use RuntimeException;
@ -29,6 +30,8 @@ use RuntimeException;
*/ */
class PullsModel extends ListModel class PullsModel extends ListModel
{ {
use GithubCredentialsTrait;
/** /**
* The object context * The object context
* *
@ -43,18 +46,18 @@ class PullsModel extends ListModel
* @since 2.0 * @since 2.0
*/ */
protected $sortFields = ['pulls.pull_id', 'pulls.title']; protected $sortFields = ['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 * @throws Exception
* *
* @since 4.0.0
*/ */
public function __construct($config = []) public function __construct($config = [])
{ {
$config = [];
if (empty($config['filter_fields'])) { if (empty($config['filter_fields'])) {
$config['filter_fields'] = [ $config['filter_fields'] = [
'applied', 'applied',
@ -63,12 +66,27 @@ class PullsModel extends ListModel
'draft', 'draft',
'label', 'label',
'branch', 'branch',
'pulls.title',
'pulls.pull_id',
]; ];
} }
parent::__construct($config); parent::__construct($config);
} }
protected function populateState($ordering = 'pulls.pull_id', $direction = 'DESC')
{
parent::populateState(
$ordering,
$direction
);
foreach ($this->getCredentials() as $name => $value) {
$this->state->set($name, $value);
}
}
/** /**
* Method to get an array of data items. * Method to get an array of data items.
* *
@ -93,13 +111,13 @@ class PullsModel extends ListModel
->select($db->quoteName(['name', 'color'])) ->select($db->quoteName(['name', 'color']))
->from($db->quoteName('#__patchtester_pulls_labels')); ->from($db->quoteName('#__patchtester_pulls_labels'));
array_walk($items, static function ($item) use ($db, $query) { array_walk($items, static function ($item) use ($db, $query) {
$query->clear('where');
$query->clear('where');
$query->where($db->quoteName('pull_id') . ' = ' . $item->pull_id); $query->where($db->quoteName('pull_id') . ' = ' . $item->pull_id);
$db->setQuery($query); $db->setQuery($query);
$item->labels = $db->loadObjectList(); $item->labels = $db->loadObjectList();
}); });
$this->cache[$store] = $items; $this->cache[$store] = $items;
return $this->cache[$store]; return $this->cache[$store];
} }
@ -118,11 +136,11 @@ class PullsModel extends ListModel
*/ */
protected function getStoreId($id = '') protected function getStoreId($id = '')
{ {
// Add the list state to the store id.
$id .= ':' . $this->getState()->get('list.start'); $id .= ':' . $this->getState()->get('list.start');
$id .= ':' . $this->getState()->get('list.limit'); $id .= ':' . $this->getState()->get('list.limit');
$id .= ':' . $this->getState()->get('list.ordering'); $id .= ':' . $this->getState()->get('list.ordering');
$id .= ':' . $this->getState()->get('list.direction'); $id .= ':' . $this->getState()->get('list.direction');
return md5($this->context . ':' . $id); return md5($this->context . ':' . $id);
} }
@ -135,11 +153,14 @@ class PullsModel extends ListModel
* *
* @return array An array of results. * @return array An array of results.
* *
* @since 2.0
* @throws RuntimeException * @throws RuntimeException
* @since 2.0
*/ */
protected function getList($query, int $limitstart = 0, int $limit = 0): array protected function getList(
{ $query,
int $limitstart = 0,
int $limit = 0
): array {
return $this->getDatabase()->setQuery($query, $limitstart, $limit) return $this->getDatabase()->setQuery($query, $limitstart, $limit)
->loadObjectList(); ->loadObjectList();
} }
@ -183,53 +204,68 @@ class PullsModel extends ListModel
$query->select('pulls.*') $query->select('pulls.*')
->select($db->quoteName('tests.id', 'applied')) ->select($db->quoteName('tests.id', 'applied'))
->from($db->quoteName('#__patchtester_pulls', 'pulls')) ->from($db->quoteName('#__patchtester_pulls', 'pulls'))
->leftJoin($db->quoteName('#__patchtester_tests', 'tests') ->leftJoin(
$db->quoteName('#__patchtester_tests', 'tests')
. ' ON ' . $db->quoteName('tests.pull_id') . ' = ' . ' ON ' . $db->quoteName('tests.pull_id') . ' = '
. $db->quoteName('pulls.pull_id')); . $db->quoteName('pulls.pull_id')
);
$search = $this->getState()->get('filter.search'); $search = $this->getState()->get('filter.search');
if (!empty($search)) { if (!empty($search)) {
if (stripos($search, 'id:') === 0) { if (stripos($search, 'id:') === 0) {
$query->where($db->quoteName('pulls.pull_id') . ' = ' . (int) substr( $query->where(
$search, $db->quoteName('pulls.pull_id') . ' = ' . (int)substr(
3 $search,
)); 3
)
);
} elseif (is_numeric($search)) { } elseif (is_numeric($search)) {
$query->where($db->quoteName('pulls.pull_id') . ' = ' . (int) $search); $query->where(
$db->quoteName('pulls.pull_id') . ' = ' . (int)$search
);
} else { } else {
$query->where('(' . $db->quoteName('pulls.title') . ' LIKE ' . $db->quote('%' . $db->escape($search, true) . '%') . ')'); $query->where(
'(' . $db->quoteName('pulls.title') . ' LIKE ' . $db->quote(
'%' . $db->escape($search, true) . '%'
) . ')'
);
} }
} }
$applied = $this->getState()->get('filter.applied'); $applied = $this->getState()->get('filter.applied');
if (!empty($applied)) { if (!empty($applied)) {
// Not applied patches have a NULL value, so build our value part of the query based on this // Not applied patches have a NULL value, so build our value part of the query based on this
$value = $applied === 'no' ? ' IS NULL' : ' = 1'; $value = $applied === 'no' ? ' IS NULL' : ' = 1';
$query->where($db->quoteName('applied') . $value); $query->where($db->quoteName('applied') . $value);
} }
$branch = $this->getState()->get('filter.branch'); $branch = $this->getState()->get('filter.branch');
if (!empty($branch)) { if (!empty($branch)) {
$query->where($db->quoteName('pulls.branch') . ' IN (' . implode(',', $db->quote($branch)) . ')'); $query->where(
$db->quoteName('pulls.branch') . ' IN (' . implode(
',',
$db->quote($branch)
) . ')'
);
} }
$applied = $this->getState()->get('filter.rtc'); $applied = $this->getState()->get('filter.rtc');
if (!empty($applied)) { if (!empty($applied)) {
// Not applied patches have a NULL value, so build our value part of the query based on this // Not applied patches have a NULL value, so build our value part of the query based on this
$value = $applied === 'no' ? '0' : '1'; $value = $applied === 'no' ? '0' : '1';
$query->where($db->quoteName('pulls.is_rtc') . ' = ' . $value); $query->where($db->quoteName('pulls.is_rtc') . ' = ' . $value);
} }
$npm = $this->getState()->get('filter.npm'); $npm = $this->getState()->get('filter.npm');
if (!empty($npm)) { if (!empty($npm)) {
// Not applied patches have a NULL value, so build our value part of the query based on this // Not applied patches have a NULL value, so build our value part of the query based on this
$value = $npm === 'no' ? '0' : '1'; $value = $npm === 'no' ? '0' : '1';
$query->where($db->quoteName('pulls.is_npm') . ' = ' . $value); $query->where($db->quoteName('pulls.is_npm') . ' = ' . $value);
} }
$draft = $this->getState()->get('filter.draft'); $draft = $this->getState()->get('filter.draft');
if (!empty($draft)) { if (!empty($draft)) {
// Not applied patches have a NULL value, so build our value part of the query based on this // Not applied patches have a NULL value, so build our value part of the query based on this
$value = $draft === 'no' ? '0' : '1'; $value = $draft === 'no' ? '0' : '1';
$query->where($db->quoteName('pulls.is_draft') . ' = ' . $value); $query->where($db->quoteName('pulls.is_draft') . ' = ' . $value);
} }
@ -239,27 +275,43 @@ class PullsModel extends ListModel
if (!empty($labels) && $labels[0] !== '') { if (!empty($labels) && $labels[0] !== '') {
$labelQuery $labelQuery
->select($db->quoteName('pulls_labels.pull_id')) ->select($db->quoteName('pulls_labels.pull_id'))
->select('COUNT(' . $db->quoteName('pulls_labels.name') . ') AS ' ->select(
. $db->quoteName('labelCount')) 'COUNT(' . $db->quoteName('pulls_labels.name') . ') AS '
->from($db->quoteName( . $db->quoteName('labelCount')
'#__patchtester_pulls_labels', )
'pulls_labels' ->from(
)) $db->quoteName(
->where($db->quoteName('pulls_labels.name') . ' IN (' . implode( '#__patchtester_pulls_labels',
',', 'pulls_labels'
$db->quote($labels) )
) . ')') )
->where(
$db->quoteName('pulls_labels.name') . ' IN (' . implode(
',',
$db->quote($labels)
) . ')'
)
->group($db->quoteName('pulls_labels.pull_id')); ->group($db->quoteName('pulls_labels.pull_id'));
$query->leftJoin('(' . $labelQuery->__toString() . ') AS ' . $db->quoteName('pulls_labels') $query->leftJoin(
'(' . $labelQuery->__toString() . ') AS ' . $db->quoteName(
'pulls_labels'
)
. ' ON ' . $db->quoteName('pulls_labels.pull_id') . ' = ' . ' ON ' . $db->quoteName('pulls_labels.pull_id') . ' = '
. $db->quoteName('pulls.pull_id')) . $db->quoteName('pulls.pull_id')
->where($db->quoteName('pulls_labels.labelCount') . ' = ' . count($labels)); )
->where(
$db->quoteName('pulls_labels.labelCount') . ' = ' . count(
$labels
)
);
} }
$ordering = $this->getState()->get('list.ordering', 'pulls.pull_id'); $ordering = $this->getState()->get('list.ordering', 'pulls.pull_id');
$direction = $this->getState()->get('list.direction', 'DESC'); $direction = $this->getState()->get('list.direction', 'DESC');
if (!empty($ordering)) { if (!empty($ordering)) {
$query->order($db->escape($ordering) . ' ' . $db->escape($direction)); $query->order(
$db->escape($ordering) . ' ' . $db->escape($direction)
);
} }
return $query; return $query;
@ -296,12 +348,20 @@ class PullsModel extends ListModel
try { try {
// TODO - Option to configure the batch size // TODO - Option to configure the batch size
$batchSize = 100; $batchSize = 100;
$pullsResponse = Helper::initializeGithub()->getOpenPulls($this->getState()->get('github_user'), $this->getState()->get('github_repo'), $page, $batchSize); $pullsResponse = Helper::initializeGithub()->getOpenPulls(
$pulls = json_decode($pullsResponse->body); $this->getState()->get('github_user'),
$this->getState()->get('github_repo'),
$page,
$batchSize
);
$pulls = json_decode($pullsResponse->body);
} catch (UnexpectedResponse $exception) { } catch (UnexpectedResponse $exception) {
throw new RuntimeException( throw new RuntimeException(
Text::sprintf('COM_PATCHTESTER_ERROR_GITHUB_FETCH', $exception->getMessage()), Text::sprintf(
'COM_PATCHTESTER_ERROR_GITHUB_FETCH',
$exception->getMessage()
),
$exception->getCode(), $exception->getCode(),
$exception $exception
); );
@ -332,7 +392,7 @@ class PullsModel extends ListModel
$matches[0] $matches[0]
); );
preg_match('/\d+/', $pageSegment, $pages); preg_match('/\d+/', $pageSegment, $pages);
$lastPage = (int) $pages[0]; $lastPage = (int)$pages[0];
} }
} }
} }
@ -353,30 +413,38 @@ class PullsModel extends ListModel
if (strtolower($label->name) === 'rtc') { if (strtolower($label->name) === 'rtc') {
$isRTC = true; $isRTC = true;
} elseif ( } elseif (
in_array(strtolower($label->name), ['npm resource changed', 'composer dependency changed'], true) in_array(
strtolower($label->name),
['npm resource changed', 'composer dependency changed'],
true
)
) { ) {
$isNPM = true; $isNPM = true;
} }
$labels[] = implode(',', [ $labels[] = implode(',', [
(int) $pull->number, (int)$pull->number,
$this->getDatabase()->quote($label->name), $this->getDatabase()->quote($label->name),
$this->getDatabase()->quote($label->color), $this->getDatabase()->quote($label->color),
]); ]);
} }
// Build the data object to store in the database // Build the data object to store in the database
$pullData = [ $pullData = [
(int) $pull->number, (int)$pull->number,
$this->getDatabase()->quote(HTMLHelper::_('string.truncate', ($pull->title ?? ''), 150)), $this->getDatabase()->quote(
$this->getDatabase()->quote(HTMLHelper::_('string.truncate', ($pull->body ?? ''), 100)), HTMLHelper::_('string.truncate', ($pull->title ?? ''), 150)
),
$this->getDatabase()->quote(
HTMLHelper::_('string.truncate', ($pull->body ?? ''), 100)
),
$this->getDatabase()->quote($pull->html_url), $this->getDatabase()->quote($pull->html_url),
(int) $isRTC, (int)$isRTC,
(int) $isNPM, (int)$isNPM,
$this->getDatabase()->quote($branch), $this->getDatabase()->quote($branch),
($pull->draft ? 1 : 0) ($pull->draft ? 1 : 0)
]; ];
$data[] = implode(',', $pullData); $data[] = implode(',', $pullData);
} }
// If there are no pulls to insert then bail, assume we're finished // If there are no pulls to insert then bail, assume we're finished
@ -385,15 +453,28 @@ class PullsModel extends ListModel
} }
try { try {
$this->getDatabase()->setQuery($this->getDatabase()->getQuery(true) $this->getDatabase()->setQuery(
$this->getDatabase()->getQuery(true)
->insert('#__patchtester_pulls') ->insert('#__patchtester_pulls')
->columns(['pull_id', 'title', 'description', 'pull_url', ->columns([
'is_rtc', 'is_npm', 'branch', 'is_draft']) 'pull_id',
->values($data)); 'title',
'description',
'pull_url',
'is_rtc',
'is_npm',
'branch',
'is_draft'
])
->values($data)
);
$this->getDatabase()->execute(); $this->getDatabase()->execute();
} catch (RuntimeException $exception) { } catch (RuntimeException $exception) {
throw new RuntimeException( throw new RuntimeException(
Text::sprintf('COM_PATCHTESTER_ERROR_INSERT_DATABASE', $exception->getMessage()), Text::sprintf(
'COM_PATCHTESTER_ERROR_INSERT_DATABASE',
$exception->getMessage()
),
$exception->getCode(), $exception->getCode(),
$exception $exception
); );
@ -401,10 +482,12 @@ class PullsModel extends ListModel
if ($labels) { if ($labels) {
try { try {
$this->getDatabase()->setQuery($this->getDatabase()->getQuery(true) $this->getDatabase()->setQuery(
$this->getDatabase()->getQuery(true)
->insert('#__patchtester_pulls_labels') ->insert('#__patchtester_pulls_labels')
->columns(['pull_id', 'name', 'color']) ->columns(['pull_id', 'name', 'color'])
->values($labels)); ->values($labels)
);
$this->getDatabase()->execute(); $this->getDatabase()->execute();
} catch (RuntimeException $exception) { } catch (RuntimeException $exception) {
throw new RuntimeException( throw new RuntimeException(

View File

@ -11,6 +11,8 @@ namespace Joomla\Component\Patchtester\Administrator\Model;
// phpcs:disable PSR1.Files.SideEffects // phpcs:disable PSR1.Files.SideEffects
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
\defined('_JEXEC') or die; \defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects // phpcs:enable PSR1.Files.SideEffects
@ -19,7 +21,7 @@ namespace Joomla\Component\Patchtester\Administrator\Model;
* *
* @since 2.0 * @since 2.0
*/ */
class TestsModel extends AbstractModel class TestsModel extends BaseDatabaseModel
{ {
/** /**
* Retrieves a list of applied patches * Retrieves a list of applied patches
@ -30,7 +32,7 @@ class TestsModel extends AbstractModel
*/ */
public function getAppliedPatches(): array public function getAppliedPatches(): array
{ {
$db = $this->getDb(); $db = $this->getDatabase();
$db->setQuery($db->getQuery(true) $db->setQuery($db->getQuery(true)
->select('*') ->select('*')
->from($db->quoteName('#__patchtester_tests')) ->from($db->quoteName('#__patchtester_tests'))
@ -47,6 +49,6 @@ class TestsModel extends AbstractModel
*/ */
public function truncateTable(): void public function truncateTable(): void
{ {
$this->getDb()->truncateTable('#__patchtester_tests'); $this->getDatabase()->truncateTable('#__patchtester_tests');
} }
} }

View File

@ -0,0 +1,25 @@
<?php
/**
* Patch testing component for the Joomla! CMS
*
* @copyright Copyright (C) 2011 - 2012 Ian MacLennan, Copyright (C) 2013 - 2018 Open Source Matters, Inc. All rights reserved.
* @license GNU General Public License version 2 or later
*/
namespace Joomla\Component\Patchtester\Administrator\View\Fetch;
use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/**
* View class for a list of pull requests.
*
* @since 2.0
*/
class HtmlView extends BaseHtmlView
{
}

View File

@ -132,9 +132,8 @@ class HtmlView extends BaseHtmlView
ToolbarHelper::title(Text::_('COM_PATCHTESTER'), 'patchtester fas fa-save'); ToolbarHelper::title(Text::_('COM_PATCHTESTER'), 'patchtester fas fa-save');
if (!count($this->envErrors)) { if (!count($this->envErrors)) {
$toolbar = Toolbar::getInstance('toolbar'); $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')); $toolbar->appendButton('Popup', 'sync', 'COM_PATCHTESTER_TOOLBAR_FETCH_DATA', 'index.php?option=com_patchtester&view=fetch&task=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.reset', false);
$toolbar->appendButton('Standard', 'expired', 'COM_PATCHTESTER_TOOLBAR_RESET', 'reset', false);
} }
ToolbarHelper::preferences('com_patchtester'); ToolbarHelper::preferences('com_patchtester');

View File

@ -15,8 +15,6 @@ use Joomla\CMS\Language\Text;
\defined('_JEXEC') or die; \defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects // phpcs:enable PSR1.Files.SideEffects
/** @var \Joomla\Component\Patchtester\Administrator\View\DefaultHtmlView $this */
HTMLHelper::_('jquery.framework'); HTMLHelper::_('jquery.framework');
HTMLHelper::_('behavior.core'); HTMLHelper::_('behavior.core');
HTMLHelper::_('script', 'com_patchtester/fetcher.js', ['version' => 'auto', 'relative' => true]); HTMLHelper::_('script', 'com_patchtester/fetcher.js', ['version' => 'auto', 'relative' => true]);
@ -30,5 +28,5 @@ Text::script('COM_PATCHTESTER_FETCH_AN_ERROR_HAS_OCCURRED');
<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::getApplication()->getSession()->getFormToken(); ?>" value="1" />
</div> </div>

View File

@ -40,7 +40,7 @@ if (typeof Joomla === 'undefined') {
jQuery.ajax({ jQuery.ajax({
type: 'GET', type: 'GET',
url: path, url: path,
data: 'task=' + task, data: `task=${task}.${task}`,
dataType: 'json', dataType: 'json',
success: function (response, textStatus, xhr) { success: function (response, textStatus, xhr) {
try { try {

View File

@ -10,7 +10,7 @@ if (typeof Joomla === 'undefined') {
} }
document.addEventListener("DOMContentLoaded", function (event) { document.addEventListener("DOMContentLoaded", function (event) {
var submitPatch = document.querySelectorAll(".submitPatch"); const submitPatch = document.querySelectorAll(".submitPatch");
/** /**
* EventListener which listens on submitPatch Button, * EventListener which listens on submitPatch Button,
@ -21,9 +21,9 @@ document.addEventListener("DOMContentLoaded", function (event) {
*/ */
submitPatch.forEach(function (element) { submitPatch.forEach(function (element) {
element.addEventListener("click", function (event) { element.addEventListener("click", function (event) {
var currentTarget = event.currentTarget; const currentTarget = event.currentTarget;
var task = currentTarget.dataset.task const task = `${currentTarget.dataset.task}.${currentTarget.dataset.task}`
var id = currentTarget.dataset.id const id = parseInt(currentTarget.dataset.id)
PatchTester.submitpatch(task, id); PatchTester.submitpatch(task, id);
}); });