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"?>
<form addfieldprefix="PatchTester\Field">
<form addfieldprefix="Joomla\Component\Patchtester\Administrator\Field">
<fields name="filter">
<field
name="search"
@ -87,10 +87,10 @@
type="list"
onchange="this.form.submit();"
>
<option value="a.title ASC">JGLOBAL_TITLE_ASC</option>
<option value="a.title DESC">JGLOBAL_TITLE_DESC</option>
<option value="a.pull_id ASC">COM_PATCHTESTER_PULL_ID_ASC</option>
<option value="a.pull_id DESC">COM_PATCHTESTER_PULL_ID_DESC</option>
<option value="pulls.title ASC">JGLOBAL_TITLE_ASC</option>
<option value="pulls.title DESC">JGLOBAL_TITLE_DESC</option>
<option value="pulls.pull_id ASC">COM_PATCHTESTER_PULL_ID_ASC</option>
<option value="pulls.pull_id DESC">COM_PATCHTESTER_PULL_ID_DESC</option>
</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\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;
use Joomla\Component\Patchtester\Administrator\Model\PullModel;
@ -24,7 +25,7 @@ use Joomla\Component\Patchtester\Administrator\Model\PullModel;
*
* @since 2.0
*/
class ApplyController extends AbstractController
class ApplyController extends BaseController
{
/**
* Execute the controller.
@ -33,16 +34,15 @@ class ApplyController extends AbstractController
*
* @since 2.0
*/
public function execute()
public function execute($task): void
{
try {
$model = new PullModel(null, Factory::getDbo());
// Initialize the state for the model
$model->setState($this->initializeState($model));
if ($model->apply($this->getInput()->getUint('pull_id'))) {
/** @var PullModel $model */
$model = Factory::getApplication()->bootComponent('com_patchtester')->getMVCFactory()->createModel('Pull', 'Administrator', ['ignore_request' => true]);
$msg = Text::_('COM_PATCHTESTER_NO_FILES_TO_PATCH');
if ($model->apply($this->input->getUint('pull_id'))) {
$msg = Text::_('COM_PATCHTESTER_APPLY_OK');
} else {
$msg = Text::_('COM_PATCHTESTER_NO_FILES_TO_PATCH');
}
$type = 'message';
@ -51,7 +51,7 @@ class ApplyController extends AbstractController
$type = 'error';
}
$this->getApplication()->enqueueMessage($msg, $type);
$this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false));
$this->app->enqueueMessage($msg, $type);
$this->app->redirect(Route::_('index.php?option=com_patchtester', false));
}
}

View File

