From 8234d9a14a0ed744747cd1cdde7352e4cc547ff2 Mon Sep 17 00:00:00 2001 From: Hannes Papenberg Date: Thu, 8 Sep 2022 10:54:04 +0200 Subject: [PATCH] Switching to PSR-12 and updating composer dependencies --- .drone.yml | 15 +- .editorconfig | 20 + .../Controller/AbstractController.php | 156 +- .../Controller/ApplyController.php | 66 +- .../Controller/DisplayController.php | 232 +- .../Controller/FetchController.php | 137 +- .../Controller/ResetController.php | 216 +- .../Controller/RevertController.php | 57 +- .../Controller/StartfetchController.php | 167 +- .../PatchTester/Field/BranchField.php | 61 +- .../PatchTester/Field/LabelField.php | 59 +- .../GitHub/Exception/UnexpectedResponse.php | 69 +- .../PatchTester/GitHub/GitHub.php | 591 +++-- .../com_patchtester/PatchTester/Helper.php | 117 +- .../PatchTester/Model/AbstractModel.php | 153 +- .../PatchTester/Model/FetchModel.php | 5 + .../PatchTester/Model/PullModel.php | 1898 +++++++---------- .../PatchTester/Model/PullsModel.php | 933 ++++---- .../PatchTester/Model/TestsModel.php | 63 +- .../PatchTester/TrackerHelper.php | 95 +- .../PatchTester/View/AbstractHtmlView.php | 361 ++-- .../PatchTester/View/AbstractView.php | 64 +- .../PatchTester/View/DefaultHtmlView.php | 5 + .../PatchTester/View/Fetch/tmpl/default.php | 17 +- .../PatchTester/View/Pulls/PullsHtmlView.php | 234 +- .../PatchTester/View/Pulls/tmpl/default.php | 111 +- .../View/Pulls/tmpl/default_items.php | 230 +- .../PatchTester/View/Pulls/tmpl/errors.php | 13 +- .../com_patchtester/patchtester.php | 24 +- .../components/com_patchtester/script.php | 189 +- build/patchtester/hash_generator.php | 17 +- build/patchtester/release.php | 88 +- composer.json | 2 - composer.lock | 980 ++++++--- ruleset.xml | 32 + 35 files changed, 3717 insertions(+), 3760 deletions(-) create mode 100644 .editorconfig create mode 100644 ruleset.xml diff --git a/.drone.yml b/.drone.yml index a9eaae1..feb5428 100644 --- a/.drone.yml +++ b/.drone.yml @@ -3,11 +3,13 @@ kind: pipeline name: default clone: - depth: 42 steps: - name: composer - image: joomlaprojects/docker-tools:develop + image: joomlaprojects/docker-images:php7.4 + volumes: + - name: composer-cache + path: /tmp/composer-cache commands: - composer validate --no-check-all --strict - composer install --no-progress --no-suggest @@ -16,10 +18,15 @@ steps: image: joomlaprojects/docker-images:php7.2 commands: - echo $(date) - - ./administrator/components/com_patchtester/vendor/bin/phpcs --extensions=php -p --ignore=administrator/components/com_patchtester/vendor --standard=administrator/components/com_patchtester/vendor/joomla/cms-coding-standards/lib/Joomla-CMS administrator + - ./administrator/components/com_patchtester/vendor/bin/phpcs --extensions=php -p --ignore=administrator/components/com_patchtester/vendor --ignore=build/packaging --standard=PSR12 . - echo $(date) + +volumes: + - name: composer-cache + host: + path: /tmp/composer-cache --- kind: signature -hmac: c5899584898d37d46fb70cb22487532d41719c7a836be7f67ad4ac3c2267dafa +hmac: 66f585c17b678699c15f48673fa52585c02eddab99c4dcfa2f1e1e85960a2a31 ... diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..a7b8f5f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,20 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style end of lines and a blank line at the end of the file +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.php] +indent_style = space +indent_size = 4 + +[*.{js,json,scss,css,vue}] +indent_style = space +indent_size = 2 diff --git a/administrator/components/com_patchtester/PatchTester/Controller/AbstractController.php b/administrator/components/com_patchtester/PatchTester/Controller/AbstractController.php index aa3514d..e5aa161 100644 --- a/administrator/components/com_patchtester/PatchTester/Controller/AbstractController.php +++ b/administrator/components/com_patchtester/PatchTester/Controller/AbstractController.php @@ -1,4 +1,5 @@ app = $app; +// Set the context for the controller + $this->context = 'com_patchtester.' . $this->getInput()->getCmd('view', $this->defaultView); + } - /** - * The object context - * - * @var string - * @since 2.0 - */ - protected $context; + /** + * Get the application object. + * + * @return CMSApplication + * + * @since 4.0.0 + */ + public function getApplication() + { + return $this->app; + } - /** - * The default view to display - * - * @var string - * @since 2.0 - */ - protected $defaultView = 'pulls'; + /** + * Get the input object. + * + * @return Input + * + * @since 4.0.0 + */ + public function getInput() + { + return $this->app->input; + } - /** - * Instantiate the controller - * - * @param CMSApplication $app The application object. - * - * @since 2.0 - */ - public function __construct(CMSApplication $app) - { - $this->app = $app; - - // Set the context for the controller - $this->context = 'com_patchtester.' . $this->getInput()->getCmd('view', $this->defaultView); - } - - /** - * 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; - } + /** + * 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; + } } diff --git a/administrator/components/com_patchtester/PatchTester/Controller/ApplyController.php b/administrator/components/com_patchtester/PatchTester/Controller/ApplyController.php index 67a5b7a..7a5c2a1 100644 --- a/administrator/components/com_patchtester/PatchTester/Controller/ApplyController.php +++ b/administrator/components/com_patchtester/PatchTester/Controller/ApplyController.php @@ -1,4 +1,5 @@ setState($this->initializeState($model)); + if ($model->apply($this->getInput()->getUint('pull_id'))) { + $msg = Text::_('COM_PATCHTESTER_APPLY_OK'); + } else { + $msg = Text::_('COM_PATCHTESTER_NO_FILES_TO_PATCH'); + } - // Initialize the state for the model - $model->setState($this->initializeState($model)); + $type = 'message'; + } catch (\Exception $e) { + $msg = $e->getMessage(); + $type = 'error'; + } - if ($model->apply($this->getInput()->getUint('pull_id'))) - { - $msg = Text::_('COM_PATCHTESTER_APPLY_OK'); - } - else - { - $msg = Text::_('COM_PATCHTESTER_NO_FILES_TO_PATCH'); - } - - $type = 'message'; - } - catch (\Exception $e) - { - $msg = $e->getMessage(); - $type = 'error'; - } - - $this->getApplication()->enqueueMessage($msg, $type); - $this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false)); - } + $this->getApplication()->enqueueMessage($msg, $type); + $this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false)); + } } diff --git a/administrator/components/com_patchtester/PatchTester/Controller/DisplayController.php b/administrator/components/com_patchtester/PatchTester/Controller/DisplayController.php index 6a036d9..d0cd901 100644 --- a/administrator/components/com_patchtester/PatchTester/Controller/DisplayController.php +++ b/administrator/components/com_patchtester/PatchTester/Controller/DisplayController.php @@ -1,4 +1,5 @@ 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); + } + } - /** - * Execute the controller. - * - * @return boolean True on success - * - * @since 2.0 - * @throws \RuntimeException - */ - public function execute() - { - // Set up variables to build our classes - $view = $this->getInput()->getCmd('view', $this->defaultView); - $format = $this->getInput()->getCmd('format', 'html'); + if (!class_exists($modelClass)) { + throw new \RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_MODEL_NOT_FOUND', $modelClass), 500); + } - // Register the layout paths for the view - $paths = new \SplPriorityQueue; + // 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); + } - // Add the path for template overrides - $paths->insert(JPATH_THEMES . '/' . $this->getApplication()->getTemplate() . '/html/com_patchtester/' . $view, 2); + // Initialize the view class now + $view = new $viewClass($model, $paths); +// Echo the rendered view for the application + echo $view->render(); +// Finished! + return true; + } - // Add the path for the default layouts - $paths->insert(dirname(__DIR__) . '/View/' . ucfirst($view) . '/tmpl', 1); + /** + * Sets the state for the model object + * + * @param AbstractModel $model Model object + * + * @return Registry + * + * @since 2.0 + */ + protected function initializeState($model) + { + $state = parent::initializeState($model); + $app = $this->getApplication(); +// Load the filter state. + $state->set('filter.search', $app->getUserStateFromRequest($this->context . '.filter.search', 'filter_search', '')); + $state->set('filter.applied', $app->getUserStateFromRequest($this->context . '.filter.applied', 'filter_applied', '')); + $state->set('filter.branch', $app->getUserStateFromRequest($this->context . '.filter.branch', 'filter_branch', '')); + $state->set('filter.rtc', $app->getUserStateFromRequest($this->context . '.filter.rtc', 'filter_rtc', '')); + $state->set('filter.npm', $app->getUserStateFromRequest($this->context . '.filter.npm', 'filter_npm', '')); + $state->set('filter.label', $app->getUserStateFromRequest($this->context . '.filter.label', 'filter_label', '')); +// Pre-fill the limits. + $limit = $app->getUserStateFromRequest('global.list.limit', 'limit', $app->input->get('list_limit', 20), 'uint'); + $state->set('list.limit', $limit); + $fullOrdering = $app->getUserStateFromRequest($this->context . '.fullorder', 'list_fullordering', $this->defaultFullOrdering); + $orderingParts = explode(' ', $fullOrdering); + if (count($orderingParts) !== 2) { + $fullOrdering = $this->defaultFullOrdering; + $orderingParts = explode(' ', $fullOrdering); + } - // Build the class names for the model and view - $viewClass = '\\PatchTester\\View\\' . ucfirst($view) . '\\' . ucfirst($view) . ucfirst($format) . 'View'; - $modelClass = '\\PatchTester\\Model\\' . ucfirst($view) . 'Model'; + $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); + } - // Sanity check - Ensure our classes exist - if (!class_exists($viewClass)) - { - // Try to use a default view - $viewClass = '\\PatchTester\\View\\Default' . ucfirst($format) . 'View'; + // The 1st part will be the ordering + $ordering = $orderingParts[array_key_first($orderingParts)]; + if (in_array($ordering, $model->getSortFields())) { + $state->set('list.ordering', $ordering); + } - 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; - } + $value = $app->getUserStateFromRequest($this->context . '.limitstart', 'limitstart', 0); + $limitstart = ($limit != 0 ? (floor($value / $limit) * $limit) : 0); + $state->set('list.start', $limitstart); + return $state; + } } diff --git a/administrator/components/com_patchtester/PatchTester/Controller/FetchController.php b/administrator/components/com_patchtester/PatchTester/Controller/FetchController.php index 2d05a96..02f6a1b 100644 --- a/administrator/components/com_patchtester/PatchTester/Controller/FetchController.php +++ b/administrator/components/com_patchtester/PatchTester/Controller/FetchController.php @@ -1,4 +1,5 @@ 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); + /** + * Execute the controller. + * + * @return void Redirects the application + * + * @since 2.0 + */ + public function execute() + { + // 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(); + try { + // Fetch our page from the session + $page = $session->get('com_patchtester_fetcher_page', 1); + $model = new PullsModel(); + // Initialize the state for the model + $state = $this->initializeState($model); + foreach ($state as $key => $value) { + $model->setState($key, $value); + } - $session = Factory::getSession(); + $status = $model->requestFromGithub($page); + } catch (\Exception $e) { + $response = new JsonResponse($e); + $this->getApplication()->sendHeaders(); + echo json_encode($response); + $this->getApplication()->close(1); + } - try - { - // Fetch our page from the session - $page = $session->get('com_patchtester_fetcher_page', 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']); + } - $model = new PullsModel; + // Update the UI and session now + if ($status['complete'] || $page === $session->get('com_patchtester_fetcher_last_page', false)) { + $status['complete'] = true; + $status['header'] = Text::_('COM_PATCHTESTER_FETCH_SUCCESSFUL', true); + $message = Text::_('COM_PATCHTESTER_FETCH_COMPLETE_CLOSE_WINDOW', true); + } elseif (isset($status['page'])) { + $session->set('com_patchtester_fetcher_page', $status['page']); + $message = Text::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', $status['page']); - // Initialize the state for the model - $state = $this->initializeState($model); + if ($session->has('com_patchtester_fetcher_last_page')) { + $message = Text::sprintf( + 'COM_PATCHTESTER_FETCH_PAGE_NUMBER_OF_TOTAL', + $status['page'], + $session->get('com_patchtester_fetcher_last_page') + ); + } + } - foreach ($state as $key => $value) - { - $model->setState($key, $value); - } - - $status = $model->requestFromGithub($page); - } - catch (\Exception $e) - { - $response = new JsonResponse($e); - - $this->getApplication()->sendHeaders(); - echo json_encode($response); - - $this->getApplication()->close(1); - } - - // Store the last page to the session if given one - if (isset($status['lastPage']) && $status['lastPage'] !== false) - { - $session->set('com_patchtester_fetcher_last_page', $status['lastPage']); - } - - // Update the UI and session now - if ($status['complete'] || $page === $session->get('com_patchtester_fetcher_last_page', false)) - { - $status['complete'] = true; - $status['header'] = Text::_('COM_PATCHTESTER_FETCH_SUCCESSFUL', true); - - $message = Text::_('COM_PATCHTESTER_FETCH_COMPLETE_CLOSE_WINDOW', true); - } - elseif (isset($status['page'])) - { - $session->set('com_patchtester_fetcher_page', $status['page']); - - $message = Text::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', $status['page']); - - if ($session->has('com_patchtester_fetcher_last_page')) - { - $message = Text::sprintf( - 'COM_PATCHTESTER_FETCH_PAGE_NUMBER_OF_TOTAL', $status['page'], $session->get('com_patchtester_fetcher_last_page') - ); - } - } - - $response = new JsonResponse($status, $message, false, true); - - $this->getApplication()->sendHeaders(); - echo json_encode($response); - - $this->getApplication()->close(); - } + $response = new JsonResponse($status, $message, false, true); + $this->getApplication()->sendHeaders(); + echo json_encode($response); + $this->getApplication()->close(); + } } diff --git a/administrator/components/com_patchtester/PatchTester/Controller/ResetController.php b/administrator/components/com_patchtester/PatchTester/Controller/ResetController.php index f41c351..3f3bc18 100644 --- a/administrator/components/com_patchtester/PatchTester/Controller/ResetController.php +++ b/administrator/components/com_patchtester/PatchTester/Controller/ResetController.php @@ -1,4 +1,5 @@ context, null, Factory::getDbo()); + $testsModel = new TestsModel(null, Factory::getDbo()); +// Check the applied patches in the database first + $appliedPatches = $testsModel->getAppliedPatches(); + $params = ComponentHelper::getParams('com_patchtester'); +// Decide based on repository settings whether patch will be applied through Github or CIServer + if ((bool) $params->get('ci_switch', 1)) { +// Let's try to cleanly revert all applied patches with ci + foreach ($appliedPatches as $patch) { + try { + $pullModel->revertWithCIServer($patch->id); + } catch (\RuntimeException $e) { + $revertErrored = true; + } + } + } else { + // Let's try to cleanly revert all applied patches + foreach ($appliedPatches as $patch) { + try { + $pullModel->revertWithGitHub($patch->id); + } catch (\RuntimeException $e) { + $revertErrored = true; + } + } + } - $pullModel = new PullModel(null, Factory::getDbo()); - $pullsModel = new PullsModel($this->context, null, Factory::getDbo()); - $testsModel = new TestsModel(null, Factory::getDbo()); + // If we errored out reverting patches, we'll need to truncate the table + if ($revertErrored) { + try { + $testsModel->truncateTable(); + } catch (\RuntimeException $e) { + $hasErrors = true; - // Check the applied patches in the database first - $appliedPatches = $testsModel->getAppliedPatches(); + $this->getApplication()->enqueueMessage( + Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_PULLS_TABLE', $e->getMessage()), + 'error' + ); + } + } - $params = ComponentHelper::getParams('com_patchtester'); + // Now truncate the pulls table + try { + $pullsModel->truncateTable(); + } catch (\RuntimeException $e) { + $hasErrors = true; - // Decide based on repository settings whether patch will be applied through Github or CIServer - if ((bool) $params->get('ci_switch', 1)) - { - // Let's try to cleanly revert all applied patches with ci - foreach ($appliedPatches as $patch) - { - try - { - $pullModel->revertWithCIServer($patch->id); - } - catch (\RuntimeException $e) - { - $revertErrored = true; - } - } - } - else - { - // Let's try to cleanly revert all applied patches - foreach ($appliedPatches as $patch) - { - try - { - $pullModel->revertWithGitHub($patch->id); - } - catch (\RuntimeException $e) - { - $revertErrored = true; - } - } - } + $this->getApplication()->enqueueMessage( + Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_TESTS_TABLE', $e->getMessage()), + 'error' + ); + } - // If we errored out reverting patches, we'll need to truncate the table - if ($revertErrored) - { - try - { - $testsModel->truncateTable(); - } - catch (\RuntimeException $e) - { - $hasErrors = true; + // Check the backups directory to see if any .txt files remain; clear them if so + $backups = Folder::files(JPATH_COMPONENT . '/backups', '.txt'); - $this->getApplication()->enqueueMessage( - Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_PULLS_TABLE', $e->getMessage()), 'error' - ); - } - } + if (count($backups)) { + foreach ($backups as $file) { + if (!File::delete(JPATH_COMPONENT . '/backups/' . $file)) { + $this->getApplication()->enqueueMessage( + Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_COMPONENT . '/backups/' . $file), + 'error' + ); + $hasErrors = true; + } + } + } - // Now truncate the pulls table - try - { - $pullsModel->truncateTable(); - } - catch (\RuntimeException $e) - { - $hasErrors = true; + // Processing completed, inform the user of a success or fail + if ($hasErrors) { + $msg = Text::sprintf( + 'COM_PATCHTESTER_RESET_HAS_ERRORS', + JPATH_COMPONENT . '/backups', + Factory::getDbo()->replacePrefix('#__patchtester_tests') + ); + $type = 'warning'; + } else { + $msg = Text::_('COM_PATCHTESTER_RESET_OK'); + $type = 'notice'; + } + } catch (\Exception $exception) { + $msg = $exception->getMessage(); + $type = 'error'; + } - $this->getApplication()->enqueueMessage( - Text::sprintf('COM_PATCHTESTER_ERROR_TRUNCATING_TESTS_TABLE', $e->getMessage()), 'error' - ); - } - - // Check the backups directory to see if any .txt files remain; clear them if so - $backups = Folder::files(JPATH_COMPONENT . '/backups', '.txt'); - - if (count($backups)) - { - foreach ($backups as $file) - { - if (!File::delete(JPATH_COMPONENT . '/backups/' . $file)) - { - $this->getApplication()->enqueueMessage( - Text::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_COMPONENT . '/backups/' . $file), 'error' - ); - - $hasErrors = true; - } - } - } - - // Processing completed, inform the user of a success or fail - if ($hasErrors) - { - $msg = Text::sprintf( - 'COM_PATCHTESTER_RESET_HAS_ERRORS', JPATH_COMPONENT . '/backups', Factory::getDbo()->replacePrefix('#__patchtester_tests') - ); - $type = 'warning'; - } - else - { - $msg = Text::_('COM_PATCHTESTER_RESET_OK'); - $type = 'notice'; - } - } - catch (\Exception $exception) - { - $msg = $exception->getMessage(); - $type = 'error'; - } - - $this->getApplication()->enqueueMessage($msg, $type); - $this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false)); - } + $this->getApplication()->enqueueMessage($msg, $type); + $this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false)); + } } diff --git a/administrator/components/com_patchtester/PatchTester/Controller/RevertController.php b/administrator/components/com_patchtester/PatchTester/Controller/RevertController.php index 5fde430..8fa81c1 100644 --- a/administrator/components/com_patchtester/PatchTester/Controller/RevertController.php +++ b/administrator/components/com_patchtester/PatchTester/Controller/RevertController.php @@ -1,4 +1,5 @@ setState($this->initializeState($model)); + $model->revert($this->getInput()->getUint('pull_id')); + $msg = Text::_('COM_PATCHTESTER_REVERT_OK'); + $type = 'message'; + } catch (\Exception $e) { + $msg = $e->getMessage(); + $type = 'error'; + } - // Initialize the state for the model - $model->setState($this->initializeState($model)); - - $model->revert($this->getInput()->getUint('pull_id')); - - $msg = Text::_('COM_PATCHTESTER_REVERT_OK'); - $type = 'message'; - } - catch (\Exception $e) - { - $msg = $e->getMessage(); - $type = 'error'; - } - - $this->getApplication()->enqueueMessage($msg, $type); - $this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false)); - } + $this->getApplication()->enqueueMessage($msg, $type); + $this->getApplication()->redirect(Route::_('index.php?option=com_patchtester', false)); + } } diff --git a/administrator/components/com_patchtester/PatchTester/Controller/StartfetchController.php b/administrator/components/com_patchtester/PatchTester/Controller/StartfetchController.php index 5460fc4..9b43529 100644 --- a/administrator/components/com_patchtester/PatchTester/Controller/StartfetchController.php +++ b/administrator/components/com_patchtester/PatchTester/Controller/StartfetchController.php @@ -1,4 +1,5 @@ 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); + /** + * Execute the controller. + * + * @return void Redirects the application + * + * @since 2.0 + */ + public function execute() + { + // 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. + if (!Session::checkToken('request')) { + $response = new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403)); + $this->getApplication()->sendHeaders(); + echo json_encode($response); + $this->getApplication()->close(1); + } - // Check for a valid token. If invalid, send a 403 with the error message. - if (!Session::checkToken('request')) - { - $response = new JsonResponse(new \Exception(Text::_('JINVALID_TOKEN'), 403)); + // Make sure we can fetch the data from GitHub - throw an error on < 10 available requests + try { + $rateResponse = Helper::initializeGithub()->getRateLimit(); + $rate = json_decode($rateResponse->body); + } catch (\Exception $e) { + $response = new JsonResponse(new \Exception(Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e)); + $this->getApplication()->sendHeaders(); + echo json_encode($response); + $this->getApplication()->close(1); + } - $this->getApplication()->sendHeaders(); - echo json_encode($response); + // If over the API limit, we can't build this list + if ($rate->resources->core->remaining < 10) { + $response = new JsonResponse(new \Exception(Text::sprintf('COM_PATCHTESTER_API_LIMIT_LIST', Factory::getDate($rate->resources->core->reset)), 429)); + $this->getApplication()->sendHeaders(); + echo json_encode($response); + $this->getApplication()->close(1); + } - $this->getApplication()->close(1); - } + $testsModel = new TestsModel(null, Factory::getDbo()); + try { + // Sanity check, ensure there aren't any applied patches + if (count($testsModel->getAppliedPatches()) >= 1) { + $response = new JsonResponse(new \Exception(Text::_('COM_PATCHTESTER_ERROR_APPLIED_PATCHES'), 500)); + $this->getApplication()->sendHeaders(); + echo json_encode($response); + $this->getApplication()->close(1); + } + } catch (\Exception $e) { + $response = new JsonResponse($e); + $this->getApplication()->sendHeaders(); + echo json_encode($response); + $this->getApplication()->close(1); + } - // Make sure we can fetch the data from GitHub - throw an error on < 10 available requests - try - { - $rateResponse = Helper::initializeGithub()->getRateLimit(); - $rate = json_decode($rateResponse->body); - } - catch (\Exception $e) - { - $response = new JsonResponse( - new \Exception( - Text::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), - $e->getCode(), - $e - ) - ); - - $this->getApplication()->sendHeaders(); - echo json_encode($response); - - $this->getApplication()->close(1); - } - - // If over the API limit, we can't build this list - if ($rate->resources->core->remaining < 10) - { - $response = new JsonResponse( - new \Exception( - Text::sprintf('COM_PATCHTESTER_API_LIMIT_LIST', Factory::getDate($rate->resources->core->reset)), - 429 - ) - ); - - $this->getApplication()->sendHeaders(); - echo json_encode($response); - - $this->getApplication()->close(1); - } - - $testsModel = new TestsModel(null, Factory::getDbo()); - - try - { - // Sanity check, ensure there aren't any applied patches - if (count($testsModel->getAppliedPatches()) >= 1) - { - $response = new JsonResponse(new \Exception(Text::_('COM_PATCHTESTER_ERROR_APPLIED_PATCHES'), 500)); - - $this->getApplication()->sendHeaders(); - echo json_encode($response); - - $this->getApplication()->close(1); - } - } - catch (\Exception $e) - { - $response = new JsonResponse($e); - - $this->getApplication()->sendHeaders(); - echo json_encode($response); - - $this->getApplication()->close(1); - } - - // We're able to successfully pull data, prepare our environment - Factory::getSession()->set('com_patchtester_fetcher_page', 1); - - $response = new JsonResponse( - array('complete' => false, 'header' => Text::_('COM_PATCHTESTER_FETCH_PROCESSING', true)), - Text::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', 1), - false, - true - ); - - $this->getApplication()->sendHeaders(); - echo json_encode($response); - - $this->getApplication()->close(); - } + // We're able to successfully pull data, prepare our environment + Factory::getSession()->set('com_patchtester_fetcher_page', 1); + $response = new JsonResponse(array('complete' => false, 'header' => Text::_('COM_PATCHTESTER_FETCH_PROCESSING', true)), Text::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', 1), false, true); + $this->getApplication()->sendHeaders(); + echo json_encode($response); + $this->getApplication()->close(); + } } diff --git a/administrator/components/com_patchtester/PatchTester/Field/BranchField.php b/administrator/components/com_patchtester/PatchTester/Field/BranchField.php index 12c5f55..e0a1878 100644 --- a/administrator/components/com_patchtester/PatchTester/Field/BranchField.php +++ b/administrator/components/com_patchtester/PatchTester/Field/BranchField.php @@ -1,4 +1,5 @@ get('DatabaseDriver'); - $query = $db->getQuery(true); - - $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); - } + /** + * Type of field + * + * @var string + * @since 4.1.0 + */ + protected $type = 'Branch'; +/** + * Build a list of available branches. + * + * @return array List of options + * + * @since 4.1.0 + */ + public function getOptions(): array + { + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true); + $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); + } } diff --git a/administrator/components/com_patchtester/PatchTester/Field/LabelField.php b/administrator/components/com_patchtester/PatchTester/Field/LabelField.php index cc90d0f..62c5286 100644 --- a/administrator/components/com_patchtester/PatchTester/Field/LabelField.php +++ b/administrator/components/com_patchtester/PatchTester/Field/LabelField.php @@ -1,4 +1,5 @@ get('DatabaseDriver'); - $query = $db->getQuery(true); - - $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); - } + /** + * Type of field + * + * @var string + * @since 4.1.0 + */ + protected $type = 'Label'; +/** + * Build a list of available fields. + * + * @return array List of options + * + * @since 4.1.0 + */ + public function getOptions(): array + { + $db = Factory::getContainer()->get('DatabaseDriver'); + $query = $db->getQuery(true); + $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); + } } diff --git a/administrator/components/com_patchtester/PatchTester/GitHub/Exception/UnexpectedResponse.php b/administrator/components/com_patchtester/PatchTester/GitHub/Exception/UnexpectedResponse.php index 03c13a7..2560548 100644 --- a/administrator/components/com_patchtester/PatchTester/GitHub/Exception/UnexpectedResponse.php +++ b/administrator/components/com_patchtester/PatchTester/GitHub/Exception/UnexpectedResponse.php @@ -1,4 +1,5 @@ response = $response; + } - /** - * Constructor - * - * @param Response $response The Response object. - * @param string $message The Exception message to throw. - * @param integer $code The Exception code. - * @param \Exception $previous The previous exception used for the exception chaining. - * - * @since 3.0.0 - */ - public function __construct(Response $response, $message = '', $code = 0, \Exception $previous = null) - { - parent::__construct($message, $code, $previous); - - $this->response = $response; - } - - /** - * Get the Response object. - * - * @return Response - * - * @since 3.0.0 - */ - public function getResponse() - { - return $this->response; - } + /** + * Get the Response object. + * + * @return Response + * + * @since 3.0.0 + */ + public function getResponse() + { + return $this->response; + } } diff --git a/administrator/components/com_patchtester/PatchTester/GitHub/GitHub.php b/administrator/components/com_patchtester/PatchTester/GitHub/GitHub.php index 060425a..0312a53 100644 --- a/administrator/components/com_patchtester/PatchTester/GitHub/GitHub.php +++ b/administrator/components/com_patchtester/PatchTester/GitHub/GitHub.php @@ -1,4 +1,5 @@ options = $options ?: new Registry(); + $this->client = $client ?: HttpFactory::getHttp($options); + } - /** - * The HTTP client object to use in sending HTTP requests. - * - * @var Http - * @since 3.0.0 - */ - protected $client; + /** + * Get the HTTP client for this connector. + * + * @return Http + * + * @since 3.0.0 + */ + public function getClient() + { + return $this->client; + } - /** - * Constructor. - * - * @param Registry $options Connector options. - * @param Http $client The HTTP client object. - * - * @since 3.0.0 - */ - public function __construct(Registry $options = null, Http $client = null) - { - $this->options = $options ?: new Registry; - $this->client = $client ?: HttpFactory::getHttp($options); - } + /** + * Get the diff for a pull request. + * + * @param string $user The name of the owner of the GitHub repository. + * @param string $repo The name of the GitHub repository. + * @param integer $pullId The pull request number. + * + * @return Response + * + * @since 3.0.0 + */ + public function getDiffForPullRequest($user, $repo, $pullId) + { + // Build the request path. + $path = "/repos/$user/$repo/pulls/" . (int) $pullId; +// Build the request headers. + $headers = array('Accept' => 'application/vnd.github.diff'); + $prepared = $this->prepareRequest($path, 0, 0, $headers); + return $this->processResponse($this->client->get($prepared['url'], $prepared['headers'])); + } - /** - * Get the HTTP client for this connector. - * - * @return Http - * - * @since 3.0.0 - */ - public function getClient() - { - return $this->client; - } + /** + * Method to build and return a full request 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 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 + * + * @return array Associative array containing the prepared URL and request headers + * + * @since 3.0.0 + */ + protected function prepareRequest( + $path, + $page = 0, + $limit = 0, + array $headers = array() + ) { + $url = $this->fetchUrl($path, $page, $limit); + if ($token = $this->options->get('gh.token', false)) { + $headers['Authorization'] = "token $token"; + } - /** - * Get the diff for a pull request. - * - * @param string $user The name of the owner of the GitHub repository. - * @param string $repo The name of the GitHub repository. - * @param integer $pullId The pull request number. - * - * @return Response - * - * @since 3.0.0 - */ - public function getDiffForPullRequest($user, $repo, $pullId) - { - // Build the request path. - $path = "/repos/$user/$repo/pulls/" . (int) $pullId; + return array('url' => $url, 'headers' => $headers); + } - // Build the request headers. - $headers = array('Accept' => 'application/vnd.github.diff'); + /** + * Build and return a full request URL. + * + * This method will add appropriate pagination details and basic authentication credentials if necessary + * and also prepend the API url to have a complete URL for the request. + * + * @param string $path URL to inflect + * @param integer $page Page to request + * @param integer $limit Number of results to return per page + * + * @return string The request URL. + * + * @since 3.0.0 + */ + protected function fetchUrl($path, $page = 0, $limit = 0) + { + // Get a new Uri object using the API URL and given path. + $uri = new Uri($this->options->get('api.url') . $path); +// If we have a defined page number add it to the JUri object. + if ($page > 0) { + $uri->setVar('page', (int) $page); + } - $prepared = $this->prepareRequest($path, 0, 0, $headers); + // If we have a defined items per page add it to the JUri object. + if ($limit > 0) { + $uri->setVar('per_page', (int) $limit); + } - return $this->processResponse( - $this->client->get($prepared['url'], $prepared['headers']) - ); - } + return (string) $uri; + } - /** - * Method to build and return a full request 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 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 - * - * @return array Associative array containing the prepared URL and request headers - * - * @since 3.0.0 - */ - protected function prepareRequest($path, $page = 0, $limit = 0, - array $headers = array() - ) { - $url = $this->fetchUrl($path, $page, $limit); + /** + * Process the response and return it. + * + * @param Response $response The response. + * @param integer $expectedCode The expected response code. + * + * @return Response + * + * @since 3.0.0 + * @throws Exception\UnexpectedResponse + */ + protected function processResponse(Response $response, $expectedCode = 200) + { + // Validate the response code. + if ($response->code != $expectedCode) { +// 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'); - if ($token = $this->options->get('gh.token', false)) - { - $headers['Authorization'] = "token $token"; - } + throw new Exception\UnexpectedResponse( + $response, + $error, + $response->code + ); + } - return array('url' => $url, 'headers' => $headers); - } + return $response; + } - /** - * Build and return a full request URL. - * - * This method will add appropriate pagination details and basic authentication credentials if necessary - * and also prepend the API url to have a complete URL for the request. - * - * @param string $path URL to inflect - * @param integer $page Page to request - * @param integer $limit Number of results to return per page - * - * @return string The request URL. - * - * @since 3.0.0 - */ - protected function fetchUrl($path, $page = 0, $limit = 0) - { - // Get a new Uri object using the API URL and given path. - $uri = new Uri($this->options->get('api.url') . $path); + /** + * Get a file's contents from a repository. + * + * @param string $user The name of the owner of the GitHub repository. + * @param string $repo The name of the GitHub repository. + * @param string $path The content path. + * @param string $ref The name of the commit/branch/tag. Default: the repository’s default branch (usually master) + * + * @return Response + * + * @since 3.0.0 + */ + public function getFileContents($user, $repo, $path, $ref = null) + { + $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; + } - // If we have a defined page number add it to the JUri object. - if ($page > 0) - { - $uri->setVar('page', (int) $page); - } + return $this->processResponse($this->client->get($prepared['url'], $prepared['headers'])); + } - // If we have a defined items per page add it to the JUri object. - if ($limit > 0) - { - $uri->setVar('per_page', (int) $limit); - } + /** + * Get the list of modified files for a pull request. + * + * @param string $user The name of the owner of the GitHub repository. + * @param string $repo The name of the GitHub repository. + * @param integer $pullId The pull request number. + * + * @return Response + * + * @since 3.0.0 + */ + public function getFilesForPullRequest($user, $repo, $pullId, $page = 1) + { + // Build the request path. + $path = "/repos/$user/$repo/pulls/" . (int) $pullId . '/files?page=' . $page; + $prepared = $this->prepareRequest($path); + return $this->processResponse($this->client->get($prepared['url'], $prepared['headers'])); + } - return (string) $uri; - } + /** + * Get a list of the open issues for a repository. + * + * @param string $user The name of the owner of the GitHub repository. + * @param string $repo The name of the GitHub repository. + * @param integer $page The page number from which to get items. + * @param integer $limit The number of items on a page. + * + * @return Response + * + * @since 3.0.0 + */ + public function getOpenIssues($user, $repo, $page = 0, $limit = 0) + { + $prepared = $this->prepareRequest( + "/repos/$user/$repo/issues", + $page, + $limit + ); + return $this->processResponse($this->client->get($prepared['url'], $prepared['headers'])); + } - /** - * Process the response and return it. - * - * @param Response $response The response. - * @param integer $expectedCode The expected response code. - * - * @return Response - * - * @since 3.0.0 - * @throws Exception\UnexpectedResponse - */ - protected function processResponse(Response $response, $expectedCode = 200) - { - // Validate the response code. - if ($response->code != $expectedCode) - { - // 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'); + /** + * Get a list of the open pull requests for a repository. + * + * @param string $user The name of the owner of the GitHub repository. + * @param string $repo The name of the GitHub repository. + * @param integer $page The page number from which to get items. + * @param integer $limit The number of items on a page. + * + * @return Response + * + * @since 3.0.0 + */ + public function getOpenPulls($user, $repo, $page = 0, $limit = 0) + { + $prepared = $this->prepareRequest( + "/repos/$user/$repo/pulls", + $page, + $limit + ); + return $this->processResponse($this->client->get($prepared['url'], $prepared['headers'])); + } - throw new Exception\UnexpectedResponse( - $response, $error, $response->code - ); - } + /** + * Get an option from the connector. + * + * @param string $key The name of the option to get. + * @param mixed $default The default value if the option is not set. + * + * @return mixed The option value. + * + * @since 3.0.0 + */ + public function getOption($key, $default = null) + { + return $this->options->get($key, $default); + } - return $response; - } + /** + * Get a single pull request. + * + * @param string $user The name of the owner of the GitHub repository. + * @param string $repo The name of the GitHub repository. + * @param integer $pullId The pull request number. + * + * @return Response + * + * @since 3.0.0 + */ + public function getPullRequest($user, $repo, $pullId) + { + // Build the request path. + $path = "/repos/$user/$repo/pulls/" . (int) $pullId; + $prepared = $this->prepareRequest($path); + return $this->processResponse($this->client->get($prepared['url'], $prepared['headers'])); + } - /** - * Get a file's contents from a repository. - * - * @param string $user The name of the owner of the GitHub repository. - * @param string $repo The name of the GitHub repository. - * @param string $path The content path. - * @param string $ref The name of the commit/branch/tag. Default: the repository’s default branch (usually master) - * - * @return Response - * - * @since 3.0.0 - */ - public function getFileContents($user, $repo, $path, $ref = null) - { - $path = "/repos/$user/$repo/contents/$path"; + /** + * Get the rate limit for the authenticated user. + * + * @return Response + * + * @since 3.0.0 + */ + public function getRateLimit() + { + $prepared = $this->prepareRequest('/rate_limit'); + return $this->processResponse($this->client->get($prepared['url'], $prepared['headers'])); + } - $prepared = $this->prepareRequest($path); - - if ($ref) - { - $url = new Uri($prepared['url']); - $url->setVar('ref', $ref); - - $prepared['url'] = (string) $url; - } - - return $this->processResponse( - $this->client->get($prepared['url'], $prepared['headers']) - ); - } - - /** - * Get the list of modified files for a pull request. - * - * @param string $user The name of the owner of the GitHub repository. - * @param string $repo The name of the GitHub repository. - * @param integer $pullId The pull request number. - * - * @return Response - * - * @since 3.0.0 - */ - public function getFilesForPullRequest($user, $repo, $pullId, $page = 1) - { - // Build the request path. - $path = "/repos/$user/$repo/pulls/" . (int) $pullId . '/files?page=' . $page; - - $prepared = $this->prepareRequest($path); - - return $this->processResponse( - $this->client->get($prepared['url'], $prepared['headers']) - ); - } - - /** - * Get a list of the open issues for a repository. - * - * @param string $user The name of the owner of the GitHub repository. - * @param string $repo The name of the GitHub repository. - * @param integer $page The page number from which to get items. - * @param integer $limit The number of items on a page. - * - * @return Response - * - * @since 3.0.0 - */ - public function getOpenIssues($user, $repo, $page = 0, $limit = 0) - { - $prepared = $this->prepareRequest( - "/repos/$user/$repo/issues", $page, $limit - ); - - return $this->processResponse( - $this->client->get($prepared['url'], $prepared['headers']) - ); - } - - /** - * Get a list of the open pull requests for a repository. - * - * @param string $user The name of the owner of the GitHub repository. - * @param string $repo The name of the GitHub repository. - * @param integer $page The page number from which to get items. - * @param integer $limit The number of items on a page. - * - * @return Response - * - * @since 3.0.0 - */ - public function getOpenPulls($user, $repo, $page = 0, $limit = 0) - { - $prepared = $this->prepareRequest( - "/repos/$user/$repo/pulls", $page, $limit - ); - - return $this->processResponse( - $this->client->get($prepared['url'], $prepared['headers']) - ); - } - - /** - * Get an option from the connector. - * - * @param string $key The name of the option to get. - * @param mixed $default The default value if the option is not set. - * - * @return mixed The option value. - * - * @since 3.0.0 - */ - public function getOption($key, $default = null) - { - return $this->options->get($key, $default); - } - - /** - * Get a single pull request. - * - * @param string $user The name of the owner of the GitHub repository. - * @param string $repo The name of the GitHub repository. - * @param integer $pullId The pull request number. - * - * @return Response - * - * @since 3.0.0 - */ - public function getPullRequest($user, $repo, $pullId) - { - // Build the request path. - $path = "/repos/$user/$repo/pulls/" . (int) $pullId; - - $prepared = $this->prepareRequest($path); - - return $this->processResponse( - $this->client->get($prepared['url'], $prepared['headers']) - ); - } - - /** - * Get the rate limit for the authenticated user. - * - * @return Response - * - * @since 3.0.0 - */ - public function getRateLimit() - { - $prepared = $this->prepareRequest('/rate_limit'); - - return $this->processResponse( - $this->client->get($prepared['url'], $prepared['headers']) - ); - } - - /** - * Set an option for the connector. - * - * @param string $key The name of the option to set. - * @param mixed $value The option value to set. - * - * @return $this - * - * @since 3.0.0 - */ - public function setOption($key, $value) - { - $this->options->set($key, $value); - - return $this; - } + /** + * Set an option for the connector. + * + * @param string $key The name of the option to set. + * @param mixed $value The option value to set. + * + * @return $this + * + * @since 3.0.0 + */ + public function setOption($key, $value) + { + $this->options->set($key, $value); + return $this; + } } diff --git a/administrator/components/com_patchtester/PatchTester/Helper.php b/administrator/components/com_patchtester/PatchTester/Helper.php index 9adb047..3b9a5e2 100644 --- a/administrator/components/com_patchtester/PatchTester/Helper.php +++ b/administrator/components/com_patchtester/PatchTester/Helper.php @@ -1,4 +1,5 @@ set('userAgent', 'PatchTester/3.0'); + // Set the default timeout to 120 seconds + $options->set('timeout', 120); + // Set the API URL + $options->set('api.url', 'https://api.github.com'); + // If an API token is set in the params, use it for authentication + if ($params->get('gh_token', '')) { + $options->set('headers', ['Authorization' => 'token ' . $params->get('gh_token', '')]); + } else { + // Display a message about the lowered API limit without credentials + Factory::getApplication()->enqueueMessage(Text::_('COM_PATCHTESTER_NO_CREDENTIALS'), 'notice'); + } - $options = new Registry; + return new GitHub($options); + } - // Set a user agent for the request - $options->set('userAgent', 'PatchTester/3.0'); - - // Set the default timeout to 120 seconds - $options->set('timeout', 120); - - // Set the API URL - $options->set('api.url', 'https://api.github.com'); - - // If an API token is set in the params, use it for authentication - if ($params->get('gh_token', '')) - { - $options->set('headers', ['Authorization' => 'token ' . $params->get('gh_token', '')]); - } - // Display a message about the lowered API limit without credentials - else - { - Factory::getApplication()->enqueueMessage(Text::_('COM_PATCHTESTER_NO_CREDENTIALS'), 'notice'); - } - - return new GitHub($options); - } - - /** - * Initializes the CI Settings - * - * @return Registry - * - * @since 3.0 - */ - public static function initializeCISettings() - { - $params = ComponentHelper::getParams('com_patchtester'); - - $options = new Registry; - - // Set CI server address for the request - $options->set('server.url', $params->get('ci_server', 'https://ci.joomla.org:444')); - - // Set name of the zip archive - $options->set('zip.name', 'build.zip'); - $options->set('zip.log.name', 'deleted_files.log'); - - // Set temp archive for extracting and downloading files - $options->set('folder.temp', Factory::getConfig()->get('tmp_path')); - $options->set('folder.backups', JPATH_COMPONENT . '/backups'); - - // Set full url for addressing the file - $options->set('zip.url', $options->get('server.url') . '/artifacts/joomla/joomla-cms/4.0-dev/%s/patchtester/' . $options->get('zip.name')); - - return $options; - } + /** + * Initializes the CI Settings + * + * @return Registry + * + * @since 3.0 + */ + public static function initializeCISettings() + { + $params = ComponentHelper::getParams('com_patchtester'); + $options = new Registry(); +// Set CI server address for the request + $options->set('server.url', $params->get('ci_server', 'https://ci.joomla.org:444')); +// Set name of the zip archive + $options->set('zip.name', 'build.zip'); + $options->set('zip.log.name', 'deleted_files.log'); +// Set temp archive for extracting and downloading files + $options->set('folder.temp', Factory::getConfig()->get('tmp_path')); + $options->set('folder.backups', JPATH_COMPONENT . '/backups'); +// Set full url for addressing the file + $options->set('zip.url', $options->get('server.url') . '/artifacts/joomla/joomla-cms/4.0-dev/%s/patchtester/' . $options->get('zip.name')); + return $options; + } } diff --git a/administrator/components/com_patchtester/PatchTester/Model/AbstractModel.php b/administrator/components/com_patchtester/PatchTester/Model/AbstractModel.php index 8a65832..2d52bfa 100644 --- a/administrator/components/com_patchtester/PatchTester/Model/AbstractModel.php +++ b/administrator/components/com_patchtester/PatchTester/Model/AbstractModel.php @@ -1,4 +1,5 @@ state = $state ?: new Registry(); + $this->db = $db ?: Factory::getDbo(); + } - /** - * The model state. - * - * @var Registry - * @since 4.0.0 - */ - protected $state; + /** + * Get the database driver. + * + * @return \JDatabaseDriver + * + * @since 4.0.0 + */ + public function getDb() + { + return $this->db; + } - /** - * Instantiate the model. - * - * @param Registry $state The model state. - * @param \JDatabaseDriver $db The database adpater. - * - * @since 4.0.0 - */ - public function __construct(Registry $state = null, \JDatabaseDriver $db = null) - { - $this->state = $state ?: new Registry; - $this->db = $db ?: Factory::getDbo(); - } + /** + * Get the model state. + * + * @return Registry + * + * @since 4.0.0 + */ + public function getState() + { + return $this->state; + } - /** - * Get the database driver. - * - * @return \JDatabaseDriver - * - * @since 4.0.0 - */ - public function getDb() - { - return $this->db; - } + /** + * Set the database driver. + * + * @param \JDatabaseDriver $db The database driver. + * + * @return void + * + * @since 4.0.0 + */ + public function setDb(\JDatabaseDriver $db) + { + $this->db = $db; + } - /** - * Get the model state. - * - * @return Registry - * - * @since 4.0.0 - */ - public function getState() - { - return $this->state; - } - - /** - * Set the database driver. - * - * @param \JDatabaseDriver $db The database driver. - * - * @return void - * - * @since 4.0.0 - */ - public function setDb(\JDatabaseDriver $db) - { - $this->db = $db; - } - - /** - * Set the model state. - * - * @param Registry $state The state object. - * - * @return void - * - * @since 4.0.0 - */ - public function setState(Registry $state) - { - $this->state = $state; - } + /** + * Set the model state. + * + * @param Registry $state The state object. + * + * @return void + * + * @since 4.0.0 + */ + public function setState(Registry $state) + { + $this->state = $state; + } } diff --git a/administrator/components/com_patchtester/PatchTester/Model/FetchModel.php b/administrator/components/com_patchtester/PatchTester/Model/FetchModel.php index cc265c0..ab17c07 100644 --- a/administrator/components/com_patchtester/PatchTester/Model/FetchModel.php +++ b/administrator/components/com_patchtester/PatchTester/Model/FetchModel.php @@ -1,4 +1,5 @@ namespaceMapper = new \JNamespacePsr4Map; - } - - /** - * 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 - * - * @return boolean - * - * @since 3.0 - * - * @throws RuntimeException - */ - 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)) - { - return $this->applyWithCIServer($id); - } - - return $this->applyWithGitHub($id); - } - - /** - * Patches the code with the supplied pull request - * - * @param integer $id ID of the pull request to apply - * - * @return boolean - * - * @since 3.0 - * - * @throws RuntimeException - */ - private function applyWithCIServer(int $id): bool - { - // Get the CIServer Registry - $ciSettings = Helper::initializeCISettings(); - - // Get the Github object - $github = Helper::initializeGithub(); - - // Retrieve pullData for sha later on. - $pull = $this->retrieveGitHubData($github, $id); - - if ($pull->head->repo === null) - { - throw new RuntimeException(Text::_('COM_PATCHTESTER_REPO_IS_GONE')); - } - - $sha = $pull->head->sha; - - // Create tmp folder if it does not exist - if (!file_exists($ciSettings->get('folder.temp'))) - { - 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'); - - $serverZipPath = sprintf($ciSettings->get('zip.url'), $id); - - // Patch has already been applied - if (file_exists($backupsPath)) - { - return false; - } - - $version = new Version; - $httpOption = new Registry; - $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); - - // Try to download the zip file - try - { - $http = HttpFactory::getHttp($httpOption); - $result = $http->get($serverZipPath); - } - catch (RuntimeException $e) - { - $result = null; - } - - 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(); - - // 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')); - } - - Folder::create($tempPath); - - $zip = new Zip; - - if (!$zip->extract($zipPath, $tempPath)) - { - Folder::delete($tempPath); - throw new RuntimeException(Text::_('COM_PATCHTESTER_ZIP_EXTRACT_FAILED')); - } - - // Remove zip to avoid get listing afterwards - File::delete($zipPath); - - // Verify the composer autoloader for any invalid entries - 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')); - } - - // 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 - File::delete($delLogPath); - } - - // Retrieve all files and merge them into one array - $files = Folder::files($tempPath, null, true, true); - $files = str_replace(Path::clean($tempPath . '\\'), '', $files); - $files = array_merge($files, $deletedFiles); - - Folder::create($backupsPath); - - // Moves existent files to backup and replace them or creates new one if they don't exist - foreach ($files as $key => $file) - { - try - { - $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 - if (is_dir(JPATH_ROOT . '/' . $file)) - { - unset($files[$key]); - continue; - } - - if (file_exists(JPATH_ROOT . '/' . $file)) - { - // Create directories if they don't exist until file - if (!file_exists($backupsPath . '/' . $filePath)) - { - Folder::create($backupsPath . '/' . $filePath); - } - - 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)) - { - Folder::create(JPATH_ROOT . '/' . $filePath); - } - - 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()) - ); - } - } - - // Clear temp folder and store applied patch in database - Folder::delete($tempPath); - - $this->saveAppliedPatch($id, $files, $sha); - - // Update the autoloader file - $this->namespaceMapper->create(); - - // Change the media version - $version = new Version; - $version->refreshMediaVersion(); - - return true; - } - - /** - * Patches the code with the supplied pull request - * - * @param GitHub $github github object - * @param integer $id Id of the pull request - * - * @return stdClass The pull request data - * - * @since 2.0 - * - * @throws RuntimeException - */ - private function retrieveGitHubData(GitHub $github, int $id): stdClass - { - try - { - $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 - ); - } - - // 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)) - ); - } - - try - { - $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 - ); - } - - return $pull; - } - - /** - * Verify if the autoload contains any broken entries. - * - * @param string $path The path to look for the autoloader - * - * @return boolean True if there are broken dependencies | False otherwise. - * - * @since 4.0.0 - */ - private function verifyAutoloader(string $path): bool - { - $result = false; - - // Check if we have an autoload file - if (!file_exists($path . '/libraries/vendor/autoload.php')) - { - return $result; - } - - $composerFiles = [ - 'autoload_static.php', - 'autoload_files.php', - 'autoload_classmap.php', - 'autoload_namespaces.php', - '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) - { - return; - } - - // 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); - } - - /** - * Check a list of files if they exist. - * - * @param array $files The list of files to check - * @param string $path The path where the temporary patch resides - * - * @return boolean True if all files exist | False otherwise. - * - * @since 4.0.0 - */ - private function checkFilesExist(array $files, string $path): bool - { - $path = Path::clean($path); - - foreach ($files as $file) - { - if (is_array($file)) - { - $this->checkFilesExist($file, $path); - } - elseif (!file_exists($file)) - { - // Check if the file exists in the Joomla filesystem - $file = str_ireplace($path, JPATH_SITE, $file); - - if (!file_exists($file)) - { - return false; - } - } - } - - return true; - } - - /** - * Saves the applied patch into database - * - * @param integer $id ID of the applied pull request - * @param array $fileList List of files - * @param string $sha sha-key from pull request - * - * @return integer $id last inserted id - * - * @since 3.0 - */ - private function saveAppliedPatch(int $id, array $fileList, string $sha = null): int - { - $record = (object) array( - 'pull_id' => $id, - 'data' => json_encode($fileList), - 'patched_by' => Factory::getUser()->id, - 'applied' => 1, - 'applied_version' => JVERSION, - ); - - $db = $this->getDb(); - - $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) - ->update('#__patchtester_pulls') - ->set('sha = ' . $db->quote($sha)) - ->where($db->quoteName('pull_id') . ' = ' . $id) - )->execute(); - } - - return $insertId; - } - - /** - * Patches the code with the supplied pull request - * - * @param integer $id ID of the pull request to apply - * - * @return boolean - * - * @since 2.0 - * - * @throws RuntimeException - */ - private function applyWithGitHub(int $id): bool - { - $github = Helper::initializeGithub(); - - $pull = $this->retrieveGitHubData($github, $id); - - if ($pull->head->repo === null) - { - throw new RuntimeException(Text::_('COM_PATCHTESTER_REPO_IS_GONE')); - } - - 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 - ); - } - - if (!count($files)) - { - return false; - } - - $parsedFiles = $this->parseFileList($files); - - if (!count($parsedFiles)) - { - return false; - } - - foreach ($parsedFiles as $file) - { - 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) - ); - } - - break; - - case 'added': - 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->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); - - // In case encoding type ever changes - switch ($contents->encoding) - { - case 'base64': - $file->body = base64_decode($contents->content); - - break; - - default: - 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 - ); - } - - break; - } - } - - // At this point, we have ensured that we have all the new files and there are no conflicts - foreach ($parsedFiles as $file) - { - // 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')) - { - $filename = $file->action === 'renamed' ? $file->originalFile : $file->filename; - $src = JPATH_ROOT . '/' . $filename; - $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)); - } - } - - 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) - ); - } - - 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 - ) - ); - } - - 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::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; - } - - // We don't need the file's body any longer (and it causes issues with binary data when json_encode() is run), so remove it - unset($file->body); - } - - $this->saveAppliedPatch($pull->number, $parsedFiles, $pull->head->sha); - - // Update the autoloader file - $this->namespaceMapper->create(); - - // Change the media version - $version = new Version; - $version->refreshMediaVersion(); - - return true; - } - - /** - * Get all files from Github. - * - * @param int $id The pull ID - * @param int $page THhe page umber to process - * @param array $files The list of files retrieved from Github - * - * @return array LIst of files to process. - * - * @since 4.2.0 - */ - 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; - - preg_match( - '/(\?page=[0-9]{1,3}>; rel=\"last\")/', $filesResponse->getHeaders()['link'][0], $matches - ); - - if ($matches && isset($matches[0])) - { - preg_match('/\d+/', $matches[0], $pages); - $lastPage = (int) $pages[0]; - } - - if ($page <= $lastPage) - { - $files = $this->getFiles($id, ++$page, $files); - } - - return $files; - } - - /** - * Parse the list of modified files from a pull request - * - * @param stdClass $files The modified files to parse - * - * @return array - * - * @since 3.0.0 - */ - 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 - */ - $isDev = file_exists(JPATH_INSTALLATION . '/index.php'); - - foreach ($files as $file) - { - if (!$isDev) - { - $filePath = explode('/', $file->filename); - - if (in_array($filePath[0], $this->nonProductionFiles, true)) - { - continue; - } - - if (in_array($filePath[0], $this->nonProductionFolders, true)) - { - continue; - } - } - - // Sometimes the repo filename is not the production file name - $prodFileName = $file->filename; - $prodRenamedFileName = $file->previous_filename ?? false; - $filePath = explode('/', $prodFileName); - - // 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 - if ($filePath[0] === 'src') - { - $prodRenamedFileName = str_replace('src/', '', $prodRenamedFileName); - } - } - - $parsedFiles[] = (object) array( - 'action' => $file->status, - 'filename' => $prodFileName, - 'repofilename' => $file->filename, - 'fileurl' => $file->contents_url, - 'originalFile' => $prodRenamedFileName, - ); - } - - return $parsedFiles; - } - - /** - * Reverts the specified pull request - * However uses different methods for different repositories. - * - * @param integer $id ID of the pull request to revert - * - * @return boolean - * - * @since 3.0 - * @throws RuntimeException - */ - 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)) - { - return $this->revertWithCIServer($id); - } - - return $this->revertWithGitHub($id); - } - - /** - * Reverts the specified pull request with CIServer options - * - * @param integer $id ID of the pull request to revert - * - * @return boolean - * - * @since 3.0 - * @throws RuntimeException - */ - public function revertWithCIServer(int $id): bool - { - // Get the CIServer Registry - $ciSettings = Helper::initializeCISettings(); - - $testRecord = $this->getTestRecord($id); - - // We don't want to restore files from an older version - if ($testRecord->applied_version !== JVERSION) - { - return $this->removeTest($testRecord); - } - - $files = json_decode($testRecord->data, false); - - if (!$files) - { - throw new RuntimeException( - Text::sprintf( - 'COM_PATCHTESTER_ERROR_READING_DATABASE_TABLE', - __METHOD__, - htmlentities($testRecord->data) - ) - ); - } - - $backupsPath = $ciSettings->get('folder.backups') . "/$testRecord->pull_id"; - - foreach ($files as $file) - { - try - { - $filePath = explode("\\", $file); - array_pop($filePath); - $filePath = implode("\\", $filePath); - - // Delete file from root of it exists - if (file_Exists(JPATH_ROOT . "/$file")) - { - File::delete(JPATH_ROOT . "/$file"); - - // Move from backup, if it exists there - if (file_exists($backupsPath . '/' . $file)) - { - File::move($backupsPath . '/' . $file, JPATH_ROOT . '/' . $file); - } - - // If folder is empty, remove it as well - if (count(glob(JPATH_ROOT . '/' . $filePath . '/*')) === 0) - { - Folder::delete(JPATH_ROOT . '/' . $filePath); - } - } - // Move from backup, if file exists there - got deleted by patch - elseif (file_exists($backupsPath . '/' . $file)) - { - if (!file_exists(JPATH_ROOT . '/' . $filePath)) - { - Folder::create(JPATH_ROOT . '/' . $filePath); - } - - File::move($backupsPath . '/' . $file, JPATH_ROOT . '/' . $file); - } - } - catch (RuntimeException $exception) - { - throw new RuntimeException( - Text::sprintf('COM_PATCHTESTER_FAILED_REVERT_PATCH', $file, $exception->getMessage()) - ); - } - } - - Folder::delete($backupsPath); - - // Update the autoloader file - $this->namespaceMapper->create(); - - // Change the media version - $version = new Version; - $version->refreshMediaVersion(); - - return $this->removeTest($testRecord); - } - - /** - * Retrieves test data from database by specific id - * - * @param integer $id ID of the record - * - * @return stdClass $testRecord The record looking for - * - * @since 3.0.0 - */ - private function getTestRecord(int $id): stdClass - { - $db = $this->getDb(); - - return $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from('#__patchtester_tests') - ->where('id = ' . (int) $id) - )->loadObject(); - } - - /** - * Remove the database record for a test - * - * @param stdClass $testRecord The record being deleted - * - * @return boolean - * - * @since 3.0.0 - */ - 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) - ->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) - ->delete('#__patchtester_tests') - ->where('id = ' . (int) $testRecord->id) - )->execute(); - - return true; - } - - /** - * Reverts the specified pull request with Github Requests - * - * @param integer $id ID of the pull request to revert - * - * @return boolean - * - * @since 2.0 - * @throws RuntimeException - */ - public function revertWithGitHub(int $id): bool - { - $testRecord = $this->getTestRecord($id); - - // We don't want to restore files from an older version - if ($testRecord->applied_version != JVERSION) - { - return $this->removeTest($testRecord); - } - - $files = json_decode($testRecord->data, false); - - if (!$files) - { - 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'; - $dest = JPATH_ROOT . '/' . $file->filename; - - if (!File::copy($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) - ); - } - } - - break; - - case 'added': - $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) - ); - } - } - - break; - - case 'renamed': - $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) - ); - } - - if (file_exists($originalSrc)) - { - if (!File::delete($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) - ); - } - } - - break; - } - } - - // Update the autoloader file - $this->namespaceMapper->create(); - - // Change the media version - $version = new Version; - $version->refreshMediaVersion(); - - return $this->removeTest($testRecord); - } + /** + * Array containing top level non-production folders + * + * @var array + * @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', + '.travis.yml', + 'README.md', + 'build.xml', + 'composer.json', + 'composer.lock', + 'phpunit.xml.dist', + 'robots.txt.dist', + 'travisci-phpunit.xml', + 'LICENSE', + 'RoboFile.dist.ini', + 'RoboFile.php', + 'codeception.yml', + '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 \JDatabaseDriver $db The database adpater. + * + * @since 4.0.0 + */ + public function __construct(Registry $state = null, \JDatabaseDriver $db = null) + { + parent::__construct($state, $db); + $this->namespaceMapper = new \JNamespacePsr4Map(); + } + + /** + * 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 + * + * @return boolean + * + * @since 3.0 + * + * @throws RuntimeException + */ + 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)) { + return $this->applyWithCIServer($id); + } + + return $this->applyWithGitHub($id); + } + + /** + * Patches the code with the supplied pull request + * + * @param integer $id ID of the pull request to apply + * + * @return boolean + * + * @since 3.0 + * + * @throws RuntimeException + */ + private function applyWithCIServer(int $id): bool + { + // Get the CIServer Registry + $ciSettings = Helper::initializeCISettings(); +// Get the Github object + $github = Helper::initializeGithub(); +// Retrieve pullData for sha later on. + $pull = $this->retrieveGitHubData($github, $id); + if ($pull->head->repo === null) { + throw new RuntimeException(Text::_('COM_PATCHTESTER_REPO_IS_GONE')); + } + + $sha = $pull->head->sha; +// Create tmp folder if it does not exist + if (!file_exists($ciSettings->get('folder.temp'))) { + 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'); + $serverZipPath = sprintf($ciSettings->get('zip.url'), $id); +// Patch has already been applied + if (file_exists($backupsPath)) { + return false; + } + + $version = new Version(); + $httpOption = new Registry(); + $httpOption->set('userAgent', $version->getUserAgent('Joomla', true, false)); +// Try to download the zip file + try { + $http = HttpFactory::getHttp($httpOption); + $result = $http->get($serverZipPath); + } catch (RuntimeException $e) { + $result = null; + } + + 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(); +// 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')); + } + + Folder::create($tempPath); + $zip = new Zip(); + if (!$zip->extract($zipPath, $tempPath)) { + Folder::delete($tempPath); + throw new RuntimeException(Text::_('COM_PATCHTESTER_ZIP_EXTRACT_FAILED')); + } + + // Remove zip to avoid get listing afterwards + File::delete($zipPath); +// Verify the composer autoloader for any invalid entries + 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')); + } + + // 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 + File::delete($delLogPath); + } + + // Retrieve all files and merge them into one array + $files = Folder::files($tempPath, null, true, true); + $files = str_replace(Path::clean($tempPath . '\\'), '', $files); + $files = array_merge($files, $deletedFiles); + Folder::create($backupsPath); +// Moves existent files to backup and replace them or creates new one if they don't exist + foreach ($files as $key => $file) { + try { + $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 + if (is_dir(JPATH_ROOT . '/' . $file)) { + unset($files[$key]); + continue; + } + + if (file_exists(JPATH_ROOT . '/' . $file)) { +// Create directories if they don't exist until file + if (!file_exists($backupsPath . '/' . $filePath)) { + Folder::create($backupsPath . '/' . $filePath); + } + + 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)) { + Folder::create(JPATH_ROOT . '/' . $filePath); + } + + 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())); + } + } + + // Clear temp folder and store applied patch in database + Folder::delete($tempPath); + $this->saveAppliedPatch($id, $files, $sha); +// Update the autoloader file + $this->namespaceMapper->create(); +// Change the media version + $version = new Version(); + $version->refreshMediaVersion(); + return true; + } + + /** + * Patches the code with the supplied pull request + * + * @param GitHub $github github object + * @param integer $id Id of the pull request + * + * @return stdClass The pull request data + * + * @since 2.0 + * + * @throws RuntimeException + */ + private function retrieveGitHubData(GitHub $github, int $id): stdClass + { + try { + $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); + } + + // 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))); + } + + try { + $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); + } + + return $pull; + } + + /** + * Verify if the autoload contains any broken entries. + * + * @param string $path The path to look for the autoloader + * + * @return boolean True if there are broken dependencies | False otherwise. + * + * @since 4.0.0 + */ + private function verifyAutoloader(string $path): bool + { + $result = false; +// Check if we have an autoload file + if (!file_exists($path . '/libraries/vendor/autoload.php')) { + return $result; + } + + $composerFiles = [ + 'autoload_static.php', + 'autoload_files.php', + 'autoload_classmap.php', + 'autoload_namespaces.php', + '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) { + return; + } + + // 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); + } + + /** + * Check a list of files if they exist. + * + * @param array $files The list of files to check + * @param string $path The path where the temporary patch resides + * + * @return boolean True if all files exist | False otherwise. + * + * @since 4.0.0 + */ + private function checkFilesExist(array $files, string $path): bool + { + $path = Path::clean($path); + foreach ($files as $file) { + if (is_array($file)) { + $this->checkFilesExist($file, $path); + } elseif (!file_exists($file)) { + // Check if the file exists in the Joomla filesystem + $file = str_ireplace($path, JPATH_SITE, $file); + if (!file_exists($file)) { + return false; + } + } + } + + return true; + } + + /** + * Saves the applied patch into database + * + * @param integer $id ID of the applied pull request + * @param array $fileList List of files + * @param string $sha sha-key from pull request + * + * @return integer $id last inserted id + * + * @since 3.0 + */ + private function saveAppliedPatch(int $id, array $fileList, string $sha = null): int + { + $record = (object) array( + 'pull_id' => $id, + 'data' => json_encode($fileList), + 'patched_by' => Factory::getUser()->id, + 'applied' => 1, + 'applied_version' => JVERSION, + ); + $db = $this->getDb(); + $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) + ->update('#__patchtester_pulls') + ->set('sha = ' . $db->quote($sha)) + ->where($db->quoteName('pull_id') . ' = ' . $id))->execute(); + } + + return $insertId; + } + + /** + * Patches the code with the supplied pull request + * + * @param integer $id ID of the pull request to apply + * + * @return boolean + * + * @since 2.0 + * + * @throws RuntimeException + */ + private function applyWithGitHub(int $id): bool + { + $github = Helper::initializeGithub(); + $pull = $this->retrieveGitHubData($github, $id); + if ($pull->head->repo === null) { + throw new RuntimeException(Text::_('COM_PATCHTESTER_REPO_IS_GONE')); + } + + 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); + } + + if (!count($files)) { + return false; + } + + $parsedFiles = $this->parseFileList($files); + if (!count($parsedFiles)) { + return false; + } + + foreach ($parsedFiles as $file) { + 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)); + } + + + break; + case 'added': + 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->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); + // In case encoding type ever changes + switch ($contents->encoding) { + case 'base64': + $file->body = base64_decode($contents->content); + + break; + default: + 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); + } + + break; + } + } + + // At this point, we have ensured that we have all the new files and there are no conflicts + foreach ($parsedFiles as $file) { + // 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') + ) { + $filename = $file->action === 'renamed' ? $file->originalFile : $file->filename; + $src = JPATH_ROOT . '/' . $filename; + $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)); + } + } + + 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)); + } + + + 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)); + } + + + 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::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; + } + + // We don't need the file's body any longer (and it causes issues with binary data when json_encode() is run), so remove it + unset($file->body); + } + + $this->saveAppliedPatch($pull->number, $parsedFiles, $pull->head->sha); + // Update the autoloader file + $this->namespaceMapper->create(); + // Change the media version + $version = new Version(); + $version->refreshMediaVersion(); + return true; + } + + /** + * Get all files from Github. + * + * @param int $id The pull ID + * @param int $page THhe page umber to process + * @param array $files The list of files retrieved from Github + * + * @return array LIst of files to process. + * + * @since 4.2.0 + */ + 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; + + preg_match( + '/(\?page=[0-9]{1,3}>; rel=\"last\")/', + $filesResponse->getHeaders()['link'][0], + $matches + ); + if ($matches && isset($matches[0])) { + preg_match('/\d+/', $matches[0], $pages); + $lastPage = (int) $pages[0]; + } + + if ($page <= $lastPage) { + $files = $this->getFiles($id, ++$page, $files); + } + + return $files; + } + + /** + * Parse the list of modified files from a pull request + * + * @param stdClass $files The modified files to parse + * + * @return array + * + * @since 3.0.0 + */ + 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 + */ + $isDev = file_exists(JPATH_INSTALLATION . '/index.php'); + foreach ($files as $file) { + if (!$isDev) { + $filePath = explode('/', $file->filename); + if (in_array($filePath[0], $this->nonProductionFiles, true)) { + continue; + } + + if (in_array($filePath[0], $this->nonProductionFolders, true)) { + continue; + } + } + + // Sometimes the repo filename is not the production file name + $prodFileName = $file->filename; + $prodRenamedFileName = $file->previous_filename ?? false; + $filePath = explode('/', $prodFileName); + // 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 + if ($filePath[0] === 'src') { + $prodRenamedFileName = str_replace('src/', '', $prodRenamedFileName); + } + } + + $parsedFiles[] = (object) array( + 'action' => $file->status, + 'filename' => $prodFileName, + 'repofilename' => $file->filename, + 'fileurl' => $file->contents_url, + 'originalFile' => $prodRenamedFileName, + ); + } + + return $parsedFiles; + } + + /** + * Reverts the specified pull request + * However uses different methods for different repositories. + * + * @param integer $id ID of the pull request to revert + * + * @return boolean + * + * @since 3.0 + * @throws RuntimeException + */ + 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)) { + return $this->revertWithCIServer($id); + } + + return $this->revertWithGitHub($id); + } + + /** + * Reverts the specified pull request with CIServer options + * + * @param integer $id ID of the pull request to revert + * + * @return boolean + * + * @since 3.0 + * @throws RuntimeException + */ + public function revertWithCIServer(int $id): bool + { + // Get the CIServer Registry + $ciSettings = Helper::initializeCISettings(); + $testRecord = $this->getTestRecord($id); +// We don't want to restore files from an older version + if ($testRecord->applied_version !== JVERSION) { + return $this->removeTest($testRecord); + } + + $files = json_decode($testRecord->data, false); + if (!$files) { + throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_READING_DATABASE_TABLE', __METHOD__, htmlentities($testRecord->data))); + } + + $backupsPath = $ciSettings->get('folder.backups') . "/$testRecord->pull_id"; + foreach ($files as $file) { + try { + $filePath = explode("\\", $file); + array_pop($filePath); + $filePath = implode("\\", $filePath); + // Delete file from root of it exists + if (file_Exists(JPATH_ROOT . "/$file")) { + File::delete(JPATH_ROOT . "/$file"); + // Move from backup, if it exists there + if (file_exists($backupsPath . '/' . $file)) { + File::move($backupsPath . '/' . $file, JPATH_ROOT . '/' . $file); + } + + // If folder is empty, remove it as well + if (count(glob(JPATH_ROOT . '/' . $filePath . '/*')) === 0) { + Folder::delete(JPATH_ROOT . '/' . $filePath); + } + } elseif (file_exists($backupsPath . '/' . $file)) { + // Move from backup, if file exists there - got deleted by patch + if (!file_exists(JPATH_ROOT . '/' . $filePath)) { + Folder::create(JPATH_ROOT . '/' . $filePath); + } + + File::move($backupsPath . '/' . $file, JPATH_ROOT . '/' . $file); + } + } catch (RuntimeException $exception) { + throw new RuntimeException(Text::sprintf('COM_PATCHTESTER_FAILED_REVERT_PATCH', $file, $exception->getMessage())); + } + } + + Folder::delete($backupsPath); +// Update the autoloader file + $this->namespaceMapper->create(); +// Change the media version + $version = new Version(); + $version->refreshMediaVersion(); + return $this->removeTest($testRecord); + } + + /** + * Retrieves test data from database by specific id + * + * @param integer $id ID of the record + * + * @return stdClass $testRecord The record looking for + * + * @since 3.0.0 + */ + private function getTestRecord(int $id): stdClass + { + $db = $this->getDb(); + return $db->setQuery($db->getQuery(true) + ->select('*') + ->from('#__patchtester_tests') + ->where('id = ' . (int) $id))->loadObject(); + } + + /** + * Remove the database record for a test + * + * @param stdClass $testRecord The record being deleted + * + * @return boolean + * + * @since 3.0.0 + */ + 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) + ->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) + ->delete('#__patchtester_tests') + ->where('id = ' . (int) $testRecord->id))->execute(); + return true; + } + + /** + * Reverts the specified pull request with Github Requests + * + * @param integer $id ID of the pull request to revert + * + * @return boolean + * + * @since 2.0 + * @throws RuntimeException + */ + public function revertWithGitHub(int $id): bool + { + $testRecord = $this->getTestRecord($id); +// We don't want to restore files from an older version + if ($testRecord->applied_version != JVERSION) { + return $this->removeTest($testRecord); + } + + $files = json_decode($testRecord->data, false); + if (!$files) { + 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'; + $dest = JPATH_ROOT . '/' . $file->filename; + if (!File::copy($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)); + } + } + + + break; + case 'added': + $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)); + } + } + + break; + case 'renamed': + $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)); + } + + if (file_exists($originalSrc)) { + if (!File::delete($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)); + } + } + + + break; + } + } + + // Update the autoloader file + $this->namespaceMapper->create(); +// Change the media version + $version = new Version(); + $version->refreshMediaVersion(); + return $this->removeTest($testRecord); + } } diff --git a/administrator/components/com_patchtester/PatchTester/Model/PullsModel.php b/administrator/components/com_patchtester/PatchTester/Model/PullsModel.php index de82ebe..be7c5f9 100644 --- a/administrator/components/com_patchtester/PatchTester/Model/PullsModel.php +++ b/administrator/components/com_patchtester/PatchTester/Model/PullsModel.php @@ -1,4 +1,5 @@ getStoreId(); - - if (isset($this->cache[$store])) - { - return $this->cache[$store]; - } - - $items = $this->getList( - $this->getListQueryCache(), $this->getStart(), - $this->getState()->get('list.limit') - ); - - $db = $this->getDbo(); - $query = $db->getQuery(true) - ->select($db->quoteName(['name', 'color'])) - ->from($db->quoteName('#__patchtester_pulls_labels')); - - array_walk( - $items, - static function ($item) use ($db, $query) { - $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]; - } - - /** - * Method to get a store id based on the model configuration state. - * - * This is necessary because the model is used by the component and - * different modules that might need different sets of data or different - * ordering requirements. - * - * @param string $id An identifier string to generate the store id. - * - * @return string A store id. - * - * @since 2.0 - */ - 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); - } - - /** - * Gets an array of objects from the results of database query. - * - * @param \JDatabaseQuery|string $query The query. - * @param integer $limitstart Offset. - * @param integer $limit The number of records. - * - * @return array An array of results. - * - * @since 2.0 - * @throws RuntimeException - */ - protected function getList($query, $limitstart = 0, $limit = 0) - { - return $this->getDbo()->setQuery($query, $limitstart, $limit) - ->loadObjectList(); - } - - /** - * Method to cache the last query constructed. - * - * This method ensures that the query is constructed only once for a given state of the model. - * - * @return \JDatabaseQuery A JDatabaseQuery object - * - * @since 2.0 - */ - protected function getListQueryCache() - { - // Capture the last store id used. - static $lastStoreId; - - // Compute the current store id. - $currentStoreId = $this->getStoreId(); - - // If the last store id is different from the current, refresh the query. - if ($lastStoreId != $currentStoreId || empty($this->query)) - { - $lastStoreId = $currentStoreId; - $this->query = $this->getListQuery(); - } - - return $this->query; - } - - /** - * Method to get a JDatabaseQuery object for retrieving the data set from a database. - * - * @return \JDatabaseQuery A JDatabaseQuery object to retrieve the data set. - * - * @since 2.0 - */ - protected function getListQuery() - { - $db = $this->getDbo(); - $query = $db->getQuery(true); - $labelQuery = $db->getQuery(true); - - $query->select('pulls.*') - ->select($db->quoteName('tests.id', 'applied')) - ->from($db->quoteName('#__patchtester_pulls', 'pulls')) - ->leftJoin( - $db->quoteName('#__patchtester_tests', 'tests') - . ' ON ' . $db->quoteName('tests.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 - ) - ); - } - elseif (is_numeric($search)) - { - $query->where( - $db->quoteName('pulls.pull_id') . ' = ' . (int) $search - ); - } - else - { - $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 - $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)) . ')' - ); - } - - $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 - $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 - $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 - $value = $draft === 'no' ? '0' : '1'; - - $query->where($db->quoteName('pulls.is_draft') . ' = ' . $value); - } - - $labels = $this->getState()->get('filter.label'); - - 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) - ) . ')' - ) - ->group($db->quoteName('pulls_labels.pull_id')); - - $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 - ) - ); - } - - $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) - ); - } - - return $query; - } - - /** - * Retrieves the array of authorized sort fields - * - * @return array - * - * @since 2.0 - */ - public function getSortFields() - { - return $this->sortFields; - } - - /** - * Method to request new data from GitHub - * - * @param integer $page The page of the request - * - * @return array - * - * @since 2.0 - * @throws \RuntimeException - */ - public function requestFromGithub($page) - { - if ($page === 1) - { - $this->getDbo()->truncateTable('#__patchtester_pulls'); - $this->getDbo()->truncateTable('#__patchtester_pulls_labels'); - } - - 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); - } - catch (UnexpectedResponse $exception) - { - throw new \RuntimeException( - Text::sprintf( - 'COM_PATCHTESTER_ERROR_GITHUB_FETCH', - $exception->getMessage() - ), - $exception->getCode(), - $exception - ); - } - - // If this is page 1, let's check to see if we need to paginate - if ($page === 1) - { - // Default this to being a single page of results - $lastPage = 1; - - if (isset($pullsResponse->headers['link'])) - { - $linkHeader = $pullsResponse->headers['link']; - - // The `joomla/http` 2.0 package uses PSR-7 Responses which has a different format for headers, check for this - if (is_array($linkHeader)) - { - $linkHeader = $linkHeader[0]; - } - - preg_match( - '/(\?page=[0-9]{1,3}&per_page=' . $batchSize - . '+>; rel=\"last\")/', $linkHeader, $matches - ); - - if ($matches && isset($matches[0])) - { - $pageSegment = str_replace( - '&per_page=' . $batchSize, '', $matches[0] - ); - - preg_match('/\d+/', $pageSegment, $pages); - $lastPage = (int) $pages[0]; - } - } - } - - // If there are no pulls to insert then bail, assume we're finished - if (count($pulls) === 0) - { - return ['complete' => true]; - } - - $data = []; - $labels = []; - - foreach ($pulls as $pull) - { - // Check if this PR is RTC and has a `PR-` branch label - $isRTC = false; - $isNPM = false; - $branch = $pull->base->ref; - - foreach ($pull->labels as $label) - { - if (strtolower($label->name) === 'rtc') - { - $isRTC = true; - } - elseif (in_array( - strtolower($label->name), - ['npm resource changed', 'composer dependency changed'], - true - )) - { - $isNPM = true; - } - - $labels[] = implode( - ',', - [ - (int) $pull->number, - $this->getDbo()->quote($label->name), - $this->getDbo()->quote($label->color), - ] - ); - } - - // Build the data object to store in the database - $pullData = [ - (int) $pull->number, - $this->getDbo()->quote( - HTMLHelper::_('string.truncate', $pull->title, 150) - ), - $this->getDbo()->quote( - HTMLHelper::_('string.truncate', $pull->body, 100) - ), - $this->getDbo()->quote($pull->html_url), - (int) $isRTC, - (int) $isNPM, - $this->getDbo()->quote($branch), - ($pull->draft ? 1 : 0) - ]; - - $data[] = implode(',', $pullData); - } - - // If there are no pulls to insert then bail, assume we're finished - if (count($data) === 0) - { - return array('complete' => true); - } - - try - { - $this->getDbo()->setQuery( - $this->getDbo()->getQuery(true) - ->insert('#__patchtester_pulls') - ->columns( - ['pull_id', 'title', 'description', 'pull_url', - 'is_rtc', 'is_npm', 'branch', 'is_draft'] - ) - ->values($data) - ); - - $this->getDbo()->execute(); - } - catch (\RuntimeException $exception) - { - throw new \RuntimeException( - Text::sprintf( - 'COM_PATCHTESTER_ERROR_INSERT_DATABASE', - $exception->getMessage() - ), - $exception->getCode(), - $exception - ); - } - - if ($labels) - { - try - { - $this->getDbo()->setQuery( - $this->getDbo()->getQuery(true) - ->insert('#__patchtester_pulls_labels') - ->columns(['pull_id', 'name', 'color']) - ->values($labels) - ); - $this->getDbo()->execute(); - } - catch (\RuntimeException $exception) - { - throw new \RuntimeException( - Text::sprintf( - 'COM_PATCHTESTER_ERROR_INSERT_DATABASE', - $exception->getMessage() - ), - $exception->getCode(), - $exception - ); - } - } - - return [ - 'complete' => false, - 'page' => ($page + 1), - 'lastPage' => $lastPage ?? false, - ]; - } - - /** - * Truncates the pulls table - * - * @return void - * - * @since 2.0 - */ - public function truncateTable() - { - $this->getDbo()->truncateTable('#__patchtester_pulls'); - } + /** + * The object context + * + * @var string + * @since 2.0 + */ + protected $context; +/** + * Array of fields the list can be sorted on + * + * @var array + * @since 2.0 + */ + protected $sortFields = array('pulls.pull_id', 'pulls.title'); +/** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 4.0.0 + * @throws Exception + * + */ + public function __construct($config = []) + { + $config = []; + if (empty($config['filter_fields'])) { + $config['filter_fields'] = [ + 'applied', + 'rtc', + 'npm', + 'draft', + 'label', + 'branch', + ]; + } + + parent::__construct($config); + } + + /** + * Method to get an array of data items. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 2.0 + */ + public function getItems() + { + $store = $this->getStoreId(); + if (isset($this->cache[$store])) { + return $this->cache[$store]; + } + + $items = $this->getList( + $this->getListQueryCache(), + $this->getStart(), + $this->getState()->get('list.limit') + ); + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->select($db->quoteName(['name', 'color'])) + ->from($db->quoteName('#__patchtester_pulls_labels')); + array_walk($items, static function ($item) use ($db, $query) { + + $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]; + } + + /** + * Method to get a store id based on the model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id An identifier string to generate the store id. + * + * @return string A store id. + * + * @since 2.0 + */ + 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); + } + + /** + * Gets an array of objects from the results of database query. + * + * @param \JDatabaseQuery|string $query The query. + * @param integer $limitstart Offset. + * @param integer $limit The number of records. + * + * @return array An array of results. + * + * @since 2.0 + * @throws RuntimeException + */ + protected function getList($query, $limitstart = 0, $limit = 0) + { + return $this->getDbo()->setQuery($query, $limitstart, $limit) + ->loadObjectList(); + } + + /** + * Method to cache the last query constructed. + * + * This method ensures that the query is constructed only once for a given state of the model. + * + * @return \JDatabaseQuery A JDatabaseQuery object + * + * @since 2.0 + */ + protected function getListQueryCache() + { + // Capture the last store id used. + static $lastStoreId; +// Compute the current store id. + $currentStoreId = $this->getStoreId(); +// If the last store id is different from the current, refresh the query. + if ($lastStoreId != $currentStoreId || empty($this->query)) { + $lastStoreId = $currentStoreId; + $this->query = $this->getListQuery(); + } + + return $this->query; + } + + /** + * Method to get a JDatabaseQuery object for retrieving the data set from a database. + * + * @return \JDatabaseQuery A JDatabaseQuery object to retrieve the data set. + * + * @since 2.0 + */ + protected function getListQuery() + { + $db = $this->getDbo(); + $query = $db->getQuery(true); + $labelQuery = $db->getQuery(true); + $query->select('pulls.*') + ->select($db->quoteName('tests.id', 'applied')) + ->from($db->quoteName('#__patchtester_pulls', 'pulls')) + ->leftJoin($db->quoteName('#__patchtester_tests', 'tests') + . ' ON ' . $db->quoteName('tests.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 + )); + } elseif (is_numeric($search)) { + $query->where($db->quoteName('pulls.pull_id') . ' = ' . (int) $search); + } else { + $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 + $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)) . ')'); + } + + $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 + $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 + $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 + $value = $draft === 'no' ? '0' : '1'; + $query->where($db->quoteName('pulls.is_draft') . ' = ' . $value); + } + + $labels = $this->getState()->get('filter.label'); + + 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) + ) . ')') + ->group($db->quoteName('pulls_labels.pull_id')); + $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)); + } + + $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)); + } + + return $query; + } + + /** + * Retrieves the array of authorized sort fields + * + * @return array + * + * @since 2.0 + */ + public function getSortFields() + { + return $this->sortFields; + } + + /** + * Method to request new data from GitHub + * + * @param integer $page The page of the request + * + * @return array + * + * @since 2.0 + * @throws \RuntimeException + */ + public function requestFromGithub($page) + { + if ($page === 1) { + $this->getDbo()->truncateTable('#__patchtester_pulls'); + $this->getDbo()->truncateTable('#__patchtester_pulls_labels'); + } + + 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); + } catch (UnexpectedResponse $exception) { + throw new \RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_GITHUB_FETCH', $exception->getMessage()), $exception->getCode(), $exception); + } + + // If this is page 1, let's check to see if we need to paginate + if ($page === 1) { +// Default this to being a single page of results + $lastPage = 1; + if (isset($pullsResponse->headers['link'])) { + $linkHeader = $pullsResponse->headers['link']; + // The `joomla/http` 2.0 package uses PSR-7 Responses which has a different format for headers, check for this + if (is_array($linkHeader)) { + $linkHeader = $linkHeader[0]; + } + + preg_match( + '/(\?page=[0-9]{1,3}&per_page=' . $batchSize + . '+>; rel=\"last\")/', + $linkHeader, + $matches + ); + + if ($matches && isset($matches[0])) { + $pageSegment = str_replace( + '&per_page=' . $batchSize, + '', + $matches[0] + ); + preg_match('/\d+/', $pageSegment, $pages); + $lastPage = (int) $pages[0]; + } + } + } + + // If there are no pulls to insert then bail, assume we're finished + if (count($pulls) === 0) { + return ['complete' => true]; + } + + $data = []; + $labels = []; + foreach ($pulls as $pull) { + // Check if this PR is RTC and has a `PR-` branch label + $isRTC = false; + $isNPM = false; + $branch = $pull->base->ref; + foreach ($pull->labels as $label) { + if (strtolower($label->name) === 'rtc') { + $isRTC = true; + } elseif ( + in_array(strtolower($label->name), ['npm resource changed', 'composer dependency changed'], true) + ) { + $isNPM = true; + } + + $labels[] = implode(',', [ + (int) $pull->number, + $this->getDbo()->quote($label->name), + $this->getDbo()->quote($label->color), + ]); + } + + // Build the data object to store in the database + $pullData = [ + (int) $pull->number, + $this->getDbo()->quote(HTMLHelper::_('string.truncate', $pull->title, 150)), + $this->getDbo()->quote(HTMLHelper::_('string.truncate', $pull->body, 100)), + $this->getDbo()->quote($pull->html_url), + (int) $isRTC, + (int) $isNPM, + $this->getDbo()->quote($branch), + ($pull->draft ? 1 : 0) + ]; + $data[] = implode(',', $pullData); + } + + // If there are no pulls to insert then bail, assume we're finished + if (count($data) === 0) { + return array('complete' => true); + } + + try { + $this->getDbo()->setQuery($this->getDbo()->getQuery(true) + ->insert('#__patchtester_pulls') + ->columns(['pull_id', 'title', 'description', 'pull_url', + 'is_rtc', 'is_npm', 'branch', 'is_draft']) + ->values($data)); + $this->getDbo()->execute(); + } catch (\RuntimeException $exception) { + throw new \RuntimeException(Text::sprintf('COM_PATCHTESTER_ERROR_INSERT_DATABASE', $exception->getMessage()), $exception->getCode(), $exception); + } + + if ($labels) { + try { + $this->getDbo()->setQuery($this->getDbo()->getQuery(true) + ->insert('#__patchtester_pulls_labels') + ->columns(['pull_id', 'name', 'color']) + ->values($labels)); + $this->getDbo()->execute(); + } catch (\RuntimeException $exception) { + throw new \RuntimeException( + Text::sprintf( + 'COM_PATCHTESTER_ERROR_INSERT_DATABASE', + $exception->getMessage() + ), + $exception->getCode(), + $exception + ); + } + } + + return [ + 'complete' => false, + 'page' => ($page + 1), + 'lastPage' => $lastPage ?? false, + ]; + } + + /** + * Truncates the pulls table + * + * @return void + * + * @since 2.0 + */ + public function truncateTable() + { + $this->getDbo()->truncateTable('#__patchtester_pulls'); + } } diff --git a/administrator/components/com_patchtester/PatchTester/Model/TestsModel.php b/administrator/components/com_patchtester/PatchTester/Model/TestsModel.php index eb6bedc..9946d30 100644 --- a/administrator/components/com_patchtester/PatchTester/Model/TestsModel.php +++ b/administrator/components/com_patchtester/PatchTester/Model/TestsModel.php @@ -1,4 +1,5 @@ getDb(); + /** + * Retrieves a list of applied patches + * + * @return array List of applied patches + * + * @since 2.0 + */ + public function getAppliedPatches(): array + { + $db = $this->getDb(); + $db->setQuery($db->getQuery(true) + ->select('*') + ->from($db->quoteName('#__patchtester_tests')) + ->where($db->quoteName('applied') . ' = 1')); + return $db->loadObjectList('pull_id'); + } - $db->setQuery( - $db->getQuery(true) - ->select('*') - ->from($db->quoteName('#__patchtester_tests')) - ->where($db->quoteName('applied') . ' = 1') - ); - - return $db->loadObjectList('pull_id'); - } - - /** - * Truncates the tests table - * - * @return void - * - * @since 2.0 - */ - public function truncateTable(): void - { - $this->getDb()->truncateTable('#__patchtester_tests'); - } + /** + * Truncates the tests table + * + * @return void + * + * @since 2.0 + */ + public function truncateTable(): void + { + $this->getDb()->truncateTable('#__patchtester_tests'); + } } diff --git a/administrator/components/com_patchtester/PatchTester/TrackerHelper.php b/administrator/components/com_patchtester/PatchTester/TrackerHelper.php index 68d5335..6b557f0 100644 --- a/administrator/components/com_patchtester/PatchTester/TrackerHelper.php +++ b/administrator/components/com_patchtester/PatchTester/TrackerHelper.php @@ -1,4 +1,5 @@ array( - 'githubUser' => 'joomla', - 'githubrepo' => 'joomla-cms', - 'projectAlias' => 'joomla-cms', - ), - 'patchtester' => array( - 'githubUser' => 'joomla-extensions', - 'githubrepo' => 'patchtester', - 'projectAlias' => 'patchtester', - ), - 'weblinks' => array( - 'githubUser' => 'joomla-extensions', - 'githubrepo' => 'weblinks', - 'projectAlias' => 'weblinks', - ), - ); + /** + * Array containing the supported repositories integrated with the Joomla! Issue Tracker + * + * @var array + * @since 2.0 + */ + private static $projects = array( + 'joomla-cms' => array( + 'githubUser' => 'joomla', + 'githubrepo' => 'joomla-cms', + 'projectAlias' => 'joomla-cms', + ), + 'patchtester' => array( + 'githubUser' => 'joomla-extensions', + 'githubrepo' => 'patchtester', + 'projectAlias' => 'patchtester', + ), + 'weblinks' => array( + 'githubUser' => 'joomla-extensions', + 'githubrepo' => 'weblinks', + 'projectAlias' => 'weblinks', + ), + ); - /** - * Get the issue tracker project alias for a GitHub repository - * - * @param string $githubUser The owner of the GitHub repository (user or organization) - * @param string $githubRepo The GitHub repository name - * - * @return string|boolean The project alias if supported or boolean false - * - * @since 2.0 - */ - public static function getTrackerAlias($githubUser, $githubRepo) - { - // If the repo isn't even listed, no point in going further - if (!array_key_exists($githubRepo, self::$projects)) - { - return false; - } + /** + * Get the issue tracker project alias for a GitHub repository + * + * @param string $githubUser The owner of the GitHub repository (user or organization) + * @param string $githubRepo The GitHub repository name + * + * @return string|boolean The project alias if supported or boolean false + * + * @since 2.0 + */ + public static function getTrackerAlias($githubUser, $githubRepo) + { + // If the repo isn't even listed, no point in going further + if (!array_key_exists($githubRepo, self::$projects)) { + return false; + } - // Now the GitHub user must match the project (we don't support forks, sorry!) - if (self::$projects[$githubRepo]['githubUser'] !== $githubUser) - { - return false; - } + // Now the GitHub user must match the project (we don't support forks, sorry!) + if (self::$projects[$githubRepo]['githubUser'] !== $githubUser) { + return false; + } - // This project is supported - return self::$projects[$githubRepo]['projectAlias']; - } + // This project is supported + return self::$projects[$githubRepo]['projectAlias']; + } } diff --git a/administrator/components/com_patchtester/PatchTester/View/AbstractHtmlView.php b/administrator/components/com_patchtester/PatchTester/View/AbstractHtmlView.php index 4258a6d..f3873c2 100644 --- a/administrator/components/com_patchtester/PatchTester/View/AbstractHtmlView.php +++ b/administrator/components/com_patchtester/PatchTester/View/AbstractHtmlView.php @@ -1,4 +1,5 @@ paths = $paths ?: new \SplPriorityQueue(); + } - /** - * The paths queue. - * - * @var SplPriorityQueue - * @since 4.0.0 - */ - protected $paths; + /** + * Method to escape output. + * + * @param string $output The output to escape. + * + * @return string The escaped output. + * + * @since 4.0.0 + */ + public function escape($output) + { + // Escape the output. + return htmlspecialchars($output, ENT_COMPAT, 'UTF-8'); + } - /** - * Method to instantiate the view. - * - * @param AbstractModel $model The model object. - * @param SplPriorityQueue $paths The paths queue. - * - * @since 4.0.0 - */ - public function __construct($model, \SplPriorityQueue $paths = null) - { - parent::__construct($model); + /** + * Method to get the view layout. + * + * @return string The layout name. + * + * @since 4.0.0 + */ + public function getLayout() + { + return $this->layout; + } - // Setup dependencies. - $this->paths = $paths ?: new \SplPriorityQueue; - } + /** + * Method to get the layout path. + * + * @param string $layout The layout name. + * + * @return mixed The layout file name if found, false otherwise. + * + * @since 4.0.0 + */ + public function getPath($layout) + { + // Get the layout file name. + $file = Path::clean($layout . '.php'); +// Find the layout file path. + $path = Path::find(clone $this->paths, $file); + return $path; + } - /** - * Method to escape output. - * - * @param string $output The output to escape. - * - * @return string The escaped output. - * - * @since 4.0.0 - */ - public function escape($output) - { - // Escape the output. - return htmlspecialchars($output, ENT_COMPAT, 'UTF-8'); - } + /** + * Method to get the view paths. + * + * @return SplPriorityQueue The paths queue. + * + * @since 4.0.0 + */ + public function getPaths() + { + return $this->paths; + } - /** - * Method to get the view layout. - * - * @return string The layout name. - * - * @since 4.0.0 - */ - public function getLayout() - { - return $this->layout; - } + /** + * Load a template file -- first look in the templates folder for an override + * + * @param string $tpl The name of the template source file; automatically searches the template paths and compiles as needed. + * + * @return string The output of the the template script. + * + * @since 4.0.0 + * @throws \RuntimeException + */ + public function loadTemplate($tpl = null) + { + // Get the path to the file + $file = $this->getLayout(); + if (isset($tpl)) { + $file .= '_' . $tpl; + } - /** - * Method to get the layout path. - * - * @param string $layout The layout name. - * - * @return mixed The layout file name if found, false otherwise. - * - * @since 4.0.0 - */ - public function getPath($layout) - { - // Get the layout file name. - $file = Path::clean($layout . '.php'); + $path = $this->getPath($file); + if (!$path) { + throw new \RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_LAYOUTFILE_NOT_FOUND', $file), 500); + } - // Find the layout file path. - $path = Path::find(clone $this->paths, $file); + // Unset so as not to introduce into template scope + unset($tpl); + unset($file); +// Never allow a 'this' property + if (isset($this->this)) { + unset($this->this); + } - return $path; - } + // Start an output buffer. + ob_start(); +// Load the template. + include $path; +// Get the layout contents. + return ob_get_clean(); + } - /** - * Method to get the view paths. - * - * @return SplPriorityQueue The paths queue. - * - * @since 4.0.0 - */ - public function getPaths() - { - return $this->paths; - } + /** + * Method to render the view. + * + * @return string The rendered view. + * + * @since 4.0.0 + * @throws RuntimeException + */ + public function render() + { + // Get the layout path. + $path = $this->getPath($this->getLayout()); +// Check if the layout path was found. + if (!$path) { + throw new \RuntimeException('Layout Path Not Found'); + } - /** - * Load a template file -- first look in the templates folder for an override - * - * @param string $tpl The name of the template source file; automatically searches the template paths and compiles as needed. - * - * @return string The output of the the template script. - * - * @since 4.0.0 - * @throws \RuntimeException - */ - public function loadTemplate($tpl = null) - { - // Get the path to the file - $file = $this->getLayout(); + // Start an output buffer. + ob_start(); +// Load the layout. + include $path; +// Get the layout contents. + $output = ob_get_clean(); + return $output; + } - if (isset($tpl)) - { - $file .= '_' . $tpl; - } + /** + * Method to set the view layout. + * + * @param string $layout The layout name. + * + * @return $this + * + * @since 4.0.0 + */ + public function setLayout($layout) + { + $this->layout = $layout; + return $this; + } - $path = $this->getPath($file); - - if (!$path) - { - throw new \RuntimeException(Text::sprintf('JLIB_APPLICATION_ERROR_LAYOUTFILE_NOT_FOUND', $file), 500); - } - - // Unset so as not to introduce into template scope - unset($tpl); - unset($file); - - // Never allow a 'this' property - if (isset($this->this)) - { - unset($this->this); - } - - // Start an output buffer. - ob_start(); - - // Load the template. - include $path; - - // Get the layout contents. - return ob_get_clean(); - } - - /** - * Method to render the view. - * - * @return string The rendered view. - * - * @since 4.0.0 - * @throws RuntimeException - */ - public function render() - { - // Get the layout path. - $path = $this->getPath($this->getLayout()); - - // Check if the layout path was found. - if (!$path) - { - throw new \RuntimeException('Layout Path Not Found'); - } - - // Start an output buffer. - ob_start(); - - // Load the layout. - include $path; - - // Get the layout contents. - $output = ob_get_clean(); - - return $output; - } - - /** - * Method to set the view layout. - * - * @param string $layout The layout name. - * - * @return $this - * - * @since 4.0.0 - */ - public function setLayout($layout) - { - $this->layout = $layout; - - return $this; - } - - /** - * Method to set the view paths. - * - * @param \SplPriorityQueue $paths The paths queue. - * - * @return $this - * - * @since 4.0.0 - */ - public function setPaths(\SplPriorityQueue $paths) - { - $this->paths = $paths; - - return $this; - } + /** + * Method to set the view paths. + * + * @param \SplPriorityQueue $paths The paths queue. + * + * @return $this + * + * @since 4.0.0 + */ + public function setPaths(\SplPriorityQueue $paths) + { + $this->paths = $paths; + return $this; + } } diff --git a/administrator/components/com_patchtester/PatchTester/View/AbstractView.php b/administrator/components/com_patchtester/PatchTester/View/AbstractView.php index fd2fa6a..348f22a 100644 --- a/administrator/components/com_patchtester/PatchTester/View/AbstractView.php +++ b/administrator/components/com_patchtester/PatchTester/View/AbstractView.php @@ -1,4 +1,5 @@ model = $model; + } - /** - * Method to instantiate the view. - * - * @param AbstractModel $model The model object. - * - * @since 4.0.0 - */ - public function __construct($model) - { - $this->model = $model; - } - - /** - * Method to escape output. - * - * @param string $output The output to escape. - * - * @return string The escaped output. - * - * @since 4.0.0 - */ - public function escape($output) - { - return $output; - } + /** + * Method to escape output. + * + * @param string $output The output to escape. + * + * @return string The escaped output. + * + * @since 4.0.0 + */ + public function escape($output) + { + return $output; + } } diff --git a/administrator/components/com_patchtester/PatchTester/View/DefaultHtmlView.php b/administrator/components/com_patchtester/PatchTester/View/DefaultHtmlView.php index d6ce1ba..47c6891 100644 --- a/administrator/components/com_patchtester/PatchTester/View/DefaultHtmlView.php +++ b/administrator/components/com_patchtester/PatchTester/View/DefaultHtmlView.php @@ -1,4 +1,5 @@
-

