/** * 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 (!\JSession::checkToken('request')) { $response = new \JResponseJson(new \Exception(\JText::_('JINVALID_TOKEN'), 403)); $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 $github = Helper::initializeGithub(); try { $rate = $github->authorization->getRateLimit(); } catch (\Exception $e) { $response = new \JResponseJson(new \Exception(\JText::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 \JResponseJson(new \Exception(\JText::sprintf('COM_PATCHTESTER_API_LIMIT_LIST', \JFactory::getDate($rate->resources->core->reset)), 429)); $this->getApplication()->sendHeaders(); echo json_encode($response); $this->getApplication()->close(1); } // TODO - Decouple the model and context? $model = new PullsModel('com_patchtester.fetch', null, \JFactory::getDbo()); // Initialize the state for the model $model->setState($this->initializeState($model)); try { // Sanity check, ensure there aren't any applied patches if (count($model->getAppliedPatches()) >= 1) { $response = new \JResponseJson(new \Exception(\JText::_('COM_PATCHTESTER_ERROR_APPLIED_PATCHES'), 500)); $this->getApplication()->sendHeaders(); echo json_encode($response); $this->getApplication()->close(1); } } catch (\Exception $e) { $response = new \JResponseJson($e); $this->getApplication()->sendHeaders(); echo json_encode($response); $this->getApplication()->close(1); } // We're able to successfully pull data, prepare our environment \JFactory::getSession()->set('com_patchtester_fetcher_page', 1); $response = new \JResponseJson(array('complete' => false, 'header' => \JText::_('COM_PATCHTESTER_FETCH_PROCESSING', true)), \JText::sprintf('COM_PATCHTESTER_FETCH_PAGE_NUMBER', 1), false, true); $this->getApplication()->sendHeaders(); echo json_encode($response); $this->getApplication()->close(); }
/** * 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 */ public function apply($id) { // Get the Github object $github = Helper::initializeGithub(); try { $rate = $github->authorization->getRateLimit(); } catch (\Exception $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e); } // If over the API limit, we can't build this list if ($rate->resources->core->remaining == 0) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_API_LIMIT_LIST', \JFactory::getDate($rate->resources->core->reset))); } try { $pull = $github->pulls->get($this->getState()->get('github_user'), $this->getState()->get('github_repo'), $id); } catch (\Exception $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e); } if (is_null($pull->head->repo)) { throw new \RuntimeException(\JText::_('COM_PATCHTESTER_REPO_IS_GONE')); } // Set up the JHttp object $options = new Registry(); $options->set('userAgent', 'JPatchTester/2.0'); $options->set('timeout', 120); try { $transport = \JHttpFactory::getHttp($options); $patch = $transport->get($pull->diff_url)->body; } catch (\Exception $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e); } $files = $this->parsePatch($patch); if (!$files) { return false; } foreach ($files as $file) { if ($file->action == 'deleted' && !file_exists(JPATH_ROOT . '/' . $file->old)) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_FILE_DELETED_DOES_NOT_EXIST_S', $file->old)); } if ($file->action == 'added' || $file->action == 'modified') { // If the backup file already exists, we can't apply the patch if (file_exists(JPATH_COMPONENT . '/backups/' . md5($file->new) . '.txt')) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_CONFLICT_S', $file->new)); } if ($file->action == 'modified' && !file_exists(JPATH_ROOT . '/' . $file->old)) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_FILE_MODIFIED_DOES_NOT_EXIST_S', $file->old)); } $url = 'https://raw.github.com/' . urlencode($pull->head->user->login) . '/' . urlencode($pull->head->repo->name) . '/' . urlencode($pull->head->ref) . '/' . $file->new; try { $file->body = $transport->get($url)->body; } catch (\Exception $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e); } } } jimport('joomla.filesystem.file'); // At this point, we have ensured that we have all the new files and there are no conflicts foreach ($files as $file) { // We only create a backup if the file already exists if ($file->action == 'deleted' || file_exists(JPATH_ROOT . '/' . $file->new) && $file->action == 'modified') { if (!\JFile::copy(\JPath::clean(JPATH_ROOT . '/' . $file->old), JPATH_COMPONENT . '/backups/' . md5($file->old) . '.txt')) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_CANNOT_COPY_FILE', JPATH_ROOT . '/' . $file->old, JPATH_COMPONENT . '/backups/' . md5($file->old) . '.txt')); } } switch ($file->action) { case 'modified': case 'added': if (!\JFile::write(\JPath::clean(JPATH_ROOT . '/' . $file->new), $file->body)) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_CANNOT_WRITE_FILE', JPATH_ROOT . '/' . $file->new)); } break; case 'deleted': if (!\JFile::delete(\JPath::clean(JPATH_ROOT . '/' . $file->old))) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_ROOT . '/' . $file->old)); } break; } } /** @var \PatchTester\Table\TestsTable $table */ $table = \JTable::getInstance('TestsTable', '\\PatchTester\\Table\\'); $table->pull_id = $pull->number; $table->data = json_encode($files); $table->patched_by = \JFactory::getUser()->id; $table->applied = 1; $table->applied_version = JVERSION; if (!$table->store()) { throw new \RuntimeException($table->getError()); } return true; }
/** * 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) { // Get the Github object $github = Helper::initializeGithub(); // If on page 1, dump the old data if ($page === 1) { $this->getDb()->truncateTable('#__patchtester_pulls'); } try { // TODO - Option to configure the batch size $pulls = $github->pulls->getList($this->getState()->get('github_user'), $this->getState()->get('github_repo'), 'open', $page, 100); } catch (\DomainException $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_GITHUB_FETCH', $e->getMessage()), $e->getCode(), $e); } $count = is_array($pulls) ? count($pulls) : 0; // If there are no pulls to insert then bail, assume we're finished if ($count === 0 || empty($pulls)) { return array('complete' => true); } $data = array(); foreach ($pulls as $pull) { // Build the data object to store in the database $pullData = array($pull->number, $this->getDb()->quote(\JHtml::_('string.truncate', $pull->title, 150)), $this->getDb()->quote(\JHtml::_('string.truncate', $pull->body, 100)), $this->getDb()->quote($pull->html_url), $this->getDb()->quote($pull->head->sha)); $data[] = implode($pullData, ','); } $this->getDb()->setQuery($this->getDb()->getQuery(true)->insert($this->getDb()->quoteName('#__patchtester_pulls'))->columns(array($this->getDb()->quoteName('pull_id'), $this->getDb()->quoteName('title'), $this->getDb()->quoteName('description'), $this->getDb()->quoteName('pull_url'), $this->getDb()->quoteName('sha')))->values($data)); try { $this->getDb()->execute(); } catch (\RuntimeException $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_INSERT_DATABASE', $e->getMessage()), $e->getCode(), $e); } // Need to make another request return array('complete' => false, 'page' => $page + 1); }
/** * 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 */ public function apply($id) { // Get the Github object $github = Helper::initializeGithub(); try { $rateResponse = $github->getRateLimit(); $rate = json_decode($rateResponse->body); } catch (UnexpectedResponse $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e); } // If over the API limit, we can't build this list if ($rate->resources->core->remaining == 0) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_API_LIMIT_LIST', \JFactory::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); } catch (UnexpectedResponse $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e); } if (is_null($pull->head->repo)) { throw new \RuntimeException(\JText::_('COM_PATCHTESTER_REPO_IS_GONE')); } try { $filesResponse = $github->getFilesForPullRequest($this->getState()->get('github_user'), $this->getState()->get('github_repo'), $id); $files = json_decode($filesResponse->body); } catch (UnexpectedResponse $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e); } if (!count($files)) { return false; } $parsedFiles = $this->parseFileList($files); foreach ($parsedFiles as $file) { switch ($file->action) { case 'deleted': if (!file_exists(JPATH_ROOT . '/' . $file->filename)) { throw new \RuntimeException(\JText::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(\JText::sprintf('COM_PATCHTESTER_CONFLICT_S', $file->filename)); } if ($file->action == 'modified' && !file_exists(JPATH_ROOT . '/' . $file->filename)) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_FILE_MODIFIED_DOES_NOT_EXIST_S', $file->filename)); } try { $contentsResponse = $github->getFileContents($pull->head->user->login, $this->getState()->get('github_repo'), $file->repofilename, urlencode($pull->head->ref)); $contents = json_decode($contentsResponse->body); // In case encoding type ever changes switch ($contents->encoding) { case 'base64': $file->body = base64_decode($contents->content); break; default: throw new \RuntimeException(\JText::_('COM_PATCHTESTER_ERROR_UNSUPPORTED_ENCODING')); } } catch (UnexpectedResponse $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_COULD_NOT_CONNECT_TO_GITHUB', $e->getMessage()), $e->getCode(), $e); } break; } } jimport('joomla.filesystem.file'); jimport('joomla.filesystem.path'); // 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 (!\JFile::copy(\JPath::clean($src), $dest)) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_CANNOT_COPY_FILE', $src, $dest)); } } switch ($file->action) { case 'modified': case 'added': if (!\JFile::write(\JPath::clean(JPATH_ROOT . '/' . $file->filename), $file->body)) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_CANNOT_WRITE_FILE', JPATH_ROOT . '/' . $file->filename)); } break; case 'deleted': if (!\JFile::delete(\JPath::clean(JPATH_ROOT . '/' . $file->filename))) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_ROOT . '/' . $file->filename)); } break; case 'renamed': if (!\JFile::delete(\JPath::clean(JPATH_ROOT . '/' . $file->originalFile))) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_CANNOT_DELETE_FILE', JPATH_ROOT . '/' . $file->originalFile)); } if (!\JFile::write(\JPath::clean(JPATH_ROOT . '/' . $file->filename), $file->body)) { throw new \RuntimeException(\JText::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); } $record = (object) array('pull_id' => $pull->number, 'data' => json_encode($parsedFiles), 'patched_by' => \JFactory::getUser()->id, 'applied' => 1, 'applied_version' => JVERSION); $db = $this->getDb(); $db->insertObject('#__patchtester_tests', $record); // Insert the retrieved commit SHA into the pulls table for this item $db->setQuery($db->getQuery(true)->update('#__patchtester_pulls')->set('sha = ' . $db->quote($pull->head->sha))->where($db->quoteName('pull_id') . ' = ' . (int) $id))->execute(); return true; }
/** * 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 on page 1, dump the old data if ($page === 1) { $this->getDb()->truncateTable('#__patchtester_pulls'); } try { // TODO - Option to configure the batch size $batchSize = 100; $pullsResponse = Helper::initializeGithub()->getOpenIssues($this->getState()->get('github_user'), $this->getState()->get('github_repo'), $page, $batchSize); $pulls = json_decode($pullsResponse->body); } catch (UnexpectedResponse $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_GITHUB_FETCH', $e->getMessage()), $e->getCode(), $e); } // 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'])) { preg_match('/(\\?page=[0-9]&per_page=' . $batchSize . '+>; rel=\\"last\\")/', $pullsResponse->headers['Link'], $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 array('complete' => true); } $data = array(); foreach ($pulls as $pull) { if (isset($pull->pull_request)) { // Check if this PR is RTC $isRTC = false; foreach ($pull->labels as $label) { if ($label->name === 'RTC') { $isRTC = true; break; } } // Build the data object to store in the database $pullData = array((int) $pull->number, $this->getDb()->quote(\JHtml::_('string.truncate', $pull->title, 150)), $this->getDb()->quote(\JHtml::_('string.truncate', $pull->body, 100)), $this->getDb()->quote($pull->pull_request->html_url), (int) $isRTC); $data[] = implode($pullData, ','); } } // If there are no pulls to insert then bail, assume we're finished if (count($data) === 0) { return array('complete' => true); } $this->getDb()->setQuery($this->getDb()->getQuery(true)->insert('#__patchtester_pulls')->columns(array('pull_id', 'title', 'description', 'pull_url', 'is_rtc'))->values($data)); try { $this->getDb()->execute(); } catch (\RuntimeException $e) { throw new \RuntimeException(\JText::sprintf('COM_PATCHTESTER_ERROR_INSERT_DATABASE', $e->getMessage()), $e->getCode(), $e); } // Need to make another request return array('complete' => false, 'page' => $page + 1, 'lastPage' => isset($lastPage) ? $lastPage : false); }