@ -9,10 +9,7 @@
namespace Joomla\Component\Patchtester\Administrator\Controller;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\Registry\Registry;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
@ -33,111 +30,4 @@ class DisplayController extends BaseController
* @since 1.6
*/
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;
use Exception;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Response\JsonResponse;
use Joomla\Component\Patchtester\Administrator\Model\PullsModel;
@ -23,48 +25,42 @@ use Joomla\Component\Patchtester\Administrator\Model\PullsModel;
*
* @since 2.0
*/
class FetchController extends AbstractController
class FetchController extends BaseController
{
/**
* Execute the controller.
*
* @return void Redirects the application
*
* @throws Exception
* @since 2.0
*/
public function execute()
public function execute($task)
{
// We don't want this request to be cached.
$this->getApplication()->setHeader('Expires', 'Mon, 1 Jan 2001 00:00:00 GMT', true);
$this->getApplication()->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true);
$this->getApplication()->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false);
$this->getApplication()->setHeader('Pragma', 'no-cache');
$this->getApplication()->setHeader('Content-Type', $this->getApplication()->mimeType . '; charset=' . $this->getApplication()->charSet);
$session = Factory::getSession();
$this->app->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->app->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false);
$this->app->setHeader('Pragma', 'no-cache');
$this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet);
$session = Factory::getApplication()->getSession();
try {
// Fetch our page from the session
$page = $session->get('com_patchtester_fetcher_page', 1);
$model = new PullsModel();
// Initialize the state for the model
$state = $this->initializeState($model);
foreach ($state as $key => $value) {
$model->setState($key, $value);
}
/** @var PullsModel $model */
$model = $this->app->bootComponent('com_patchtester')->getMVCFactory()->createModel('Pulls', 'Administrator', ['ignore_request' => true]);
$status = $model->requestFromGithub($page);
} catch (\Exception $e) {
} catch (Exception $e) {
$response = new JsonResponse($e);
$this->getApplication()->sendHeaders();
$this->app->sendHeaders();
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) {
$session->set('com_patchtester_fetcher_last_page', $status['lastPage']);
}
// Update the UI and session now
if ($status['complete'] || $page === $session->get('com_patchtester_fetcher_last_page', false)) {
$status['complete'] = true;
$status['header'] = Text::_('COM_PATCHTESTER_FETCH_SUCCESSFUL', true);
@ -83,8 +79,8 @@ class FetchController extends AbstractController
}
$response = new JsonResponse($status, $message, false, true);
$this->getApplication()->sendHeaders();
$this->app->sendHeaders();
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\Filesystem\Folder;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;
use Joomla\Component\Patchtester\Administrator\Model\PullModel;
use Joomla\Component\Patchtester\Administrator\Model\PullsModel;
@ -28,7 +29,7 @@ use Joomla\Filesystem\File;
*
* @since 2.0
*/
class ResetController extends AbstractController
class ResetController extends BaseController
{
/**
* Execute the controller.
@ -37,20 +38,25 @@ class ResetController extends AbstractController
*
* @since 2.0
*/
public function execute(): void
public function execute($task): void
{
try {
$hasErrors = false;
$revertErrored = false;
$pullModel = new PullModel(null, Factory::getDbo());
$pullsModel = new PullsModel($this->context, null, Factory::getDbo());
$testsModel = new TestsModel(null, Factory::getDbo());
// Check the applied patches in the database first
$mvcFactory = Factory::getApplication()->bootComponent('com_patchtester')->getMVCFactory();
/** @var PullModel $pullModel */
$pullModel = $mvcFactory->createModel('Pull', 'Administrator', ['ignore_request' => true]);
/** @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();
$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)) {
// 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) {
try {
$pullModel->revertWithCIServer($patch->id);
@ -76,7 +82,7 @@ class ResetController extends AbstractController
} catch (\RuntimeException $e) {
$hasErrors = true;
$this->getApplication()->enqueueMessage(
$this->app->enqueueMessage(
Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_PULLS_TABLE', $e->getMessage()),
'error'
);
@ -89,7 +95,7 @@ class ResetController extends AbstractController
} catch (\RuntimeException $e) {
$hasErrors = true;
$this->getApplication()->enqueueMessage(
$this->app->enqueueMessage(
Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_TESTS_TABLE', $e->getMessage()),
'error'
);
@ -101,7 +107,7 @@ class ResetController extends AbstractController
if (count($backups)) {
foreach ($backups as $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),
'error'
);
@ -115,7 +121,7 @@ class ResetController extends AbstractController
$msg = Text::sprintf(
'COM_PATCHTESTER_RESET_HAS_ERRORS',
JPATH_COMPONENT . '/backups',
Factory::getDbo()->replacePrefix('#__patchtester_tests')
Factory::getApplication()->get('DatabaseDriver')->replacePrefix('#__patchtester_tests')
);
$type = 'warning';
} else {
@ -127,7 +133,7 @@ class ResetController extends AbstractController
$type = 'error';
}
$this->getApplication()->enqueueMessage($msg, $type);
$this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false));
$this->app->enqueueMessage($msg, $type);
$this->app->redirect(Route::_('index.php?option=com_patchtester', false));
}
}

View File

@ -9,8 +9,8 @@
namespace Joomla\Component\Patchtester\Administrator\Controller;
use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;
use Joomla\Component\Patchtester\Administrator\Model\PullModel;
@ -23,7 +23,7 @@ use Joomla\Component\Patchtester\Administrator\Model\PullModel;
*
* @since 2.0
*/
class RevertController extends AbstractController
class RevertController extends BaseController
{
/**
* Execute the controller.
@ -32,13 +32,12 @@ class RevertController extends AbstractController
*
* @since 2.0
*/
public function execute()
public function execute($task)
{
try {
$model = new PullModel(null, Factory::getDbo());
// Initialize the state for the model
$model->setState($this->initializeState($model));
$model->revert($this->getInput()->getUint('pull_id'));
/** @var PullModel $model */
$model = $this->app->bootComponent('com_patchtester')->getMVCFactory()->createModel('Pull', 'Administrator', ['ignore_request' => true]);
$model->revert($this->input->getUint('pull_id'));
$msg = Text::_('COM_PATCHTESTER_REVERT_OK');
$type = 'message';
} catch (\Exception $e) {
@ -46,7 +45,7 @@ class RevertController extends AbstractController
$type = 'error';
}
$this->getApplication()->enqueueMessage($msg, $type);
$this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false));
$this->app->enqueueMessage($msg, $type);
$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\Language\Text;
use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Response\JsonResponse;
use Joomla\CMS\Session\Session;
use Joomla\Component\Patchtester\Administrator\Helper\Helper;
use Joomla\Component\Patchtester\Administrator\Model\TestsModel;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
@ -25,7 +25,7 @@ use Joomla\Component\Patchtester\Administrator\Model\TestsModel;
*
* @since 2.0
*/
class StartfetchController extends AbstractController
class StartfetchController extends BaseController
{
/**
* Execute the controller.
@ -34,20 +34,19 @@ class StartfetchController extends AbstractController
*
* @since 2.0
*/
public function execute()
public function execute($task): void
{
// We don't want this request to be cached.
$this->getApplication()->setHeader('Expires', 'Mon, 1 Jan 2001 00:00:00 GMT', true);
$this->getApplication()->setHeader('Last-Modified', gmdate('D, d M Y H:i:s') . ' GMT', true);
$this->getApplication()->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false);
$this->getApplication()->setHeader('Pragma', 'no-cache');
$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.
$this->app->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->app->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0', false);
$this->app->setHeader('Pragma', 'no-cache');
$this->app->setHeader('Content-Type', $this->app->mimeType . '; charset=' . $this->app->charSet);
if (!Session::checkToken('request')) {
$response = new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403));
$this->getApplication()->sendHeaders();
$this->app->sendHeaders();
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
@ -56,40 +55,48 @@ class StartfetchController extends AbstractController
$rate = json_decode($rateResponse->body);
} catch (\Exception $e) {
$response = new JsonResponse(new \Exception(Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e));
$this->getApplication()->sendHeaders();
$this->app->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
$this->app->close(1);
}
// If over the API limit, we can't build this list
if ($rate->resources->core->remaining < 10) {
$response = new JsonResponse(new \Exception(Text::sprintf('COM_PATCHTESTER_API_LIMIT_LIST', Factory::getDate($rate->resources->core->reset)), 429));
$this->getApplication()->sendHeaders();
$this->app->sendHeaders();
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 {
// Sanity check, ensure there aren't any applied patches
if (count($testsModel->getAppliedPatches()) >= 1) {
$response = new JsonResponse(new \Exception(Text::_('COM_PATCHTESTER_ERROR_APPLIED_PATCHES'), 500));
$this->getApplication()->sendHeaders();
$this->app->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
$this->app->close(1);
}
} catch (\Exception $e) {
$response = new JsonResponse($e);
$this->getApplication()->sendHeaders();
$this->app->sendHeaders();
echo json_encode($response);
$this->getApplication()->close(1);
$this->app->close(1);
}
// We're able to successfully pull data, prepare our environment
Factory::getSession()->set('com_patchtester_fetcher_page', 1);
$response = new JsonResponse(array('complete' => false, 'header' => Text::_('COM_PATCHTESTER_FETCH_PROCESSING', true)), Text::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', 1), false, true);
$this->getApplication()->sendHeaders();
Factory::getApplication()->getSession()->set('com_patchtester_fetcher_page', 1);
$response = new JsonResponse(
[
'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);
$this->getApplication()->close();
$this->app->close();
}
}

View File

@ -7,16 +7,18 @@
* @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
*
* @since 3.0.0
*/
class UnexpectedResponse extends \DomainException
class UnexpectedResponse extends DomainException
{
/**
* The Response object.
@ -25,18 +27,23 @@ class UnexpectedResponse extends \DomainException
* @since 3.0.0
*/
private $response;
/**
/**
* Constructor
*
* @param Response $response The Response object.
* @param string $message The Exception message to throw.
* @param integer $code The Exception code.
* @param \Exception $previous The previous exception used for the exception chaining.
* @param Response $response The Response object.
* @param string $message The Exception message to throw.
* @param int $code The Exception code.
* @param Exception|null $previous The previous exception used for the exception chaining.
*
* @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);
$this->response = $response;
}
@ -48,7 +55,7 @@ class UnexpectedResponse extends \DomainException
*
* @since 3.0.0
*/
public function getResponse()
public function getResponse(): Response
{
return $this->response;
}

View File

@ -31,7 +31,8 @@ class BranchField extends ListField
* @since 4.1.0
*/
protected $type = 'Branch';
/**
/**
* Build a list of available branches.
*
* @return array List of options
@ -40,14 +41,19 @@ class BranchField extends ListField
*/
public function getOptions(): array
{
$db = Factory::getContainer()->get('DatabaseDriver');
$db = Factory::getContainer()->get('DatabaseDriver');
$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'))
->from('#__patchtester_pulls')
->where($db->quoteName('branch') . ' != ' . $db->quote(''))
->order($db->quoteName('branch') . ' ASC');
$options = $db->setQuery($query)->loadAssocList();
return array_merge(parent::getOptions(), $options);
}
}

View File

@ -31,7 +31,8 @@ class LabelField extends ListField
* @since 4.1.0
*/
protected $type = 'Label';
/**
/**
* Build a list of available fields.
*
* @return array List of options
@ -40,13 +41,18 @@ class LabelField extends ListField
*/
public function getOptions(): array
{
$db = Factory::getContainer()->get('DatabaseDriver');
$db = Factory::getContainer()->get('DatabaseDriver');
$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'))
->from($db->quoteName('#__patchtester_pulls_labels'))
->order($db->quoteName('name') . ' ASC');
$options = $db->setQuery($query)->loadAssocList();
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\HttpFactory;
use Joomla\CMS\Http\Response;
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;
/**
@ -30,18 +30,19 @@ class GitHub
* @since 3.0.0
*/
protected $options;
/**
/**
* The HTTP client object to use in sending HTTP requests.
*
* @var Http
* @since 3.0.0
*/
protected $client;
/**
/**
* Constructor.
*
* @param Registry $options Connector options.
* @param Http $client The HTTP client object.
* @param Registry|null $options Connector options.
* @param Http|null $client The HTTP client object.
*
* @since 3.0.0
*/
@ -58,7 +59,7 @@ class GitHub
*
* @since 3.0.0
*/
public function getClient()
public function getClient(): Http
{
return $this->client;
}
@ -66,22 +67,27 @@ class GitHub
/**
* Get the diff for a pull request.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number.
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param int $pullId The pull request number.
*
* @return Response
*
* @since 3.0.0
*/
public function getDiffForPullRequest($user, $repo, $pullId)
{
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId;
// Build the request headers.
$headers = array('Accept' => 'application/vnd.github.diff');
public function getDiffForPullRequest(
string $user,
string $repo,
int $pullId
): Response {
$path = "/repos/$user/$repo/pulls/" . $pullId;
$headers = ['Accept' => 'application/vnd.github.diff'];
$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.
*
* @param string $path Path to process
* @param integer $page Page to request
* @param integer $limit Number of results to return per page
* @param array $headers The headers to send with the request
* @param string $path Path to process
* @param int $page Page to request
* @param int $limit Number of results to return per page
* @param array $headers The headers to send with the request
*
* @return array Associative array containing the prepared URL and request headers
*
* @since 3.0.0
*/
protected function prepareRequest(
$path,
$page = 0,
$limit = 0,
array $headers = array()
) {
string $path,
int $page = 0,
int $limit = 0,
array $headers = []
): array {
$url = $this->fetchUrl($path, $page, $limit);
if ($token = $this->options->get('gh.token', false)) {
$headers['Authorization'] = "token $token";
}
return array('url' => $url, 'headers' => $headers);
return ['url' => $url, 'headers' => $headers];
}
/**
@ -118,9 +125,9 @@ class GitHub
* This method will add appropriate pagination details and basic authentication credentials if necessary
* and also prepend the API url to have a complete URL for the request.
*
* @param string $path URL to inflect
* @param integer $page Page to request
* @param integer $limit Number of results to return per page
* @param string $path URL to inflect
* @param int $page Page to request
* @param int $limit Number of results to return per page
*
* @return string The request URL.
*
@ -130,38 +137,39 @@ class GitHub
{
// Get a new Uri object using the API URL and given path.
$uri = new Uri($this->options->get('api.url') . $path);
// If we have a defined page number add it to the JUri object.
// If we have a defined page number add it to the JUri object.
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 ($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.
*
* @param Response $response The response.
* @param integer $expectedCode The expected response code.
* @param int $expectedCode The expected response code.
*
* @return Response
*
* @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.
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);
$error = isset($body->error) ? $body->error
: (isset($body->message) ? $body->message : 'Unknown Error');
$error = $body->error ?? ($body->message ?? 'Unknown Error');
throw new UnexpectedResponse(
$response,
@ -187,15 +195,17 @@ class GitHub
*/
public function getFileContents($user, $repo, $path, $ref = null)
{
$path = "/repos/$user/$repo/contents/$path";
$path = "/repos/$user/$repo/contents/$path";
$prepared = $this->prepareRequest($path);
if ($ref) {
$url = new Uri($prepared['url']);
$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)
{
// 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);
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.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $page The page number from which to get items.
* @param integer $limit The number of items on a page.
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param int $page The page number from which to get items.
* @param int $limit The number of items on a page.
*
* @return Response
*
@ -236,16 +250,19 @@ class GitHub
$page,
$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.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $page The page number from which to get items.
* @param integer $limit The number of items on a page.
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param int $page The page number from which to get items.
* @param int $limit The number of items on a page.
*
* @return Response
*
@ -258,7 +275,10 @@ class GitHub
$page,
$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.
*
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param integer $pullId The pull request number.
* @param string $user The name of the owner of the GitHub repository.
* @param string $repo The name of the GitHub repository.
* @param int $pullId The pull request number.
*
* @return Response
*
@ -290,9 +310,12 @@ class GitHub
public function getPullRequest($user, $repo, $pullId)
{
// Build the request path.
$path = "/repos/$user/$repo/pulls/" . (int) $pullId;
$path = "/repos/$user/$repo/pulls/" . (int)$pullId;
$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()
{
$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)
{
$this->options->set($key, $value);
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\Factory;
use Joomla\CMS\Language\Text;
use Joomla\Component\Patchtester\Administrator\GitHub\GitHub;
use Joomla\Registry\Registry;
use src\GitHub\GitHub;
// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;

View File

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

View File

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

View File

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

View File

@ -11,6 +11,8 @@ namespace Joomla\Component\Patchtester\Administrator\Model;
// phpcs:disable PSR1.Files.SideEffects
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
@ -19,7 +21,7 @@ namespace Joomla\Component\Patchtester\Administrator\Model;
*
* @since 2.0
*/
class TestsModel extends AbstractModel
class TestsModel extends BaseDatabaseModel
{
/**
* Retrieves a list of applied patches
@ -30,7 +32,7 @@ class TestsModel extends AbstractModel
*/
public function getAppliedPatches(): array
{
$db = $this->getDb();
$db = $this->getDatabase();
$db->setQuery($db->getQuery(true)
->select('*')
->from($db->quoteName('#__patchtester_tests'))
@ -47,6 +49,6 @@ class TestsModel extends AbstractModel
*/
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');
if (!count($this->envErrors)) {
$toolbar = Toolbar::getInstance('toolbar');
$toolbar->appendButton('Popup', 'sync', 'COM_PATCHTESTER_TOOLBAR_FETCH_DATA', 'index.php?option=com_patchtester&view=fetch&tmpl=component', 500, 210, 0, 0, 'window.parent.location.reload()', Text::_('COM_PATCHTESTER_HEADING_FETCH_DATA'));
// Add a reset button.
$toolbar->appendButton('Standard', 'expired', 'COM_PATCHTESTER_TOOLBAR_RESET', 'reset', false);
$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'));
$toolbar->appendButton('Standard', 'expired', 'COM_PATCHTESTER_TOOLBAR_RESET', 'reset.reset', false);
}
ToolbarHelper::preferences('com_patchtester');

View File

@ -15,8 +15,6 @@ use Joomla\CMS\Language\Text;
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects
/** @var \Joomla\Component\Patchtester\Administrator\View\DefaultHtmlView $this */
HTMLHelper::_('jquery.framework');
HTMLHelper::_('behavior.core');
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-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>
<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>

View File

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

View File

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