-

-
-
-
- +

+

+
+
+
+
diff --git a/administrator/components/com_patchtester/PatchTester/View/Pulls/PullsHtmlView.php b/administrator/components/com_patchtester/PatchTester/View/Pulls/PullsHtmlView.php index 0e4792f..076aab8 100644 --- a/administrator/components/com_patchtester/PatchTester/View/Pulls/PullsHtmlView.php +++ b/administrator/components/com_patchtester/PatchTester/View/Pulls/PullsHtmlView.php @@ -1,4 +1,5 @@ envErrors[] = Text::_('COM_PATCHTESTER_REQUIREMENT_OPENSSL'); + } - /** - * Array of open pull requests - * - * @var array - * @since 2.0 - */ - protected $items; + if (!in_array('https', stream_get_wrappers(), true)) { + $this->envErrors[] = Text::_('COM_PATCHTESTER_REQUIREMENT_HTTPS'); + } - /** - * Pagination object - * - * @var Pagination - * @since 2.0 - */ - protected $pagination; + if (!count($this->envErrors)) { + $this->state = $this->model->getState(); + $this->items = $this->model->getItems(); + $this->pagination = $this->model->getPagination(); + $this->filterForm = $this->model->getFilterForm(); + $this->activeFilters = $this->model->getActiveFilters(); + $this->trackerAlias = TrackerHelper::getTrackerAlias($this->state->get('github_user'), $this->state->get('github_repo')); + } - /** - * Form object for search filters - * - * @var Form - * @since 4.1.0 - */ - public $filterForm; + // Change the layout if there are environment errors + if (count($this->envErrors)) { + $this->setLayout('errors'); + } - /** - * The active search filters - * - * @var array - * @since 4.1.0 - */ - public $activeFilters; + $this->addToolbar(); + Text::script('COM_PATCHTESTER_CONFIRM_RESET'); + return parent::render(); + } - /** - * The model state - * - * @var Registry - * @since 2.0.0 - */ - protected $state; + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 2.0.0 + */ + protected function addToolbar(): void + { + ToolbarHelper::title(Text::_('COM_PATCHTESTER'), 'patchtester fas fa-save'); + if (!count($this->envErrors)) { + $toolbar = Toolbar::getInstance('toolbar'); + $toolbar->appendButton('Popup', 'sync', 'COM_PATCHTESTER_TOOLBAR_FETCH_DATA', 'index.php?option=com_patchtester&view=fetch&tmpl=component', 500, 210, 0, 0, 'window.parent.location.reload()', Text::_('COM_PATCHTESTER_HEADING_FETCH_DATA')); + // Add a reset button. + $toolbar->appendButton('Standard', 'expired', 'COM_PATCHTESTER_TOOLBAR_RESET', 'reset', false); + } - /** - * The issue tracker project alias - * - * @var string|boolean - * @since 2.0 - */ - protected $trackerAlias; - - /** - * Method to render the view. - * - * @return string The rendered view. - * - * @since 2.0.0 - * @throws Exception - */ - public function render(): string - { - if (!extension_loaded('openssl')) - { - $this->envErrors[] = Text::_('COM_PATCHTESTER_REQUIREMENT_OPENSSL'); - } - - if (!in_array('https', stream_get_wrappers(), true)) - { - $this->envErrors[] = Text::_('COM_PATCHTESTER_REQUIREMENT_HTTPS'); - } - - if (!count($this->envErrors)) - { - $this->state = $this->model->getState(); - $this->items = $this->model->getItems(); - $this->pagination = $this->model->getPagination(); - $this->filterForm = $this->model->getFilterForm(); - $this->activeFilters = $this->model->getActiveFilters(); - $this->trackerAlias = TrackerHelper::getTrackerAlias( - $this->state->get('github_user'), - $this->state->get('github_repo') - ); - } - - // Change the layout if there are environment errors - if (count($this->envErrors)) - { - $this->setLayout('errors'); - } - - $this->addToolbar(); - - Text::script('COM_PATCHTESTER_CONFIRM_RESET'); - - return parent::render(); - } - - /** - * Add the page title and toolbar. - * - * @return void - * - * @since 2.0.0 - */ - protected function addToolbar(): void - { - ToolbarHelper::title(Text::_('COM_PATCHTESTER'), 'patchtester fas fa-save'); - - if (!count($this->envErrors)) - { - $toolbar = Toolbar::getInstance('toolbar'); - - $toolbar->appendButton( - 'Popup', - 'sync', - 'COM_PATCHTESTER_TOOLBAR_FETCH_DATA', - 'index.php?option=com_patchtester&view=fetch&tmpl=component', - 500, - 210, - 0, - 0, - 'window.parent.location.reload()', - Text::_('COM_PATCHTESTER_HEADING_FETCH_DATA') - ); - - // Add a reset button. - $toolbar->appendButton('Standard', 'expired', 'COM_PATCHTESTER_TOOLBAR_RESET', 'reset', false); - } - - ToolbarHelper::preferences('com_patchtester'); - } + ToolbarHelper::preferences('com_patchtester'); + } } diff --git a/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default.php b/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default.php index 81878cc..8419f86 100644 --- a/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default.php +++ b/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default.php @@ -1,4 +1,5 @@ '3.5.0', 'relative' => true]); HTMLHelper::_('script', 'com_patchtester/patchtester.js', ['version' => 'auto', 'relative' => true]); ?>
-
-
-
- $this]); ?> -
- items)) : ?> -
- - -
- - - - - - - - - - - - - - - loadTemplate('items'); ?> - -
- , -
- - - - - - - - - - - -
- +
+
+
+ $this]); ?> +
+ items)) : + ?> +
+ + +
+ + + + + + + + + + + + + + + loadTemplate('items'); ?> + +
+ , +
+ + + + + + + + + + + +
+ - pagination->getListFooter(); ?> + pagination->getListFooter(); ?> - - - - -
-
-
-
+ + + + +
+
+
+
diff --git a/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default_items.php b/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default_items.php index d7d361f..a70d372 100644 --- a/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default_items.php +++ b/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default_items.php @@ -1,4 +1,5 @@ items as $i => $item) : - $status = ''; - - if ($item->applied) : - $status = ' class="table-active"'; - endif; -?> - > - - pull_id; ?> - is_draft) : ?> - - - - - - - escape($item->title); ?> - -
-
- - - -
- trackerAlias) : ?> -
- - - -
- - applied) : ?> -
- sha, 0, 10)); ?> -
- -
- labels) > 0) : ?> + $status = ''; + if ($item->applied) : + $status = ' class="table-active"'; + endif; + ?> + > + + pull_id; ?> + is_draft) : + ?> + + + + + + + escape($item->title); ?> + +
+
+ + + +
+ trackerAlias) : + ?> +
+ + + +
+ + applied) : + ?> +
+ sha, 0, 10)); ?> +
+ +
+ labels) > 0) : + ?>
- labels as $label): ?> + labels as $label) : + ?> name)) - { - case 'a11y': - case 'conflicting files': - case 'documentation required': - case 'information required': - case 'j3 issue': - case 'language change': - case 'mysql 5.7': - case 'needs new owner': - case 'no code attached yet': - case 'pbf': - case 'pr-3.9-dev': - case 'pr-3.10-dev': - case 'pr-4.1-dev': - case 'pr-i10n_4.0-dev': - case 'pr-staging': - case 'release blocker': - case 'rfc': - case 'test instructions missing': - case 'updates requested': - $textColor = '000000'; - break; - default: - $textColor = 'FFFFFF'; - break; - } + switch (strtolower($label->name)) { + case 'a11y': + case 'conflicting files': + case 'documentation required': + case 'information required': + case 'j3 issue': + case 'language change': + case 'mysql 5.7': + case 'needs new owner': + case 'no code attached yet': + case 'pbf': + case 'pr-3.9-dev': + case 'pr-3.10-dev': + case 'pr-4.1-dev': + case 'pr-i10n_4.0-dev': + case 'pr-staging': + case 'release blocker': + case 'rfc': + case 'test instructions missing': + case 'updates requested': + $textColor = '000000'; + + break; + default: + $textColor = 'FFFFFF'; + + break; + } ?> name; ?> - +
- - - - escape($item->branch); ?> - - - is_rtc) : ?> - - - - - - - applied) : ?> - - - - - - - applied) : ?> - - - - - - - + + + escape($item->branch); ?> + + + is_rtc) : + ?> + + + + + + + applied) : + ?> + + + + + + + applied) : + ?> + + + + + + +

    -envErrors as $error) : ?> -
  • - +envErrors as $error) : + ?> +
  • +
