From f3d1468185eeeb01e8c01a13155f59296f4ccf4d Mon Sep 17 00:00:00 2001 From: Stefan Giehl Date: Mon, 27 Apr 2020 08:43:08 +0200 Subject: Moves some MobileMessaging API methods to a new model (#15842) * make MobileMessaging.sanitizePhoneNumber private * moves some methods to new model * submodule update --- plugins/MobileMessaging/API.php | 242 +++------------------ plugins/MobileMessaging/Controller.php | 3 +- plugins/MobileMessaging/MobileMessaging.php | 15 +- plugins/MobileMessaging/Model.php | 193 ++++++++++++++++ .../tests/Integration/MobileMessagingTest.php | 43 ++-- 5 files changed, 250 insertions(+), 246 deletions(-) create mode 100644 plugins/MobileMessaging/Model.php (limited to 'plugins/MobileMessaging') diff --git a/plugins/MobileMessaging/API.php b/plugins/MobileMessaging/API.php index 62e10f5768..06bd65ea84 100644 --- a/plugins/MobileMessaging/API.php +++ b/plugins/MobileMessaging/API.php @@ -25,6 +25,14 @@ class API extends \Piwik\Plugin\API const VERIFICATION_CODE_LENGTH = 5; const SMS_FROM = 'Matomo'; + /** @var Model $model */ + protected $model; + + public function __construct(Model $model) + { + $this->model = $model; + } + /** * determine if SMS API credential are available for the current user * @@ -34,31 +42,10 @@ class API extends \Piwik\Plugin\API { Piwik::checkUserHasSomeViewAccess(); - $credential = $this->getSMSAPICredential(); + $credential = $this->model->getSMSAPICredential(); return isset($credential[MobileMessaging::API_KEY_OPTION]); } - private function getSMSAPICredential() - { - $settings = $this->getCredentialManagerSettings(); - - $credentials = isset($settings[MobileMessaging::API_KEY_OPTION]) ? $settings[MobileMessaging::API_KEY_OPTION] : null; - - // fallback for older values, where api key has been stored as string value - if (!empty($credentials) && !is_array($credentials)) { - $credentials = array( - 'apiKey' => $credentials - ); - } - - return array( - MobileMessaging::PROVIDER_OPTION => - isset($settings[MobileMessaging::PROVIDER_OPTION]) ? $settings[MobileMessaging::PROVIDER_OPTION] : null, - MobileMessaging::API_KEY_OPTION => - $credentials, - ); - } - /** * return the SMS API Provider for the current user * @@ -67,7 +54,7 @@ class API extends \Piwik\Plugin\API public function getSMSProvider() { $this->checkCredentialManagementRights(); - $credential = $this->getSMSAPICredential(); + $credential = $this->model->getSMSAPICredential(); return $credential[MobileMessaging::PROVIDER_OPTION]; } @@ -86,12 +73,12 @@ class API extends \Piwik\Plugin\API $smsProviderInstance = SMSProvider::factory($provider); $smsProviderInstance->verifyCredential($credentials); - $settings = $this->getCredentialManagerSettings(); + $settings = $this->model->getCredentialManagerSettings(); $settings[MobileMessaging::PROVIDER_OPTION] = $provider; $settings[MobileMessaging::API_KEY_OPTION] = $credentials; - $this->setCredentialManagerSettings($settings); + $this->model->setCredentialManagerSettings($settings); return true; } @@ -107,7 +94,7 @@ class API extends \Piwik\Plugin\API { Piwik::checkUserIsNotAnonymous(); - $phoneNumber = self::sanitizePhoneNumber($phoneNumber); + $phoneNumber = $this->sanitizePhoneNumber($phoneNumber); $verificationCode = ""; for ($i = 0; $i < self::VERIFICATION_CODE_LENGTH; $i++) { @@ -123,13 +110,13 @@ class API extends \Piwik\Plugin\API ) ); - $this->sendSMS($smsText, $phoneNumber, self::SMS_FROM); + $this->model->sendSMS($smsText, $phoneNumber, self::SMS_FROM); - $phoneNumbers = $this->retrievePhoneNumbers(); + $phoneNumbers = $this->model->retrievePhoneNumbers(Piwik::getCurrentUserLogin()); $phoneNumbers[$phoneNumber] = $verificationCode; - $this->savePhoneNumbers($phoneNumbers); + $this->model->savePhoneNumbers(Piwik::getCurrentUserLogin(), $phoneNumbers); - $this->increaseCount(MobileMessaging::PHONE_NUMBER_VALIDATION_REQUEST_COUNT_OPTION, $phoneNumber); + $this->model->increaseCount(Piwik::getCurrentUserLogin(), MobileMessaging::PHONE_NUMBER_VALIDATION_REQUEST_COUNT_OPTION, $phoneNumber); return true; } @@ -137,42 +124,14 @@ class API extends \Piwik\Plugin\API /** * sanitize phone number * - * @ignore * @param string $phoneNumber * @return string sanitized phone number */ - public static function sanitizePhoneNumber($phoneNumber) + private function sanitizePhoneNumber($phoneNumber) { return str_replace(' ', '', $phoneNumber); } - /** - * send a SMS - * - * @param string $content - * @param string $phoneNumber - * @param string $from - * @return bool true - * @ignore - */ - public function sendSMS($content, $phoneNumber, $from) - { - Piwik::checkUserIsNotAnonymous(); - - $credential = $this->getSMSAPICredential(); - $SMSProvider = SMSProvider::factory($credential[MobileMessaging::PROVIDER_OPTION]); - $SMSProvider->sendSMS( - $credential[MobileMessaging::API_KEY_OPTION], - $content, - $phoneNumber, - $from - ); - - $this->increaseCount(MobileMessaging::SMS_SENT_COUNT_OPTION, $phoneNumber); - - return true; - } - /** * get remaining credit * @@ -182,7 +141,7 @@ class API extends \Piwik\Plugin\API { $this->checkCredentialManagementRights(); - $credential = $this->getSMSAPICredential(); + $credential = $this->model->getSMSAPICredential(); $SMSProvider = SMSProvider::factory($credential[MobileMessaging::PROVIDER_OPTION]); return $SMSProvider->getCreditLeft( $credential[MobileMessaging::API_KEY_OPTION] @@ -200,9 +159,9 @@ class API extends \Piwik\Plugin\API { Piwik::checkUserIsNotAnonymous(); - $phoneNumbers = $this->retrievePhoneNumbers(); + $phoneNumbers = $this->model->retrievePhoneNumbers(Piwik::getCurrentUserLogin()); unset($phoneNumbers[$phoneNumber]); - $this->savePhoneNumbers($phoneNumbers); + $this->model->savePhoneNumbers(Piwik::getCurrentUserLogin(), $phoneNumbers); /** * Triggered after a phone number has been deleted. This event should be used to clean up any data that is @@ -223,48 +182,6 @@ class API extends \Piwik\Plugin\API return true; } - private function retrievePhoneNumbers() - { - $settings = $this->getCurrentUserSettings(); - - $phoneNumbers = array(); - if (isset($settings[MobileMessaging::PHONE_NUMBERS_OPTION])) { - $phoneNumbers = $settings[MobileMessaging::PHONE_NUMBERS_OPTION]; - } - - return $phoneNumbers; - } - - private function savePhoneNumbers($phoneNumbers) - { - $settings = $this->getCurrentUserSettings(); - - $settings[MobileMessaging::PHONE_NUMBERS_OPTION] = $phoneNumbers; - - $this->setCurrentUserSettings($settings); - } - - private function increaseCount($option, $phoneNumber) - { - $settings = $this->getCurrentUserSettings(); - - $counts = array(); - if (isset($settings[$option])) { - $counts = $settings[$option]; - } - - $countToUpdate = 0; - if (isset($counts[$phoneNumber])) { - $countToUpdate = $counts[$phoneNumber]; - } - - $counts[$phoneNumber] = $countToUpdate + 1; - - $settings[$option] = $counts; - - $this->setCurrentUserSettings($settings); - } - /** * validate phone number * @@ -277,13 +194,13 @@ class API extends \Piwik\Plugin\API { Piwik::checkUserIsNotAnonymous(); - $phoneNumbers = $this->retrievePhoneNumbers(); + $phoneNumbers = $this->model->retrievePhoneNumbers(Piwik::getCurrentUserLogin()); if (isset($phoneNumbers[$phoneNumber])) { if ($verificationCode == $phoneNumbers[$phoneNumber]) { $phoneNumbers[$phoneNumber] = null; - $this->savePhoneNumbers($phoneNumbers); + $this->model->savePhoneNumbers(Piwik::getCurrentUserLogin(), $phoneNumbers); return true; } } @@ -291,53 +208,6 @@ class API extends \Piwik\Plugin\API return false; } - /** - * get phone number list - * - * @return array $phoneNumber => $isValidated - * @ignore - */ - public function getPhoneNumbers() - { - Piwik::checkUserIsNotAnonymous(); - - $rawPhoneNumbers = $this->retrievePhoneNumbers(); - - $phoneNumbers = array(); - foreach ($rawPhoneNumbers as $phoneNumber => $verificationCode) { - $phoneNumbers[$phoneNumber] = self::isActivated($verificationCode); - } - - return $phoneNumbers; - } - - /** - * get activated phone number list - * - * @return array $phoneNumber - * @ignore - */ - public function getActivatedPhoneNumbers() - { - Piwik::checkUserIsNotAnonymous(); - - $phoneNumbers = $this->retrievePhoneNumbers(); - - $activatedPhoneNumbers = array(); - foreach ($phoneNumbers as $phoneNumber => $verificationCode) { - if (self::isActivated($verificationCode)) { - $activatedPhoneNumbers[] = $phoneNumber; - } - } - - return $activatedPhoneNumbers; - } - - private static function isActivated($verificationCode) - { - return $verificationCode === null; - } - /** * delete the SMS API credential * @@ -347,67 +217,15 @@ class API extends \Piwik\Plugin\API { $this->checkCredentialManagementRights(); - $settings = $this->getCredentialManagerSettings(); + $settings = $this->model->getCredentialManagerSettings(); $settings[MobileMessaging::API_KEY_OPTION] = null; - $this->setCredentialManagerSettings($settings); + $this->model->setCredentialManagerSettings($settings); return true; } - private function checkCredentialManagementRights() - { - $this->getDelegatedManagement() ? Piwik::checkUserIsNotAnonymous() : Piwik::checkUserHasSuperUserAccess(); - } - - private function setUserSettings($user, $settings) - { - Option::set( - $user . MobileMessaging::USER_SETTINGS_POSTFIX_OPTION, - json_encode($settings) - ); - } - - private function setCurrentUserSettings($settings) - { - $this->setUserSettings(Piwik::getCurrentUserLogin(), $settings); - } - - private function setCredentialManagerSettings($settings) - { - $this->setUserSettings($this->getCredentialManagerLogin(), $settings); - } - - private function getCredentialManagerLogin() - { - return $this->getDelegatedManagement() ? Piwik::getCurrentUserLogin() : ''; - } - - private function getUserSettings($user) - { - $optionIndex = $user . MobileMessaging::USER_SETTINGS_POSTFIX_OPTION; - $userSettings = Option::get($optionIndex); - - if (empty($userSettings)) { - $userSettings = array(); - } else { - $userSettings = json_decode($userSettings, true); - } - - return $userSettings; - } - - private function getCredentialManagerSettings() - { - return $this->getUserSettings($this->getCredentialManagerLogin()); - } - - private function getCurrentUserSettings() - { - return $this->getUserSettings(Piwik::getCurrentUserLogin()); - } - /** * Specify if normal users can manage their own SMS API credential * @@ -416,7 +234,7 @@ class API extends \Piwik\Plugin\API public function setDelegatedManagement($delegatedManagement) { Piwik::checkUserHasSuperUserAccess(); - Option::set(MobileMessaging::DELEGATED_MANAGEMENT_OPTION, $delegatedManagement); + $this->model->setDelegatedManagement($delegatedManagement); } /** @@ -427,7 +245,11 @@ class API extends \Piwik\Plugin\API public function getDelegatedManagement() { Piwik::checkUserHasSomeViewAccess(); - $option = Option::get(MobileMessaging::DELEGATED_MANAGEMENT_OPTION); - return $option === 'true'; + return $this->model->getDelegatedManagement(); + } + + private function checkCredentialManagementRights() + { + $this->getDelegatedManagement() ? Piwik::checkUserIsNotAnonymous() : Piwik::checkUserHasSuperUserAccess(); } } diff --git a/plugins/MobileMessaging/Controller.php b/plugins/MobileMessaging/Controller.php index e931942767..42cff6ca12 100644 --- a/plugins/MobileMessaging/Controller.php +++ b/plugins/MobileMessaging/Controller.php @@ -63,6 +63,7 @@ class Controller extends ControllerAdmin $view->isSuperUser = Piwik::hasUserSuperUserAccess(); $mobileMessagingAPI = API::getInstance(); + $model = new Model(); $view->delegatedManagement = $mobileMessagingAPI->getDelegatedManagement(); $view->credentialSupplied = $mobileMessagingAPI->areSMSAPICredentialProvided(); $view->accountManagedByCurrentUser = $view->isSuperUser || $view->delegatedManagement; @@ -131,7 +132,7 @@ class Controller extends ControllerAdmin } $view->countries = $countries; - $view->phoneNumbers = $mobileMessagingAPI->getPhoneNumbers(); + $view->phoneNumbers = $model->getPhoneNumbers(Piwik::getCurrentUserLogin()); $this->setBasicVariablesView($view); } diff --git a/plugins/MobileMessaging/MobileMessaging.php b/plugins/MobileMessaging/MobileMessaging.php index e5e0be1cca..9b62119660 100644 --- a/plugins/MobileMessaging/MobileMessaging.php +++ b/plugins/MobileMessaging/MobileMessaging.php @@ -114,7 +114,7 @@ class MobileMessaging extends \Piwik\Plugin { if (self::manageEvent($reportType)) { // phone number validation - $availablePhoneNumbers = APIMobileMessaging::getInstance()->getActivatedPhoneNumbers(); + $availablePhoneNumbers = $this->getModel()->getActivatedPhoneNumbers(Piwik::getCurrentUserLogin()); $phoneNumbers = $parameters[self::PHONE_NUMBERS_PARAMETER]; foreach ($phoneNumbers as $key => $phoneNumber) { @@ -205,9 +205,9 @@ class MobileMessaging extends \Piwik\Plugin $reportSubject = Piwik::translate('General_Reports'); } - $mobileMessagingAPI = APIMobileMessaging::getInstance(); + $model = $this->getModel(); foreach ($phoneNumbers as $phoneNumber) { - $mobileMessagingAPI->sendSMS( + $model->sendSMS( $contents, $phoneNumber, $reportSubject @@ -216,7 +216,7 @@ class MobileMessaging extends \Piwik\Plugin } } - public static function template_reportParametersScheduledReports(&$out, $context = '') + public function template_reportParametersScheduledReports(&$out, $context = '') { if (Piwik::isUserIsAnonymous()) { return; @@ -225,7 +225,7 @@ class MobileMessaging extends \Piwik\Plugin $view = new View('@MobileMessaging/reportParametersScheduledReports'); $view->reportType = self::MOBILE_TYPE; $view->context = $context; - $numbers = APIMobileMessaging::getInstance()->getActivatedPhoneNumbers(); + $numbers = $this->getModel()->getActivatedPhoneNumbers(Piwik::getCurrentUserLogin()); $phoneNumbers = array(); if (!empty($numbers)) { @@ -270,4 +270,9 @@ class MobileMessaging extends \Piwik\Plugin // when it becomes available, all the MobileMessaging settings (API credentials, phone numbers, etc..) should be removed from the option table return; } + + protected function getModel() + { + return new Model(); + } } diff --git a/plugins/MobileMessaging/Model.php b/plugins/MobileMessaging/Model.php new file mode 100644 index 0000000000..cced8dfb4d --- /dev/null +++ b/plugins/MobileMessaging/Model.php @@ -0,0 +1,193 @@ +getSMSAPICredential(); + $SMSProvider = SMSProvider::factory($credential[MobileMessaging::PROVIDER_OPTION]); + $SMSProvider->sendSMS( + $credential[MobileMessaging::API_KEY_OPTION], + $content, + $phoneNumber, + $from + ); + + $this->increaseCount(Piwik::getCurrentUserLogin(), MobileMessaging::SMS_SENT_COUNT_OPTION, $phoneNumber); + + return true; + } + + /** + * get activated phone number list + * + * @param string $login + * @return array $phoneNumber + */ + public function getActivatedPhoneNumbers($login) + { + $phoneNumbers = $this->retrievePhoneNumbers($login); + + $activatedPhoneNumbers = array(); + foreach ($phoneNumbers as $phoneNumber => $verificationCode) { + if ($this->isActivated($verificationCode)) { + $activatedPhoneNumbers[] = $phoneNumber; + } + } + + return $activatedPhoneNumbers; + } + + public function retrievePhoneNumbers($login) + { + $settings = $this->getUserSettings($login); + + $phoneNumbers = array(); + if (isset($settings[MobileMessaging::PHONE_NUMBERS_OPTION])) { + $phoneNumbers = $settings[MobileMessaging::PHONE_NUMBERS_OPTION]; + } + + return $phoneNumbers; + } + + public function savePhoneNumbers($login, $phoneNumbers) + { + $settings = $this->getUserSettings($login); + + $settings[MobileMessaging::PHONE_NUMBERS_OPTION] = $phoneNumbers; + + $this->setUserSettings($login, $settings); + } + + public function increaseCount($login, $option, $phoneNumber) + { + $settings = $this->getUserSettings($login); + + $counts = array(); + if (isset($settings[$option])) { + $counts = $settings[$option]; + } + + $countToUpdate = 0; + if (isset($counts[$phoneNumber])) { + $countToUpdate = $counts[$phoneNumber]; + } + + $counts[$phoneNumber] = $countToUpdate + 1; + + $settings[$option] = $counts; + + $this->setUserSettings($login, $settings); + } + + public function getSMSAPICredential() + { + $settings = $this->getCredentialManagerSettings(); + + $credentials = isset($settings[MobileMessaging::API_KEY_OPTION]) ? $settings[MobileMessaging::API_KEY_OPTION] : null; + + // fallback for older values, where api key has been stored as string value + if (!empty($credentials) && !is_array($credentials)) { + $credentials = array( + 'apiKey' => $credentials + ); + } + + return array( + MobileMessaging::PROVIDER_OPTION => + isset($settings[MobileMessaging::PROVIDER_OPTION]) ? $settings[MobileMessaging::PROVIDER_OPTION] : null, + MobileMessaging::API_KEY_OPTION => + $credentials, + ); + } + + /** + * get phone number list + * + * @param string $login + * @return array $phoneNumber => $isValidated + */ + public function getPhoneNumbers($login) + { + $rawPhoneNumbers = $this->retrievePhoneNumbers($login); + + $phoneNumbers = array(); + foreach ($rawPhoneNumbers as $phoneNumber => $verificationCode) { + $phoneNumbers[$phoneNumber] = $this->isActivated($verificationCode); + } + + return $phoneNumbers; + } + + public function setCredentialManagerSettings($settings) + { + $this->setUserSettings($this->getCredentialManagerLogin(), $settings); + } + + public function getCredentialManagerSettings() + { + return $this->getUserSettings($this->getCredentialManagerLogin()); + } + + public function getDelegatedManagement() + { + $option = Option::get(MobileMessaging::DELEGATED_MANAGEMENT_OPTION); + return $option === 'true'; + } + + public function setDelegatedManagement($delegatedManagement) + { + Option::set(MobileMessaging::DELEGATED_MANAGEMENT_OPTION, $delegatedManagement); + } + + private function isActivated($verificationCode) + { + return $verificationCode === null; + } + + private function setUserSettings($login, $settings) + { + Option::set( + $login . MobileMessaging::USER_SETTINGS_POSTFIX_OPTION, + json_encode($settings) + ); + } + + private function getCredentialManagerLogin() + { + return $this->getDelegatedManagement() ? Piwik::getCurrentUserLogin() : ''; + } + + private function getUserSettings($user) + { + $optionIndex = $user . MobileMessaging::USER_SETTINGS_POSTFIX_OPTION; + $userSettings = Option::get($optionIndex); + + if (empty($userSettings)) { + $userSettings = array(); + } else { + $userSettings = json_decode($userSettings, true); + } + + return $userSettings; + } +} \ No newline at end of file diff --git a/plugins/MobileMessaging/tests/Integration/MobileMessagingTest.php b/plugins/MobileMessaging/tests/Integration/MobileMessagingTest.php index f390541104..9c29ebe7d2 100644 --- a/plugins/MobileMessaging/tests/Integration/MobileMessagingTest.php +++ b/plugins/MobileMessaging/tests/Integration/MobileMessagingTest.php @@ -8,8 +8,10 @@ namespace Piwik\Plugins\MobileMessaging\tests\Integration; +use Piwik\Piwik; use Piwik\Plugins\MobileMessaging\API as APIMobileMessaging; use Piwik\Plugins\MobileMessaging\MobileMessaging; +use Piwik\Plugins\MobileMessaging\Model; use Piwik\Plugins\MobileMessaging\SMSProvider; use Piwik\Plugins\ScheduledReports\API as APIScheduledReports; use Piwik\Plugins\SitesManager\API as APISitesManager; @@ -40,8 +42,6 @@ class MobileMessagingTest extends IntegrationTestCase /** * When the MultiSites plugin is not activated, the SMS content should invite the user to activate it back - * - * @group Plugins */ public function testWarnUserViaSMSMultiSitesDeactivated() { @@ -157,8 +157,6 @@ class MobileMessagingTest extends IntegrationTestCase } /** - * @group Plugins - * * @dataProvider getTruncateTestCases */ public function testTruncate($expected, $stringToTruncate, $maximumNumberOfConcatenatedSMS, $appendedString) @@ -183,8 +181,6 @@ class MobileMessagingTest extends IntegrationTestCase } /** - * @group Plugins - * * @dataProvider getContainsUCS2CharactersTestCases */ public function testContainsUCS2Characters($expected, $stringToTest) @@ -195,23 +191,13 @@ class MobileMessagingTest extends IntegrationTestCase ); } - /** - * @group Plugins - */ - public function testSanitizePhoneNumber() - { - $this->assertEquals('676932647', APIMobileMessaging::sanitizePhoneNumber(' 6 76 93 26 47')); - } - - /** - * @group Plugins - */ public function testPhoneNumberIsSanitized() { $mobileMessagingAPI = APIMobileMessaging::getInstance(); + $model = new Model(); $mobileMessagingAPI->setSMSAPICredential('StubbedProvider', ''); $mobileMessagingAPI->addPhoneNumber(' 6 76 93 26 47'); - $this->assertEquals('676932647', key($mobileMessagingAPI->getPhoneNumbers())); + $this->assertEquals('676932647', key($model->getPhoneNumbers(Piwik::getCurrentUserLogin()))); } /** @@ -226,8 +212,6 @@ class MobileMessagingTest extends IntegrationTestCase } /** - * @group Plugins - * * @dataProvider getSendReportTestCases */ public function testSendReport($expectedReportContent, $expectedPhoneNumber, $expectedFrom, $reportContent, $phoneNumber, $reportSubject) @@ -236,22 +220,21 @@ class MobileMessagingTest extends IntegrationTestCase 'parameters' => array(MobileMessaging::PHONE_NUMBERS_PARAMETER => array($phoneNumber)), ); - $stubbedAPIMobileMessaging = $this->getMockBuilder('\\Piwik\\Plugins\\MobileMessaging\\API') - ->setMethods(array('sendSMS', 'getInstance')) - ->disableOriginalConstructor() - ->getMock(); - $stubbedAPIMobileMessaging->expects($this->once())->method('sendSMS')->with( + $stubbedModel = $this->getMockBuilder(Model::class) + ->onlyMethods(array('sendSMS')) + ->getMock(); + $stubbedModel->expects($this->once())->method('sendSMS')->with( $this->equalTo($expectedReportContent, 0), $this->equalTo($expectedPhoneNumber, 1), $this->equalTo($expectedFrom, 2) ); - \Piwik\Plugins\MobileMessaging\API::setSingletonInstance($stubbedAPIMobileMessaging); - - $mobileMessaging = new MobileMessaging(); - $mobileMessaging->sendReport(MobileMessaging::MOBILE_TYPE, $report, $reportContent, null, null, $reportSubject, null, null, null, false); + $stubbedMobileMessaging = $this->getMockBuilder(MobileMessaging::class) + ->onlyMethods(['getModel']) + ->getMock(); - \Piwik\Plugins\MobileMessaging\API::unsetInstance(); + $stubbedMobileMessaging->expects($this->once())->method('getModel')->will($this->returnValue($stubbedModel)); + $stubbedMobileMessaging->sendReport(MobileMessaging::MOBILE_TYPE, $report, $reportContent, null, null, $reportSubject, null, null, null, false); } public function provideContainerConfig() -- cgit v1.2.3