diff --git a/administrator/components/com_patchtester/patchtester.php b/administrator/components/com_patchtester/patchtester.php index a86ad4c..9516278 100644 --- a/administrator/components/com_patchtester/patchtester.php +++ b/administrator/components/com_patchtester/patchtester.php @@ -1,4 +1,5 @@ authorise('core.manage', 'com_patchtester')) -{ - throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); +if (!Factory::getUser()->authorise('core.manage', 'com_patchtester')) { + throw new RuntimeException(Text::_('JERROR_ALERTNOAUTHOR'), 403); } // Application reference $app = Factory::getApplication(); - // Import our Composer autoloader to load the component classes require_once __DIR__ . '/vendor/autoload.php'; - // Build the controller class name based on task $task = $app->input->getCmd('task', 'display'); - // If $task is an empty string, apply our default since JInput might not -if ($task === '') -{ - $task = 'display'; +if ($task === '') { + $task = 'display'; } $class = '\\PatchTester\\Controller\\' . ucfirst(strtolower($task)) . 'Controller'; - -if (!class_exists($class)) -{ - throw new InvalidArgumentException(Text::sprintf('JLIB_APPLICATION_ERROR_INVALID_CONTROLLER_CLASS', $class), 404); +if (!class_exists($class)) { + throw new InvalidArgumentException(Text::sprintf('JLIB_APPLICATION_ERROR_INVALID_CONTROLLER_CLASS', $class), 404); } // Instantiate and execute the controller diff --git a/administrator/components/com_patchtester/script.php b/administrator/components/com_patchtester/script.php index d8cfba7..0bf0594 100644 --- a/administrator/components/com_patchtester/script.php +++ b/administrator/components/com_patchtester/script.php @@ -1,4 +1,5 @@ minimumJoomla = '4.0'; + $this->minimumPhp = JOOMLA_MINIMUM_PHP; + $this->deleteFiles = array( + '/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default_errors.php', + ); + $this->deleteFolders = array( + '/administrator/components/com_patchtester/PatchTester/Table', + '/components/com_patchtester', + ); + Factory::getApplication() + ->getLanguage() + ->load('com_patchtester.sys', JPATH_ADMINISTRATOR, null, true); + } - /** - * Extension script constructor. - * - * @since 3.0.0 - */ - public function __construct() - { - $this->minimumJoomla = '4.0'; - $this->minimumPhp = JOOMLA_MINIMUM_PHP; + /** + * Show the message on install. + * + * @param ComponentAdapter $parent The class calling this method + * + * @return void + * + * @since 4.0.0 + */ + public function install(ComponentAdapter $parent): void + { + ?> +

+ + +

+

+ deleteFiles = array( - '/administrator/components/com_patchtester/PatchTester/View/Pulls/tmpl/default_errors.php', - ); - - $this->deleteFolders = array( - '/administrator/components/com_patchtester/PatchTester/Table', - '/components/com_patchtester', - ); - - Factory::getApplication() - ->getLanguage() - ->load('com_patchtester.sys', JPATH_ADMINISTRATOR, null, true); - } - - /** - * Show the message on install. - * - * @param ComponentAdapter $parent The class calling this method - * - * @return void - * - * @since 4.0.0 - */ - public function install(ComponentAdapter $parent): void - { - ?> -

- - -

-

- -

- - -

-

- +

+ + +

+

+ -

- -

-

- +

+ +

+

+ removeFiles(); - } + /** + * Function to perform changes during postflight + * + * @param string $type The action being performed + * @param ComponentAdapter $parent The class calling this method + * + * @return void + * + * @since 3.0.0 + */ + public function postflight(string $type, ComponentAdapter $parent): void + { + $this->removeFiles(); + } } diff --git a/build/patchtester/hash_generator.php b/build/patchtester/hash_generator.php index 01f1573..a25aa97 100644 --- a/build/patchtester/hash_generator.php +++ b/build/patchtester/hash_generator.php @@ -1,5 +1,6 @@ #!/usr/bin/env php isDir() || $file->isDot()) - { - continue; - } +foreach (new DirectoryIterator($packageDir) as $file) { + if ($file->isDir() || $file->isDot()) { + continue; + } - $hashes[$file->getFilename()] = array( - 'sha384' => hash_file('sha384', $file->getPathname()), - ); + $hashes[$file->getFilename()] = array( + 'sha384' => hash_file('sha384', $file->getPathname()), + ); } $jsonOptions = PHP_VERSION_ID >= 50400 ? JSON_PRETTY_PRINT : 0; diff --git a/build/patchtester/release.php b/build/patchtester/release.php index e698249..a180b6e 100644 --- a/build/patchtester/release.php +++ b/build/patchtester/release.php @@ -1,5 +1,6 @@ #!/usr/bin/env php :' . PHP_TAB . 'Version (ex: 3.0.0, 3.0.1-dev)' . PHP_EOL; - echo PHP_TAB . PHP_TAB . '--exclude-manifest: Exclude updating the update server manifest'; - echo PHP_EOL; + echo PHP_EOL; + echo 'Usage: php ' . $command . ' [options]' . PHP_EOL; + echo PHP_TAB . '[options]:' . PHP_EOL; + echo PHP_TAB . PHP_TAB . '-v :' . PHP_TAB . 'Version (ex: 3.0.0, 3.0.1-dev)' . PHP_EOL; + echo PHP_TAB . PHP_TAB . '--exclude-manifest: Exclude updating the update server manifest'; + echo PHP_EOL; } $manifestFile = '/administrator/components/com_patchtester/patchtester.xml'; @@ -39,31 +40,27 @@ $updateServerFile = '/manifest.xml'; // Check arguments (exit if incorrect cli arguments). $opts = getopt('v:', array('exclude-manifest')); -if (empty($opts['v'])) -{ - usage($argv[0]); - die(); +if (empty($opts['v'])) { + usage($argv[0]); + die(); } // Check version string (exit if not correct). $versionParts = explode('-', $opts['v']); -if (!preg_match('#^[0-9]+\.[0-9]+\.[0-9]+$#', $versionParts[0])) -{ - usage($argv[0]); - die(); +if (!preg_match('#^[0-9]+\.[0-9]+\.[0-9]+$#', $versionParts[0])) { + usage($argv[0]); + die(); } -if (isset($versionParts[1]) && !preg_match('#(dev|alpha|beta|rc)[0-9]*#', $versionParts[1])) -{ - usage($argv[0]); - die(); +if (isset($versionParts[1]) && !preg_match('#(dev|alpha|beta|rc)[0-9]*#', $versionParts[1])) { + usage($argv[0]); + die(); } -if (isset($versionParts[2]) && $versionParts[2] !== 'dev') -{ - usage($argv[0]); - die(); +if (isset($versionParts[2]) && $versionParts[2] !== 'dev') { + usage($argv[0]); + die(); } // Make sure we use the correct language and timezone. @@ -77,15 +74,15 @@ umask(022); $versionSubParts = explode('.', $versionParts[0]); $version = array( - 'main' => $versionSubParts[0] . '.' . $versionSubParts[1], - 'release' => $versionSubParts[0] . '.' . $versionSubParts[1] . '.' . $versionSubParts[2], - 'dev_devel' => $versionSubParts[2] . (!empty($versionParts[1]) ? '-' . $versionParts[1] : '') . (!empty($versionParts[2]) ? '-' . $versionParts[2] : ''), - 'credate' => date('d-F-Y'), + 'main' => $versionSubParts[0] . '.' . $versionSubParts[1], + 'release' => $versionSubParts[0] . '.' . $versionSubParts[1] . '.' . $versionSubParts[2], + 'dev_devel' => $versionSubParts[2] . (!empty($versionParts[1]) ? '-' . $versionParts[1] : '') . (!empty($versionParts[2]) ? '-' . $versionParts[2] : ''), + 'credate' => date('d-F-Y'), ); // Prints version information. echo PHP_EOL; -echo 'Version data:'. PHP_EOL; +echo 'Version data:' . PHP_EOL; echo '- Main:' . PHP_TAB . PHP_TAB . PHP_TAB . $version['main'] . PHP_EOL; echo '- Release:' . PHP_TAB . PHP_TAB . $version['release'] . PHP_EOL; echo '- Full:' . PHP_TAB . PHP_TAB . PHP_TAB . $version['main'] . '.' . $version['dev_devel'] . PHP_EOL; @@ -96,33 +93,30 @@ echo PHP_EOL; $rootPath = dirname(dirname(__DIR__)); // Updates the version and creation date in the component manifest file. -if (file_exists($rootPath . $manifestFile)) -{ - $fileContents = file_get_contents($rootPath . $manifestFile); - $fileContents = preg_replace('#[^<]*#', '' . $version['main'] . '.' . $version['dev_devel'] . '', $fileContents); - $fileContents = preg_replace('#[^<]*#', '' . $version['credate'] . '', $fileContents); - file_put_contents($rootPath . $manifestFile, $fileContents); +if (file_exists($rootPath . $manifestFile)) { + $fileContents = file_get_contents($rootPath . $manifestFile); + $fileContents = preg_replace('#[^<]*#', '' . $version['main'] . '.' . $version['dev_devel'] . '', $fileContents); + $fileContents = preg_replace('#[^<]*#', '' . $version['credate'] . '', $fileContents); + file_put_contents($rootPath . $manifestFile, $fileContents); } // Replaces the `__DEPLOY_VERSION__` marker with the "release" version number system('cd ' . $rootPath . ' && find administrator -name "*.php" -type f -exec sed -i "s/__DEPLOY_VERSION__/' . $version['release'] . '/g" "{}" \;'); // If not instructed to exclude it, update the update server's manifest -if (!isset($opts['exclude-manifest'])) -{ - if (file_exists($rootPath . $updateServerFile)) - { - $fileContents = file_get_contents($rootPath . $updateServerFile); - $fileContents = preg_replace('#[^<]*#', 'https://github.com/joomla-extensions/patchtester/releases/tag/' . $version['release'] . '', $fileContents); - $fileContents = preg_replace('#[^<]*#', 'https://github.com/joomla-extensions/patchtester/releases/download/' . $version['release'] . '/com_patchtester_' . $version['release'] . '.zip', $fileContents); - file_put_contents($rootPath . $updateServerFile, $fileContents); +if (!isset($opts['exclude-manifest'])) { + if (file_exists($rootPath . $updateServerFile)) { + $fileContents = file_get_contents($rootPath . $updateServerFile); + $fileContents = preg_replace('#[^<]*#', 'https://github.com/joomla-extensions/patchtester/releases/tag/' . $version['release'] . '', $fileContents); + $fileContents = preg_replace('#[^<]*#', 'https://github.com/joomla-extensions/patchtester/releases/download/' . $version['release'] . '/com_patchtester_' . $version['release'] . '.zip', $fileContents); + file_put_contents($rootPath . $updateServerFile, $fileContents); - echo '*************' . PHP_EOL; - echo '* IMPORTANT *' . PHP_EOL; - echo '*************' . PHP_EOL; - echo '' . PHP_EOL; - echo 'Ensure you regenerate the SHA384 package hash before publishing the updated manifest!!!' . PHP_EOL; - } + echo '*************' . PHP_EOL; + echo '* IMPORTANT *' . PHP_EOL; + echo '*************' . PHP_EOL; + echo '' . PHP_EOL; + echo 'Ensure you regenerate the SHA384 package hash before publishing the updated manifest!!!' . PHP_EOL; + } } echo 'Version bump complete!' . PHP_EOL . PHP_EOL; diff --git a/composer.json b/composer.json index c0e792c..c713222 100644 --- a/composer.json +++ b/composer.json @@ -14,8 +14,6 @@ }, "require-dev": { "joomla/crowdin-sync": "dev-master", - "joomla/cms-coding-standards": "~2.0.0-alpha2@dev", - "joomla/coding-standards": "~3.0@dev", "squizlabs/php_codesniffer": "~3.0" }, "autoload": { diff --git a/composer.lock b/composer.lock index b19795a..c2f71d4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d81ecddb06617ae1dcb19dd0e1a15e03", + "content-hash": "d0bb5decb1e421235bf65cfc943835da", "packages": [], "packages-dev": [ { @@ -45,27 +45,33 @@ } ], "description": "A crowdin API implementation in PHP", + "support": { + "issues": "https://github.com/elkuku/crowdin-api/issues", + "source": "https://github.com/elkuku/crowdin-api/tree/master" + }, + "abandoned": true, "time": "2017-07-01T13:35:05+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "6.5.2", + "version": "6.5.8", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "43ece0e75098b7ecd8d13918293029e555a50f82" + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/43ece0e75098b7ecd8d13918293029e555a50f82", - "reference": "43ece0e75098b7ecd8d13918293029e555a50f82", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a52f0440530b54fa079ce76e8c5d196a42cad981", + "reference": "a52f0440530b54fa079ce76e8c5d196a42cad981", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^1.0", - "guzzlehttp/psr7": "^1.6.1", - "php": ">=5.5" + "guzzlehttp/psr7": "^1.9", + "php": ">=5.5", + "symfony/polyfill-intl-idn": "^1.17" }, "require-dev": { "ext-curl": "*", @@ -73,7 +79,6 @@ "psr/log": "^1.1" }, "suggest": { - "ext-intl": "Required for Internationalized Domain Name (IDN) support", "psr/log": "Required for using the Log middleware" }, "type": "library", @@ -83,22 +88,52 @@ } }, "autoload": { - "psr-4": { - "GuzzleHttp\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Jeremy Lindblom", + "email": "jeremeamia@gmail.com", + "homepage": "https://github.com/jeremeamia" + }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle is a PHP HTTP client library", @@ -112,71 +147,122 @@ "rest", "web service" ], - "time": "2019-12-23T11:57:10+00:00" + "support": { + "issues": "https://github.com/guzzle/guzzle/issues", + "source": "https://github.com/guzzle/guzzle/tree/6.5.8" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle", + "type": "tidelift" + } + ], + "time": "2022-06-20T22:16:07+00:00" }, { "name": "guzzlehttp/promises", - "version": "v1.3.1", + "version": "1.5.2", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646" + "reference": "b94b2807d85443f9719887892882d0329d1e2598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/a59da6cf61d80060647ff4d3eb2c03a2bc694646", - "reference": "a59da6cf61d80060647ff4d3eb2c03a2bc694646", + "url": "https://api.github.com/repos/guzzle/promises/zipball/b94b2807d85443f9719887892882d0329d1e2598", + "reference": "b94b2807d85443f9719887892882d0329d1e2598", "shasum": "" }, "require": { - "php": ">=5.5.0" + "php": ">=5.5" }, "require-dev": { - "phpunit/phpunit": "^4.0" + "symfony/phpunit-bridge": "^4.4 || ^5.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "1.5-dev" } }, "autoload": { - "psr-4": { - "GuzzleHttp\\Promise\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\Promise\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", + "homepage": "https://github.com/Tobion" } ], "description": "Guzzle promises library", "keywords": [ "promise" ], - "time": "2016-12-20T10:07:11+00:00" + "support": { + "issues": "https://github.com/guzzle/promises/issues", + "source": "https://github.com/guzzle/promises/tree/1.5.2" + }, + "funding": [ + { + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises", + "type": "tidelift" + } + ], + "time": "2022-08-28T14:55:35+00:00" }, { "name": "guzzlehttp/psr7", - "version": "1.6.1", + "version": "1.9.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "239400de7a173fe9901b9ac7c06497751f00727a" + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/239400de7a173fe9901b9ac7c06497751f00727a", - "reference": "239400de7a173fe9901b9ac7c06497751f00727a", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", + "reference": "e98e3e6d4f86621a9b75f623996e6bbdeb4b9318", "shasum": "" }, "require": { @@ -189,37 +275,58 @@ }, "require-dev": { "ext-zlib": "*", - "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.8" + "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" }, "suggest": { - "zendframework/zend-httphandlerrunner": "Emit PSR-7 responses" + "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6-dev" + "dev-master": "1.9-dev" } }, "autoload": { - "psr-4": { - "GuzzleHttp\\Psr7\\": "src/" - }, "files": [ "src/functions_include.php" - ] + ], + "psr-4": { + "GuzzleHttp\\Psr7\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ + { + "name": "Graham Campbell", + "email": "hello@gjcampbell.co.uk", + "homepage": "https://github.com/GrahamCampbell" + }, { "name": "Michael Dowling", "email": "mtdowling@gmail.com", "homepage": "https://github.com/mtdowling" }, + { + "name": "George Mponos", + "email": "gmponos@gmail.com", + "homepage": "https://github.com/gmponos" + }, + { + "name": "Tobias Nyholm", + "email": "tobias.nyholm@gmail.com", + "homepage": "https://github.com/Nyholm" + }, + { + "name": "Márk Sági-Kazár", + "email": "mark.sagikazar@gmail.com", + "homepage": "https://github.com/sagikazarmark" + }, { "name": "Tobias Schultze", + "email": "webmaster@tubo-world.de", "homepage": "https://github.com/Tobion" } ], @@ -234,77 +341,55 @@ "uri", "url" ], - "time": "2019-07-01T23:21:34+00:00" - }, - { - "name": "ircmaxell/password-compat", - "version": "v1.0.4", - "source": { - "type": "git", - "url": "https://github.com/ircmaxell/password_compat.git", - "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c" + "support": { + "issues": "https://github.com/guzzle/psr7/issues", + "source": "https://github.com/guzzle/psr7/tree/1.9.0" }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/ircmaxell/password_compat/zipball/5c5cde8822a69545767f7c7f3058cb15ff84614c", - "reference": "5c5cde8822a69545767f7c7f3058cb15ff84614c", - "shasum": "" - }, - "require-dev": { - "phpunit/phpunit": "4.*" - }, - "type": "library", - "autoload": { - "files": [ - "lib/password.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ + "funding": [ { - "name": "Anthony Ferrara", - "email": "ircmaxell@php.net", - "homepage": "http://blog.ircmaxell.com" + "url": "https://github.com/GrahamCampbell", + "type": "github" + }, + { + "url": "https://github.com/Nyholm", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7", + "type": "tidelift" } ], - "description": "A compatibility library for the proposed simplified password hashing algorithm: https://wiki.php.net/rfc/password_hash", - "homepage": "https://github.com/ircmaxell/password_compat", - "keywords": [ - "hashing", - "password" - ], - "time": "2014-11-20T16:49:30+00:00" + "time": "2022-06-20T21:43:03+00:00" }, { "name": "joomla/application", - "version": "1.9.2", + "version": "1.9.3", "source": { "type": "git", "url": "https://github.com/joomla-framework/application.git", - "reference": "6c89fdde878f7ebb7d6455f664133e9497163e2e" + "reference": "2a2fee9fa2ebb07c0d28da07f6e4ea3c56b77d16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joomla-framework/application/zipball/6c89fdde878f7ebb7d6455f664133e9497163e2e", - "reference": "6c89fdde878f7ebb7d6455f664133e9497163e2e", + "url": "https://api.github.com/repos/joomla-framework/application/zipball/2a2fee9fa2ebb07c0d28da07f6e4ea3c56b77d16", + "reference": "2a2fee9fa2ebb07c0d28da07f6e4ea3c56b77d16", "shasum": "" }, "require": { - "joomla/input": "~1.2", - "joomla/registry": "^1.4.5|~2.0", - "php": "^5.3.10|~7.0", - "psr/log": "~1.0" + "joomla/input": "^1.2", + "joomla/registry": "^1.4.5|^2.0", + "php": "^5.3.10|^7.0|^8.0", + "psr/log": "^1.0" }, "require-dev": { - "joomla/coding-standards": "~2.0@alpha", - "joomla/event": "~1.2|~2.0", - "joomla/session": "^1.2.1|~2.0", - "joomla/test": "~1.1", - "joomla/uri": "~1.1", - "phpunit/phpunit": "^4.8.35|^5.4.3|~6.0" + "joomla/coding-standards": "^2.0@alpha", + "joomla/event": "^1.2", + "joomla/session": "^1.2.1", + "joomla/test": "^1.1", + "joomla/uri": "^1.1", + "phpunit/phpunit": "^4.8.35|^5.4.3|^6.0|^7.0|^8.0", + "symfony/phpunit-bridge": "^3.4.26|^4.1.12|^4.2.7|^5.0", + "symfony/polyfill-php72": "^1.5" }, "suggest": { "joomla/session": "To use AbstractWebApplication with session support, install joomla/session", @@ -332,109 +417,21 @@ "framework", "joomla" ], - "time": "2019-03-28T14:55:36+00:00" - }, - { - "name": "joomla/cms-coding-standards", - "version": "2.0.0-alpha2", - "source": { - "type": "git", - "url": "https://github.com/joomla/cms-coding-standards.git", - "reference": "25abae52f9167fa47ce57331bbece8079ad27ab3" + "support": { + "issues": "https://github.com/joomla-framework/application/issues", + "source": "https://github.com/joomla-framework/application/tree/1.9.3" }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/joomla/cms-coding-standards/zipball/25abae52f9167fa47ce57331bbece8079ad27ab3", - "reference": "25abae52f9167fa47ce57331bbece8079ad27ab3", - "shasum": "" - }, - "require": { - "joomla/coding-standards": "~2.0 || ~3.0", - "php": ">=5.3.10" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.7" - }, - "suggest": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.3 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." - }, - "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - } - }, - "autoload": { - "psr-4": { - "Joomla-CMS\\Sniffs\\": "lib/Joomla-CMS/Sniffs" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ + "funding": [ { - "name": "Joomla Coding Standards Contributors", - "homepage": "https://github.com/joomla/cms-coding-standards/graphs/contributors" - } - ], - "description": "Extended Joomla Coding Standards for the Joomla CMS application", - "homepage": "https://github.com/joomla/cms-coding-standards", - "keywords": [ - "coding standards", - "joomla", - "php codesniffer", - "phpcs" - ], - "time": "2019-07-13T18:56:54+00:00" - }, - { - "name": "joomla/coding-standards", - "version": "dev-3.x-dev", - "source": { - "type": "git", - "url": "https://github.com/joomla/coding-standards.git", - "reference": "57a14333518d49907d16e216d0494c2fb4f8276d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/joomla/coding-standards/zipball/57a14333518d49907d16e216d0494c2fb4f8276d", - "reference": "57a14333518d49907d16e216d0494c2fb4f8276d", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "squizlabs/php_codesniffer": "^3.4" - }, - "require-dev": { - "phpunit/phpunit": "^4.8.7" - }, - "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-3.x-dev": "3.0-dev" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "GPL-2.0-or-later" - ], - "authors": [ + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, { - "name": "Joomla Coding Standards Contributors", - "homepage": "https://github.com/joomla/coding-standards/graphs/contributors" + "url": "https://github.com/joomla", + "type": "github" } ], - "description": "Joomla Coding Standards", - "homepage": "https://github.com/joomla/coding-standards", - "keywords": [ - "coding standards", - "joomla", - "php codesniffer", - "phpcs" - ], - "time": "2020-01-21T18:27:08+00:00" + "time": "2022-01-25T17:10:25+00:00" }, { "name": "joomla/compat", @@ -471,6 +468,10 @@ "framework", "joomla" ], + "support": { + "issues": "https://github.com/joomla-framework/compat/issues", + "source": "https://github.com/joomla-framework/compat/tree/1.2.0" + }, "time": "2015-02-24T00:21:06+00:00" }, { @@ -479,12 +480,12 @@ "source": { "type": "git", "url": "https://github.com/joomla-projects/crowdin-sync.git", - "reference": "c1db9d2186a273b66297f4fbf239bbc06b70a9fb" + "reference": "f46c206e18511afac316987d5d68f1fe3511c15e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joomla-projects/crowdin-sync/zipball/c1db9d2186a273b66297f4fbf239bbc06b70a9fb", - "reference": "c1db9d2186a273b66297f4fbf239bbc06b70a9fb", + "url": "https://api.github.com/repos/joomla-projects/crowdin-sync/zipball/f46c206e18511afac316987d5d68f1fe3511c15e", + "reference": "f46c206e18511afac316987d5d68f1fe3511c15e", "shasum": "" }, "require": { @@ -494,6 +495,7 @@ "php": "~5.4|~7.0", "symfony/yaml": "~2.8|~3.0" }, + "default-branch": true, "bin": [ "bin/crowdin" ], @@ -513,25 +515,30 @@ "crowdin", "joomla" ], - "time": "2019-09-10T14:24:41+00:00" + "support": { + "issues": "https://github.com/joomla-projects/crowdin-sync/issues", + "source": "https://github.com/joomla-projects/crowdin-sync/tree/master" + }, + "abandoned": true, + "time": "2020-05-24T01:31:21+00:00" }, { "name": "joomla/filter", - "version": "1.3.5", + "version": "1.4.4", "source": { "type": "git", "url": "https://github.com/joomla-framework/filter.git", - "reference": "ee1d870b5c188056745e1dd3cece21522e2158b8" + "reference": "09733d70db6c6d91e53e0e0d0fcde9b8638175c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joomla-framework/filter/zipball/ee1d870b5c188056745e1dd3cece21522e2158b8", - "reference": "ee1d870b5c188056745e1dd3cece21522e2158b8", + "url": "https://api.github.com/repos/joomla-framework/filter/zipball/09733d70db6c6d91e53e0e0d0fcde9b8638175c4", + "reference": "09733d70db6c6d91e53e0e0d0fcde9b8638175c4", "shasum": "" }, "require": { "joomla/string": "~1.3|~2.0", - "php": "^5.3.10|~7.0" + "php": "^5.3.10|~7.0|^8.0" }, "require-dev": { "joomla/coding-standards": "~2.0@alpha", @@ -563,7 +570,21 @@ "framework", "joomla" ], - "time": "2018-05-26T15:48:53+00:00" + "support": { + "issues": "https://github.com/joomla-framework/filter/issues", + "source": "https://github.com/joomla-framework/filter/tree/1.4.4" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2022-03-29T12:14:25+00:00" }, { "name": "joomla/input", @@ -610,20 +631,24 @@ "input", "joomla" ], + "support": { + "issues": "https://github.com/joomla-framework/input/issues", + "source": "https://github.com/joomla-framework/input/tree/1.4.0" + }, "time": "2019-06-15T22:13:58+00:00" }, { "name": "joomla/registry", - "version": "1.6.3", + "version": "1.6.4", "source": { "type": "git", "url": "https://github.com/joomla-framework/registry.git", - "reference": "51c08b18838aaf2104e38c3846e34b4706e83644" + "reference": "87450394f093efcb3ac5fc978e73d1403ebe8a38" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joomla-framework/registry/zipball/51c08b18838aaf2104e38c3846e34b4706e83644", - "reference": "51c08b18838aaf2104e38c3846e34b4706e83644", + "url": "https://api.github.com/repos/joomla-framework/registry/zipball/87450394f093efcb3ac5fc978e73d1403ebe8a38", + "reference": "87450394f093efcb3ac5fc978e73d1403ebe8a38", "shasum": "" }, "require": { @@ -635,7 +660,7 @@ "require-dev": { "joomla/coding-standards": "~2.0@alpha", "joomla/test": "~1.0", - "phpunit/phpunit": "^4.8.35|^5.4.3|~6.0", + "phpunit/phpunit": "^4.8.35|^5.4.3|~6.0|~7.0|~8.0", "symfony/yaml": "~2.0|~3.0|~4.0|~5.0" }, "suggest": { @@ -663,43 +688,60 @@ "joomla", "registry" ], - "time": "2019-11-27T16:58:55+00:00" + "support": { + "issues": "https://github.com/joomla-framework/registry/issues", + "source": "https://github.com/joomla-framework/registry/tree/1.6.4" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2022-01-08T18:33:07+00:00" }, { "name": "joomla/string", - "version": "1.4.3", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/joomla-framework/string.git", - "reference": "fed0eee67f83b68674e8c6542ecfa28390b32fec" + "reference": "bcdb8d45ad3953bf3cfc2b9577288823cab381b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joomla-framework/string/zipball/fed0eee67f83b68674e8c6542ecfa28390b32fec", - "reference": "fed0eee67f83b68674e8c6542ecfa28390b32fec", + "url": "https://api.github.com/repos/joomla-framework/string/zipball/bcdb8d45ad3953bf3cfc2b9577288823cab381b9", + "reference": "bcdb8d45ad3953bf3cfc2b9577288823cab381b9", "shasum": "" }, "require": { - "php": "^5.3.10|~7.0" + "php": "^7.2.5|~8.0.0|~8.1.0", + "symfony/deprecation-contracts": "^2.1" + }, + "conflict": { + "doctrine/inflector": "<1.2" }, "require-dev": { - "joomla/coding-standards": "~2.0@alpha", - "joomla/test": "~1.0", - "phpunit/phpunit": "^4.8.35|^5.4.3|~6.0" + "doctrine/inflector": "^1.2", + "joomla/coding-standards": "^3.0@dev", + "joomla/test": "^2.0.1", + "phpunit/phpunit": "^8.5|^9.0" }, "suggest": { + "doctrine/inflector": "To use the string inflector", "ext-mbstring": "For improved processing" }, "type": "joomla-package", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-2.0-dev": "2.0-dev" } }, "autoload": { - "psr-4": { - "Joomla\\String\\": "src/" - }, "files": [ "src/phputf8/utf8.php", "src/phputf8/ord.php", @@ -716,7 +758,10 @@ "src/phputf8/ucwords.php", "src/phputf8/utils/ascii.php", "src/phputf8/utils/validation.php" - ] + ], + "psr-4": { + "Joomla\\String\\": "src/" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -729,34 +774,48 @@ "joomla", "string" ], - "time": "2019-08-07T12:34:12+00:00" + "support": { + "issues": "https://github.com/joomla-framework/string/issues", + "source": "https://github.com/joomla-framework/string/tree/2.0.1" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2022-08-15T12:47:17+00:00" }, { "name": "joomla/utilities", - "version": "1.6.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/joomla-framework/utilities.git", - "reference": "b54beb07ddf2d8074f6f8f43c365f84ddf714c8f" + "reference": "ab1660fd22184700306b932c39410ba955dc82e1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/joomla-framework/utilities/zipball/b54beb07ddf2d8074f6f8f43c365f84ddf714c8f", - "reference": "b54beb07ddf2d8074f6f8f43c365f84ddf714c8f", + "url": "https://api.github.com/repos/joomla-framework/utilities/zipball/ab1660fd22184700306b932c39410ba955dc82e1", + "reference": "ab1660fd22184700306b932c39410ba955dc82e1", "shasum": "" }, "require": { - "joomla/string": "~1.3|~2.0", - "php": "^5.3.10|~7.0" + "joomla/string": "^2.0", + "php": "^7.2.5|~8.0.0|~8.1.0" }, "require-dev": { - "joomla/coding-standards": "~2.0@alpha", - "phpunit/phpunit": "^4.8.35|^5.4.3|~6.0" + "joomla/coding-standards": "^3.0@dev", + "phpunit/phpunit": "^8.5|^9.0" }, "type": "joomla-package", "extra": { "branch-alias": { - "dev-master": "1.x-dev" + "dev-2.0-dev": "2.0-dev" } }, "autoload": { @@ -775,7 +834,21 @@ "joomla", "utilities" ], - "time": "2019-07-17T01:48:57+00:00" + "support": { + "issues": "https://github.com/joomla-framework/utilities/issues", + "source": "https://github.com/joomla-framework/utilities/tree/2.0.1" + }, + "funding": [ + { + "url": "https://community.joomla.org/sponsorship-campaigns.html", + "type": "custom" + }, + { + "url": "https://github.com/joomla", + "type": "github" + } + ], + "time": "2022-08-15T11:29:34+00:00" }, { "name": "psr/http-message", @@ -825,20 +898,23 @@ "request", "response" ], + "support": { + "source": "https://github.com/php-fig/http-message/tree/master" + }, "time": "2016-08-06T14:39:51+00:00" }, { "name": "psr/log", - "version": "1.1.3", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/php-fig/log.git", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc" + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/0f73288fd15629204f9d42b7055f72dacbe811fc", - "reference": "0f73288fd15629204f9d42b7055f72dacbe811fc", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", "shasum": "" }, "require": { @@ -862,7 +938,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common interface for logging libraries", @@ -872,7 +948,10 @@ "psr", "psr-3" ], - "time": "2020-03-23T09:12:05+00:00" + "support": { + "source": "https://github.com/php-fig/log/tree/1.1.4" + }, + "time": "2021-05-03T11:20:27+00:00" }, { "name": "ralouphie/getallheaders", @@ -912,20 +991,24 @@ } ], "description": "A polyfill for getallheaders.", + "support": { + "issues": "https://github.com/ralouphie/getallheaders/issues", + "source": "https://github.com/ralouphie/getallheaders/tree/develop" + }, "time": "2019-03-08T08:55:37+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.5.4", + "version": "3.7.1", "source": { "type": "git", "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", - "reference": "dceec07328401de6211037abbb18bda423677e26" + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/dceec07328401de6211037abbb18bda423677e26", - "reference": "dceec07328401de6211037abbb18bda423677e26", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/1359e176e9307e906dc3d890bcc9603ff6d90619", + "reference": "1359e176e9307e906dc3d890bcc9603ff6d90619", "shasum": "" }, "require": { @@ -963,24 +1046,99 @@ "phpcs", "standards" ], - "time": "2020-01-30T22:20:29+00:00" + "support": { + "issues": "https://github.com/squizlabs/PHP_CodeSniffer/issues", + "source": "https://github.com/squizlabs/PHP_CodeSniffer", + "wiki": "https://github.com/squizlabs/PHP_CodeSniffer/wiki" + }, + "time": "2022-06-18T07:21:10+00:00" }, { - "name": "symfony/polyfill-ctype", - "version": "v1.15.0", + "name": "symfony/deprecation-contracts", + "version": "v2.5.2", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14" + "url": "https://github.com/symfony/deprecation-contracts.git", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/4719fa9c18b0464d399f1a63bf624b42b6fa8d14", - "reference": "4719fa9c18b0464d399f1a63bf624b42b6fa8d14", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.5-dev" + }, + "thanks": { + "name": "symfony/contracts", + "url": "https://github.com/symfony/contracts" + } + }, + "autoload": { + "files": [ + "function.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "A generic function and convention to trigger deprecation notices", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.2" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" + }, + { + "name": "symfony/polyfill-ctype", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-ctype.git", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "provide": { + "ext-ctype": "*" }, "suggest": { "ext-ctype": "For best performance" @@ -988,16 +1146,20 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1021,6 +1183,9 @@ "polyfill", "portable" ], + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1035,38 +1200,134 @@ "type": "tidelift" } ], - "time": "2020-02-27T09:26:54+00:00" + "time": "2022-05-24T11:49:31+00:00" }, { - "name": "symfony/polyfill-php55", - "version": "v1.15.0", + "name": "symfony/polyfill-intl-idn", + "version": "v1.26.0", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-php55.git", - "reference": "bb47cf11bd615810c631c12caeadf70ea91d2d80" + "url": "https://github.com/symfony/polyfill-intl-idn.git", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/bb47cf11bd615810c631c12caeadf70ea91d2d80", - "reference": "bb47cf11bd615810c631c12caeadf70ea91d2d80", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/59a8d271f00dd0e4c2e518104cc7963f655a1aa8", + "reference": "59a8d271f00dd0e4c2e518104cc7963f655a1aa8", "shasum": "" }, "require": { - "ircmaxell/password-compat": "~1.0", - "php": ">=5.3.3" + "php": ">=7.1", + "symfony/polyfill-intl-normalizer": "^1.10", + "symfony/polyfill-php72": "^1.10" + }, + "suggest": { + "ext-intl": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.15-dev" + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php55\\": "" - }, "files": [ "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Idn\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Laurent Bassin", + "email": "laurent@bassin.info" + }, + { + "name": "Trevor Rowbotham", + "email": "trevor.rowbotham@pm.me" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "idn", + "intl", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-intl-normalizer", + "version": "v1.26.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-intl-normalizer.git", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd", + "reference": "219aa369ceff116e673852dce47c3a41794c14bd", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "suggest": { + "ext-intl": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, + "classmap": [ + "Resources/stubs" ] }, "notification-url": "https://packagist.org/downloads/", @@ -1083,6 +1344,76 @@ "homepage": "https://symfony.com/contributors" } ], + "description": "Symfony polyfill for intl's Normalizer class and related functions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "intl", + "normalizer", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/polyfill-php55", + "version": "v1.20.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php55.git", + "reference": "c17452124a883900e1d73961f9075a638399c1a0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php55/zipball/c17452124a883900e1d73961f9075a638399c1a0", + "reference": "c17452124a883900e1d73961f9075a638399c1a0", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "metapackage", + "extra": { + "branch-alias": { + "dev-main": "1.20-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], "description": "Symfony polyfill backporting some PHP 5.5+ features to lower PHP versions", "homepage": "https://symfony.com", "keywords": [ @@ -1091,20 +1422,113 @@ "portable", "shim" ], - "time": "2020-02-27T09:26:54+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php55/tree/v1.20.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2020-10-23T14:02:19+00:00" }, { - "name": "symfony/yaml", - "version": "v3.4.39", + "name": "symfony/polyfill-php72", + "version": "v1.26.0", "source": { "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "e701b47e11749970f63803879c4febb520f07b6c" + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e701b47e11749970f63803879c4febb520f07b6c", - "reference": "e701b47e11749970f63803879c4febb520f07b6c", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/bf44a9fd41feaac72b074de600314a93e2ae78e2", + "reference": "bf44a9fd41feaac72b074de600314a93e2ae78e2", + "shasum": "" + }, + "require": { + "php": ">=7.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.26-dev" + }, + "thanks": { + "name": "symfony/polyfill", + "url": "https://github.com/symfony/polyfill" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ], + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "support": { + "source": "https://github.com/symfony/polyfill-php72/tree/v1.26.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-05-24T11:49:31+00:00" + }, + { + "name": "symfony/yaml", + "version": "v3.4.47", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "88289caa3c166321883f67fe5130188ebbb47094" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/88289caa3c166321883f67fe5130188ebbb47094", + "reference": "88289caa3c166321883f67fe5130188ebbb47094", "shasum": "" }, "require": { @@ -1121,11 +1545,6 @@ "symfony/console": "For validating YAML files using the lint command" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" @@ -1150,6 +1569,9 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/yaml/tree/v3.4.47" + }, "funding": [ { "url": "https://symfony.com/sponsor", @@ -1164,15 +1586,13 @@ "type": "tidelift" } ], - "time": "2020-03-25T12:02:26+00:00" + "time": "2020-10-24T10:57:07+00:00" } ], "aliases": [], "minimum-stability": "stable", "stability-flags": { - "joomla/crowdin-sync": 20, - "joomla/cms-coding-standards": 20, - "joomla/coding-standards": 20 + "joomla/crowdin-sync": 20 }, "prefer-stable": false, "prefer-lowest": false, @@ -1181,5 +1601,5 @@ "ext-json": "*" }, "platform-dev": [], - "plugin-api-version": "1.1.0" + "plugin-api-version": "2.3.0" } diff --git a/ruleset.xml b/ruleset.xml new file mode 100644 index 0000000..49649d8 --- /dev/null +++ b/ruleset.xml @@ -0,0 +1,32 @@ + + + The Joomla CMS PSR-12 exceptions. + + + build/packag* + administrator/components/com_patchtester/vendor/* + + + + + + + + + + + + + + + build/patchtester/release\.php + + + + administrator/components/com_patchtester/script\.php + + + + administrator/components/com_patchtester/script\.php + +