{"action":"created","comment":{"url":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/comments/244406230","pull_request_review_id":188368173,"id":244406230,"node_id":"MDI0OlB1bGxSZXF1ZXN0UmV2aWV3Q29tbWVudDI0NDQwNjIzMA==","diff_hunk":"@@ -0,0 +1,1049 @@\n+<?php\n+\n+/**\n+ *\n+ * SugarCRM Community Edition is a customer relationship management program developed by\n+ * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.\n+ *\n+ * SuiteCRM is an extension to SugarCRM Community Edition developed by SalesAgility Ltd.\n+ * Copyright (C) 2011 - 2018 SalesAgility Ltd.\n+ *\n+ * This program is free software; you can redistribute it and/or modify it under\n+ * the terms of the GNU Affero General Public License version 3 as published by the\n+ * Free Software Foundation with the addition of the following permission added\n+ * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK\n+ * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY\n+ * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.\n+ *\n+ * This program is distributed in the hope that it will be useful, but WITHOUT\n+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS\n+ * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more\n+ * details.\n+ *\n+ * You should have received a copy of the GNU Affero General Public License along with\n+ * this program; if not, see http://www.gnu.org/licenses or write to the Free\n+ * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA\n+ * 02110-1301 USA.\n+ *\n+ * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,\n+ * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.\n+ *\n+ * The interactive user interfaces in modified source and object code versions\n+ * of this program must display Appropriate Legal Notices, as required under\n+ * Section 5 of the GNU Affero General Public License version 3.\n+ *\n+ * In accordance with Section 7(b) of the GNU Affero General Public License version 3,\n+ * these Appropriate Legal Notices must retain the display of the \"Powered by\n+ * SugarCRM\" logo and \"Supercharged by SuiteCRM\" logo. If the display of the logos is not\n+ * reasonably feasible for technical reasons, the Appropriate Legal Notices must\n+ * display the words \"Powered by SugarCRM\" and \"Supercharged by SuiteCRM\".\n+ */\n+if (!defined('sugarEntry') || !sugarEntry) {\n+    die('Not A Valid Entry Point');\n+}\n+\n+require_once __DIR__ . '/../../modules/Users/User.php';\n+require_once __DIR__ . '/../../modules/Meetings/Meeting.php';\n+require_once __DIR__ . '/GoogleSyncExceptions.php';\n+\n+use SuiteCRM\\Utility\\SuiteValidator;\n+\n+/**\n+ * Implements Google Calendar Syncing\n+ *\n+ * @license https://raw.githubusercontent.com/salesagility/SuiteCRM/master/LICENSE.txt\n+ * GNU Affero General Public License version 3\n+ * @author Benjamin Long <ben@offsite.guru>\n+ */\n+\n+class GoogleSyncBase\n+{    \n+    /** @var User The SuiteCRM User Bean we're currently working with */\n+    protected $workingUser;\n+\n+    /** @var \\Google_Client The Google client object for the current sync job */\n+    protected $gClient;\n+\n+    /** @var \\Google_Service_Calendar The Google Calendar Service Object */\n+    protected $gService;\n+\n+    /** @var array The Google AuthcConfig json */\n+    protected $authJson = array();\n+\n+    /** @var string The local timezone */\n+    protected $timezone;\n+\n+    /** @var string The Calendar ID */\n+    protected $calendarId;\n+\n+    /** @var array An array of SuiteCRM meeting id's that we've already synced this session */\n+    protected $syncedList = array();\n+\n+    /** @var object A Database Instance */\n+    protected $db;\n+\n+    /** @var object A Logger Instance */\n+    protected $logger;\n+\n+    /** @var string The log level before we begin */\n+    protected $oldLogLevel;\n+\n+    /**\n+     * Class Constructor\n+     * \n+     * @param array $sugarConfig - using $sugar_config as a dependency\n+     */\n+    public function __construct($sugarConfig)\n+    {\n+        // This sets the log level to a variable that can be set on the command line while running cron.php on the server. It's for debugging only.\n+        // EXAMPLE: $ GSYNC_LOGLEVEL=debug php cron.php\n+        $this->logger = LoggerManager::getLogger();\n+        if (isset($_SERVER['GSYNC_LOGLEVEL'])) { // TODO: is it possible to using $sugar_config instead superglobals? (a global usage is one step closer to clean code than using a superglobal)\n+            $this->oldLogLevel = $this->logger->getLogLevel();\n+            $this->logger->setLevel($_SERVER['GSYNC_LOGLEVEL']); // TODO: it changes the log level but as it is a global, it will affect the other parts of core until this class __destructor is called! (and it's bad)\n+            $this->logger->fatal(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'Log Level Set To: ' . $_SERVER['GSYNC_LOGLEVEL']);\n+        }\n+        $this->timezone = date_default_timezone_get(); // This defaults to the server timezone. Overridden later.\n+        $this->authJson = $this->getAuthJson($sugarConfig);\n+        $this->db = DBManagerFactory::getInstance();\n+        $this->logger->debug(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . '__construct');\n+    }\n+\n+    /**\n+     * Class Destructor\n+     */\n+    public function __destruct()\n+    {\n+        // Set the log level back to the original value\n+        $this->logger->debug(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . '__destruct');\n+        if (isset($this->oldLogLevel)) {\n+            $this->logger->fatal(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'Setting Log Level Back To: ' . $this->oldLogLevel);\n+            $this->logger->setLevel($this->oldLogLevel);\n+        }\n+    }\n+\n+    /**\n+     * Gets the auth json string from the system\n+     *\n+     * @param array $sugarConfig\n+     * @return array|false json on success, false on failure\n+     */\n+    protected function getAuthJson($sugarConfig)\n+    {\n+        $authJson_local = json_decode(base64_decode($sugarConfig['google_auth_json']), true);\n+        if (!$authJson_local) {\n+            // The authconfig json string is invalid json\n+            // TODO: It should be an exception (do not leave same 'false' return value for differenc failure reason, Inform the caller about this problem)\n+            $this->logger->fatal(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'AuthConfig is not proper JSON string');\n+            return false;\n+        } elseif (!array_key_exists('web', $authJson_local)) {\n+            // The authconfig is valid json, but the 'web' key is missing. This is not a valid authconfig.\n+            // TODO: It should be an exception (do not leave same 'false' return value for differenc failure reason, Inform the caller about this problem)\n+            $this->logger->fatal(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'AuthConfig is missing required [web] key');\n+            return false;\n+        }\n+        return $authJson_local;\n+    }\n+\n+    /**\n+     * Creates and Sets the Google client in the object\n+     *\n+     * @param string $id : the SuiteCRM user id\n+     *\n+     * @return bool Success/Failure\n+     * @throws GoogleSyncException\n+     */\n+    protected function setClient($id)\n+    {\n+        $isValidator = new SuiteValidator();\n+        if (!$isValidator->isValidId($id)) {\n+            throw new GoogleSyncException('Google Sync Base trying to set Client but given an invalid ID: ' . $id, GoogleSyncException::INVALID_CLIENT_ID);\n+        }\n+        \n+        if (!$gClient_local = $this->getClient($id)) {\n+            return false;\n+        }\n+        $this->gClient = $gClient_local;\n+        return true;\n+    }\n+\n+    /**\n+     * Set the Google client up for the user by id\n+     *\n+     * @param string $id : the SuiteCRM user id\n+     *\n+     * @return \\Google_Client|false Google_Client on success. False on failure.\n+     * @throws GoogleSyncException if unable to retrive the user\n+     */\n+    protected function getClient($id)\n+    {\n+        $isValidator = new SuiteValidator();\n+        if (!$isValidator->isValidId($id)) {\n+            throw new GoogleSyncException('GoogleSyncBase trying to get Client but given ID is invalid: ' . $id, GoogleSyncException::INVALID_CLIENT_ID);\n+        }\n+\n+        // Retrieve user bean\n+        if (!isset($this->workingUser)) {\n+            $this->workingUser = BeanFactory::getBean('Users', $id);\n+            if (!$this->workingUser) {\n+                throw new GoogleSyncException('Unable to retrieve a User bean', GoogleSyncException::UNABLE_TO_RETRIEVE_USER);\n+            }\n+        }\n+\n+        // Retrieve Access Token JSON from user preference\n+        $accessToken = json_decode(base64_decode($this->workingUser->getPreference('GoogleApiToken', 'GoogleSync')), true);\n+        if (!array_key_exists('access_token', $accessToken)) {\n+            // The Token is invalid JSON or missing\n+            // TODO: It should be an exception (do not leave same 'false' return value for differenc failure reason, Inform the caller about this problem)\n+            $this->logger->fatal(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'Invalid or Missing AuthToken');\n+            return false;\n+        }\n+        // The refresh token is only provided once, on first authentication. It must be added afterwards.\n+        if (!array_key_exists('refresh_token', $accessToken)) {\n+            $accessToken['refresh_token'] = base64_decode($this->workingUser->getPreference('GoogleApiRefreshToken', 'GoogleSync'));\n+        }\n+\n+        // New Google Client and refresh the token if needed\n+        try {\n+            $client = $this->getGoogleClient($accessToken);\n+        } catch (Exception $e) {\n+            // TODO: handle the exception instead just loggin it, it will be logged if unhandled by suitecrm core.\n+            $this->logger->fatal('Caught exception: ',  $e->getMessage());\n+        }\n+\n+        if (!$client) {\n+            return false;\n+        }\n+        return $client;\n+    }\n+\n+    /**\n+     * New Google Client and refresh the token if needed\n+     *\n+     * @param array $accessToken\n+     * @return \\Google_Client or false on Exception\n+     * @throws GoogleSyncException If the refresh token is missing\n+     * @throws Exception rethrows if caught from Google_Client::fetchAccessTokenWithRefreshToken\n+     */\n+    protected function getGoogleClient($accessToken)\n+    {\n+        // New Google Client\n+        $client = new Google_Client();\n+        $client->setApplicationName('SuiteCRM');\n+        $client->setScopes(Google_Service_Calendar::CALENDAR);\n+        $client->setAccessType('offline');\n+        $client->setAuthConfig($this->authJson);\n+        $client->setAccessToken($accessToken);\n+\n+        // Refresh the token if needed\n+        if ($client->isAccessTokenExpired()) {\n+            $this->logger->info(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'Refreshing Access Token');\n+            $refreshToken = $client->getRefreshToken();\n+            if (!empty($refreshToken)) {\n+                try {\n+                    $client->fetchAccessTokenWithRefreshToken($refreshToken);\n+                } catch (Exception $e) {\n+                    // TODO: handle the exception instead just loggin it, it will be logged if unhandled by suitecrm core.\n+                    $this->logger->fatal('Caught exception: ' . $e->getMessage());\n+                    throw $e;\n+                }\n+                // Save new token to user preference\n+                $this->workingUser->setPreference('GoogleApiToken', base64_encode(json_encode($client->getAccessToken())), 'GoogleSync');\n+                $this->workingUser->savePreferencesToDB();    \n+            } elseif (empty($refreshToken)) {\n+                throw new GoogleSyncException('Refresh token is missing', GoogleSyncException::NO_REFRESH_TOKEN);\n+            }\n+        }\n+        return $client;\n+    }\n+\n+    /**\n+     * Initialize Service for User\n+     * \n+     * @param string $id The SuiteCRM user id\n+     * \n+     * @return bool Success/Failure\n+     * @throws GoogleSyncException if $id is invalid\n+     * @throws GoogleSyncException if unable to retrive the user\n+     * @throws GoogleSyncException if Google Client fails to set up\n+     * @throws GoogleSyncException if timezone set fails\n+     * @throws GoogleSyncException if unable to setup Google Service\n+     * @throws GoogleSyncException if unable to setup Google Calendar Id\n+     */\n+    protected function initUserService($id)\n+    {\n+        $isValidator = new SuiteValidator();\n+        if (!$isValidator->isValidId($this->db->quote($id))) {\n+            throw new GoogleSyncException('Invalid ID requested in initUserService', GoogleSyncException::INVALID_CLIENT_ID);\n+        }\n+\n+        // Retrieve user bean\n+        $this->workingUser = null;\n+        $this->workingUser = BeanFactory::getBean('Users', $id);\n+        if (!$this->workingUser) {\n+            throw new GoogleSyncException('Unable to retrieve a User bean', GoogleSyncException::UNABLE_TO_RETRIEVE_USER);\n+        }\n+\n+        if (!$this->setClient($id)) {\n+            // TODO: It shouldn't be in the log as it is an exception, it will be logged if unhandled by suitecrm core.\n+            $this->logger->fatal(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'Unable to setup Google Client');\n+            throw new GoogleSyncException('Unable to setup Google Client', GoogleSyncException::UNABLE_TO_SETUP_GCLIENT);\n+        }\n+\n+        if (!$this->setTimezone($this->workingUser->getPreference('timezone', 'global'))) {\n+            // TODO: It shouldn't be in the log as it is an exception, it will be logged if unhandled by suitecrm core.\n+            $this->logger->fatal(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'Failed to set the working user and timezone');\n+            throw new GoogleSyncException('Failed to set the working user\\'s timezone', GoogleSyncException::TIMEZONE_SET_FAILURE);\n+        }\n+\n+        if (!$this->setGService()) {\n+            // TODO: It shouldn't be in the log as it is an exception, it will be logged if unhandled by suitecrm core.\n+            $this->logger->fatal(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'Unable to setup Google Service');\n+            throw new GoogleSyncException('Unable to setup Google Service', GoogleSyncException::GSERVICE_FAILURE);\n+        }\n+\n+        if (!$this->setUsersGoogleCalendar()) {\n+            // TODO: It shouldn't be in the log as it is an exception, it will be logged if unhandled by suitecrm core.\n+            $this->logger->fatal(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'Unable to setup Google Calendar Id');\n+            throw new GoogleSyncException('Unable to setup Google Calendar Id', GoogleSyncException::GCALENDAR_FAILURE);\n+        }\n+        return true;\n+    }\n+\n+    /**\n+     * Retrieve List of meetings owned by the Current Working User\n+     *\n+     *\n+     * @return array Array of SuiteCRM Meeting Beans\n+     * @throws GoogleSyncException if $this->workingUser->id is invalid\n+     * @throws GoogleSyncException if unable to get Meetings bean\n+     */\n+    protected function getUserMeetings($userId)\n+    {\n+        $this->logger->info(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'Id ' . $userId);\n+        // Validate the workingUser id\n+        $isValidator = new SuiteValidator();\n+        if (!$isValidator->isValidId($userId)) {\n+            throw new GoogleSyncException('Invalid ID requested in getUserMeetings', GoogleSyncException::INVALID_USER_ID);\n+        }\n+\n+        // We do it this way so we also get deleted meetings\n+        $query = \"SELECT id FROM meetings WHERE assigned_user_id = '\" . $userId . \"' AND date_start <= now() + interval 3 month\";\n+        $result = $this->db->query($query);\n+\n+        $meetings = array();\n+        while ($row = $this->db->fetchByAssoc($result)) {\n+            $meeting = BeanFactory::getBean('Meetings');\n+            if (!$meeting) {\n+                throw new GoogleSyncException('Unable to get Meetings bean.', GoogleSyncException::UNABLE_TO_RETRIEVE_MEETING);\n+            }\n+            $meeting->retrieve($row['id'], true, false);\n+            $meetings[] = $meeting;\n+        }\n+        return $meetings;\n+    }\n+\n+    /**\n+     * Set user's google calendar id\n+     *\n+     *\n+     * @return bool|int Success/Failure calendar ID if success\n+     */\n+    protected function setUsersGoogleCalendar()\n+    {\n+\n+        // Make sure we have a Google Calendar Service instance\n+        if (!$this->isServiceExists()) {\n+            return false;\n+        }\n+\n+        // get list of users calendars\n+        $calendarList = $this->gService->calendarList->listCalendarList();\n+\n+        // find the id of the 'SuiteCRM' calendar ... in the future, this will set the calendar of the users choosing.\n+        $this->calendarId = $this->getSuiteCRMCalendar($calendarList);\n+\n+        // if the SuiteCRM calendar doesn't exist... Create it!\n+        if (!$this->isCalendarExists()) {\n+            // TODO: It should be an exception?\n+            $this->logger->info(__FILE__ . ':' . __LINE__ . ' ' . __METHOD__ . ' - ' . 'Unable to find the SuiteCRM Google Calendar, Creating it!');\n+            $calendar = new Google_Service_Calendar_Calendar();\n+            $calendar->setSummary('SuiteCRM');\n+            $calendar->setTimeZone($this->timezone);\n+\n+            $createdCalendar = $this->gService->calendars->insert($calendar);\n+            $this->calendarId = $createdCalendar->getId();\n+        }\n+\n+        // Final check to make sure we have an ID\n+        if (!$this->isCalendarExists()) {\n+            return false;\n+        }\n+        return $this->calendarId;\n+    }\n+\n+    /**\n+     * find the id of the 'SuiteCRM' calendar ... in the future, this will return the calendar of the users choosing.\n+     *\n+     * @param Google_Service_Calendar_CalendarList $calendarList\n+     * \n+     * @return string|null Matching Google Calendar ID or null.\n+     */\n+    protected function getSuiteCRMCalendar(Google_Service_Calendar_CalendarList $calendarList)\n+    {\n+        foreach ($calendarList->getItems() as $calendarListEntry) {\n+            if ($calendarListEntry->getSummary() == 'SuiteCRM') {\n+                $return = $calendarListEntry->getId();\n+                break;\n+            }\n+        }\n+        if (empty($return)) { // TODO: this empty-check is not neccessary, return the getId() results as it is.\n+            return null;\n+        }\n+        return $return;\n+    }\n+\n+    /**\n+     * Get events in users google calendar\n+     *\n+     *\n+     * @return bool|array Array of Google_Service_Calendar_Event Objects\n+     */\n+    protected function getUserGoogleEvents()\n+    {\n+\n+        // Make sure we have a Google Calendar Service instance and\n+        // Make sure we have a calendar id\n+        if (!$this->isServiceExists() || !$this->isCalendarExists()) {\n+            return false;\n+        }\n+\n+        // Set Options for what events we get from Google\n+        $optParams = array(\n+            'maxResults' => 500,\n+            'showDeleted' => true,\n+            'singleEvents' => true,\n+            'timeMin' => date('c', strtotime('-1 month')),\n+            'timeMax' => date('c', strtotime('+3 month')),\n+        );\n+\n+        $results_g = $this->gService->events->listEvents($this->calendarId, $optParams);\n+\n+        // We only want the events, not the leading cruft\n+        $results = $results_g->getItems(); // TODO: getItems() returns a Google_Service_Calendar_Event, but this code expect an array in the next line? Something missing here?","path":"include/GoogleSync/GoogleSyncBase.php","position":433,"original_position":433,"commit_id":"b8e682309d92e31d6ea85d20f172ce7150ce60fa","original_commit_id":"b8e682309d92e31d6ea85d20f172ce7150ce60fa","user":{"login":"ApatheticCosmos","id":16560296,"node_id":"MDQ6VXNlcjE2NTYwMjk2","avatar_url":"https://avatars3.githubusercontent.com/u/16560296?v=4","gravatar_id":"","url":"https://api.github.com/users/ApatheticCosmos","html_url":"https://github.com/ApatheticCosmos","followers_url":"https://api.github.com/users/ApatheticCosmos/followers","following_url":"https://api.github.com/users/ApatheticCosmos/following{/other_user}","gists_url":"https://api.github.com/users/ApatheticCosmos/gists{/gist_id}","starred_url":"https://api.github.com/users/ApatheticCosmos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ApatheticCosmos/subscriptions","organizations_url":"https://api.github.com/users/ApatheticCosmos/orgs","repos_url":"https://api.github.com/users/ApatheticCosmos/repos","events_url":"https://api.github.com/users/ApatheticCosmos/events{/privacy}","received_events_url":"https://api.github.com/users/ApatheticCosmos/received_events","type":"User","site_admin":false},"body":"@gymad\r\nlistEvents() above returns Google_Service_Calendar_Events\r\ngetItems() returns an array of Google_Service_Calendar_Event objects.\r\nGoogle made the names a bit confusing.","created_at":"2018-12-28T20:56:05Z","updated_at":"2018-12-28T20:56:05Z","html_url":"https://github.com/salesagility/SuiteCRM/pull/6539#discussion_r244406230","pull_request_url":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/6539","author_association":"CONTRIBUTOR","_links":{"self":{"href":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/comments/244406230"},"html":{"href":"https://github.com/salesagility/SuiteCRM/pull/6539#discussion_r244406230"},"pull_request":{"href":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/6539"}}},"pull_request":{"url":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/6539","id":234067994,"node_id":"MDExOlB1bGxSZXF1ZXN0MjM0MDY3OTk0","html_url":"https://github.com/salesagility/SuiteCRM/pull/6539","diff_url":"https://github.com/salesagility/SuiteCRM/pull/6539.diff","patch_url":"https://github.com/salesagility/SuiteCRM/pull/6539.patch","issue_url":"https://api.github.com/repos/salesagility/SuiteCRM/issues/6539","number":6539,"state":"open","locked":false,"title":"Cleanup, Refactoring, and Bug Fixes for Google Sync","user":{"login":"ApatheticCosmos","id":16560296,"node_id":"MDQ6VXNlcjE2NTYwMjk2","avatar_url":"https://avatars3.githubusercontent.com/u/16560296?v=4","gravatar_id":"","url":"https://api.github.com/users/ApatheticCosmos","html_url":"https://github.com/ApatheticCosmos","followers_url":"https://api.github.com/users/ApatheticCosmos/followers","following_url":"https://api.github.com/users/ApatheticCosmos/following{/other_user}","gists_url":"https://api.github.com/users/ApatheticCosmos/gists{/gist_id}","starred_url":"https://api.github.com/users/ApatheticCosmos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ApatheticCosmos/subscriptions","organizations_url":"https://api.github.com/users/ApatheticCosmos/orgs","repos_url":"https://api.github.com/users/ApatheticCosmos/repos","events_url":"https://api.github.com/users/ApatheticCosmos/events{/privacy}","received_events_url":"https://api.github.com/users/ApatheticCosmos/received_events","type":"User","site_admin":false},"body":"A continuation of #6146 \r\nRefactored and broke up some functions. Fixed some bugs.\r\nFollowed review suggestions from @gymad\r\nThis work will be included in 7.11\r\n\r\n<!--- Provide a general summary of your changes in the Title above -->\r\n\r\n## Description\r\nThis isn't finished yet, but I wanted to get this PR out so we can start collaborating on it.\r\n<!--- Describe your changes in detail -->\r\n<!--- If fixing a bug, there should be an issue describing it with steps to reproduce -->\r\n<!--- Please link to the issue here unless your commit contains the issue number -->\r\n\r\n## Motivation and Context\r\n<!--- Why is this change required? What problem does it solve? -->\r\n\r\n## How To Test This\r\n<!--- Please describe in detail how to test your changes. -->\r\n\r\n## Types of changes\r\n<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->\r\n- [X] Bug fix (non-breaking change which fixes an issue)\r\n- [ ] New feature (non-breaking change which adds functionality)\r\n- [ ] Breaking change (fix or feature that would cause existing functionality to change)\r\n\r\n### Final checklist\r\n<!--- Go over all the following points and check all the boxes that apply. --->\r\n<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! --->\r\n- [X] My code follows the code style of this project found [here](https://docs.suitecrm.com/community/contributing-code/coding-standards/).\r\n- [ ] My change requires a change to the documentation.\r\n- [x] I have read the [**How to Contribute**](https://docs.suitecrm.com/community/contributing-code/) guidelines.\r\n\r\n<!--- Your pull request will be tested via Travis CI to automatically indicate that your changes do not prevent compilation. --->\r\n\r\n<!--- If it reports back that there are problems, you can log into the Travis system and check the log report for your pull request to see what the problem was. --->","created_at":"2018-11-27T21:34:50Z","updated_at":"2018-12-28T20:56:05Z","closed_at":null,"merged_at":null,"merge_commit_sha":"ee13df5aea1017d544e72db961585e1347d34bfc","assignee":null,"assignees":[],"requested_reviewers":[],"requested_teams":[],"labels":[{"id":1148555079,"node_id":"MDU6TGFiZWwxMTQ4NTU1MDc5","url":"https://api.github.com/repos/salesagility/SuiteCRM/labels/Community%20Contribution","name":"Community Contribution","color":"108bb5","default":false}],"milestone":null,"commits_url":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/6539/commits","review_comments_url":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/6539/comments","review_comment_url":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/comments{/number}","comments_url":"https://api.github.com/repos/salesagility/SuiteCRM/issues/6539/comments","statuses_url":"https://api.github.com/repos/salesagility/SuiteCRM/statuses/b8e682309d92e31d6ea85d20f172ce7150ce60fa","head":{"label":"ApatheticCosmos:develop","ref":"develop","sha":"b8e682309d92e31d6ea85d20f172ce7150ce60fa","user":{"login":"ApatheticCosmos","id":16560296,"node_id":"MDQ6VXNlcjE2NTYwMjk2","avatar_url":"https://avatars3.githubusercontent.com/u/16560296?v=4","gravatar_id":"","url":"https://api.github.com/users/ApatheticCosmos","html_url":"https://github.com/ApatheticCosmos","followers_url":"https://api.github.com/users/ApatheticCosmos/followers","following_url":"https://api.github.com/users/ApatheticCosmos/following{/other_user}","gists_url":"https://api.github.com/users/ApatheticCosmos/gists{/gist_id}","starred_url":"https://api.github.com/users/ApatheticCosmos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ApatheticCosmos/subscriptions","organizations_url":"https://api.github.com/users/ApatheticCosmos/orgs","repos_url":"https://api.github.com/users/ApatheticCosmos/repos","events_url":"https://api.github.com/users/ApatheticCosmos/events{/privacy}","received_events_url":"https://api.github.com/users/ApatheticCosmos/received_events","type":"User","site_admin":false},"repo":{"id":122366552,"node_id":"MDEwOlJlcG9zaXRvcnkxMjIzNjY1NTI=","name":"SuiteCRM","full_name":"ApatheticCosmos/SuiteCRM","private":false,"owner":{"login":"ApatheticCosmos","id":16560296,"node_id":"MDQ6VXNlcjE2NTYwMjk2","avatar_url":"https://avatars3.githubusercontent.com/u/16560296?v=4","gravatar_id":"","url":"https://api.github.com/users/ApatheticCosmos","html_url":"https://github.com/ApatheticCosmos","followers_url":"https://api.github.com/users/ApatheticCosmos/followers","following_url":"https://api.github.com/users/ApatheticCosmos/following{/other_user}","gists_url":"https://api.github.com/users/ApatheticCosmos/gists{/gist_id}","starred_url":"https://api.github.com/users/ApatheticCosmos/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ApatheticCosmos/subscriptions","organizations_url":"https://api.github.com/users/ApatheticCosmos/orgs","repos_url":"https://api.github.com/users/ApatheticCosmos/repos","events_url":"https://api.github.com/users/ApatheticCosmos/events{/privacy}","received_events_url":"https://api.github.com/users/ApatheticCosmos/received_events","type":"User","site_admin":false},"html_url":"https://github.com/ApatheticCosmos/SuiteCRM","description":"SuiteCRM - Open source CRM for the world","fork":true,"url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM","forks_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/forks","keys_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/teams","hooks_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/hooks","issue_events_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/issues/events{/number}","events_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/events","assignees_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/assignees{/user}","branches_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/branches{/branch}","tags_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/tags","blobs_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/git/refs{/sha}","trees_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/statuses/{sha}","languages_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/languages","stargazers_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/stargazers","contributors_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/contributors","subscribers_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/subscribers","subscription_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/subscription","commits_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/commits{/sha}","git_commits_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/git/commits{/sha}","comments_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/comments{/number}","issue_comment_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/issues/comments{/number}","contents_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/contents/{+path}","compare_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/merges","archive_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/downloads","issues_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/issues{/number}","pulls_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/pulls{/number}","milestones_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/milestones{/number}","notifications_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/labels{/name}","releases_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/releases{/id}","deployments_url":"https://api.github.com/repos/ApatheticCosmos/SuiteCRM/deployments","created_at":"2018-02-21T17:07:24Z","updated_at":"2018-11-21T17:16:59Z","pushed_at":"2018-12-28T18:45:39Z","git_url":"git://github.com/ApatheticCosmos/SuiteCRM.git","ssh_url":"git@github.com:ApatheticCosmos/SuiteCRM.git","clone_url":"https://github.com/ApatheticCosmos/SuiteCRM.git","svn_url":"https://github.com/ApatheticCosmos/SuiteCRM","homepage":"http://www.suitecrm.com","size":111761,"stargazers_count":0,"watchers_count":0,"language":"PHP","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"archived":false,"open_issues_count":2,"license":{"key":"agpl-3.0","name":"GNU Affero General Public License v3.0","spdx_id":"AGPL-3.0","url":"https://api.github.com/licenses/agpl-3.0","node_id":"MDc6TGljZW5zZTE="},"forks":0,"open_issues":2,"watchers":0,"default_branch":"master"}},"base":{"label":"salesagility:develop","ref":"develop","sha":"4cbd47ed0c6d5ee804b1f53354bd2e051b79d4e3","user":{"login":"salesagility","id":1450870,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE0NTA4NzA=","avatar_url":"https://avatars1.githubusercontent.com/u/1450870?v=4","gravatar_id":"","url":"https://api.github.com/users/salesagility","html_url":"https://github.com/salesagility","followers_url":"https://api.github.com/users/salesagility/followers","following_url":"https://api.github.com/users/salesagility/following{/other_user}","gists_url":"https://api.github.com/users/salesagility/gists{/gist_id}","starred_url":"https://api.github.com/users/salesagility/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/salesagility/subscriptions","organizations_url":"https://api.github.com/users/salesagility/orgs","repos_url":"https://api.github.com/users/salesagility/repos","events_url":"https://api.github.com/users/salesagility/events{/privacy}","received_events_url":"https://api.github.com/users/salesagility/received_events","type":"Organization","site_admin":false},"repo":{"id":13044600,"node_id":"MDEwOlJlcG9zaXRvcnkxMzA0NDYwMA==","name":"SuiteCRM","full_name":"salesagility/SuiteCRM","private":false,"owner":{"login":"salesagility","id":1450870,"node_id":"MDEyOk9yZ2FuaXphdGlvbjE0NTA4NzA=","avatar_url":"https://avatars1.githubusercontent.com/u/1450870?v=4","gravatar_id":"","url":"https://api.github.com/users/salesagility","html_url":"https://github.com/salesagility","followers_url":"https://api.github.com/users/salesagility/followers","following_url":"https://api.github.com/users/salesagility/following{/other_user}","gists_url":"https://api.github.com/users/salesagility/gists{/gist_id}","starred_url":"https://api.github.com/users/salesagility/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/salesagility/subscriptions","organizations_url":"https://api.github.com/users/salesagility/orgs","repos_url":"https://api.github.com/users/salesagility/repos","events_url":"https://api.github.com/users/salesagility/events{/privacy}","received_events_url":"https://api.github.com/users/salesagility/received_events","type":"Organization","site_admin":false},"html_url":"https://github.com/salesagility/SuiteCRM","description":"SuiteCRM - Open source CRM for the world","fork":false,"url":"https://api.github.com/repos/salesagility/SuiteCRM","forks_url":"https://api.github.com/repos/salesagility/SuiteCRM/forks","keys_url":"https://api.github.com/repos/salesagility/SuiteCRM/keys{/key_id}","collaborators_url":"https://api.github.com/repos/salesagility/SuiteCRM/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/salesagility/SuiteCRM/teams","hooks_url":"https://api.github.com/repos/salesagility/SuiteCRM/hooks","issue_events_url":"https://api.github.com/repos/salesagility/SuiteCRM/issues/events{/number}","events_url":"https://api.github.com/repos/salesagility/SuiteCRM/events","assignees_url":"https://api.github.com/repos/salesagility/SuiteCRM/assignees{/user}","branches_url":"https://api.github.com/repos/salesagility/SuiteCRM/branches{/branch}","tags_url":"https://api.github.com/repos/salesagility/SuiteCRM/tags","blobs_url":"https://api.github.com/repos/salesagility/SuiteCRM/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/salesagility/SuiteCRM/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/salesagility/SuiteCRM/git/refs{/sha}","trees_url":"https://api.github.com/repos/salesagility/SuiteCRM/git/trees{/sha}","statuses_url":"https://api.github.com/repos/salesagility/SuiteCRM/statuses/{sha}","languages_url":"https://api.github.com/repos/salesagility/SuiteCRM/languages","stargazers_url":"https://api.github.com/repos/salesagility/SuiteCRM/stargazers","contributors_url":"https://api.github.com/repos/salesagility/SuiteCRM/contributors","subscribers_url":"https://api.github.com/repos/salesagility/SuiteCRM/subscribers","subscription_url":"https://api.github.com/repos/salesagility/SuiteCRM/subscription","commits_url":"https://api.github.com/repos/salesagility/SuiteCRM/commits{/sha}","git_commits_url":"https://api.github.com/repos/salesagility/SuiteCRM/git/commits{/sha}","comments_url":"https://api.github.com/repos/salesagility/SuiteCRM/comments{/number}","issue_comment_url":"https://api.github.com/repos/salesagility/SuiteCRM/issues/comments{/number}","contents_url":"https://api.github.com/repos/salesagility/SuiteCRM/contents/{+path}","compare_url":"https://api.github.com/repos/salesagility/SuiteCRM/compare/{base}...{head}","merges_url":"https://api.github.com/repos/salesagility/SuiteCRM/merges","archive_url":"https://api.github.com/repos/salesagility/SuiteCRM/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/salesagility/SuiteCRM/downloads","issues_url":"https://api.github.com/repos/salesagility/SuiteCRM/issues{/number}","pulls_url":"https://api.github.com/repos/salesagility/SuiteCRM/pulls{/number}","milestones_url":"https://api.github.com/repos/salesagility/SuiteCRM/milestones{/number}","notifications_url":"https://api.github.com/repos/salesagility/SuiteCRM/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/salesagility/SuiteCRM/labels{/name}","releases_url":"https://api.github.com/repos/salesagility/SuiteCRM/releases{/id}","deployments_url":"https://api.github.com/repos/salesagility/SuiteCRM/deployments","created_at":"2013-09-23T19:10:08Z","updated_at":"2018-12-28T13:14:07Z","pushed_at":"2018-12-28T20:53:47Z","git_url":"git://github.com/salesagility/SuiteCRM.git","ssh_url":"git@github.com:salesagility/SuiteCRM.git","clone_url":"https://github.com/salesagility/SuiteCRM.git","svn_url":"https://github.com/salesagility/SuiteCRM","homepage":"https://www.suitecrm.com","size":111692,"stargazers_count":1248,"watchers_count":1248,"language":"PHP","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":937,"mirror_url":null,"archived":false,"open_issues_count":1004,"license":{"key":"agpl-3.0","name":"GNU Affero General Public License v3.0","spdx_id":"AGPL-3.0","url":"https://api.github.com/licenses/agpl-3.0","node_id":"MDc6TGljZW5zZTE="},"forks":937,"open_issues":1004,"watchers":1248,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/6539"},"html":{"href":"https://github.com/salesagility/SuiteCRM/pull/6539"},"issue":{"href":"https://api.github.com/repos/salesagility/SuiteCRM/issues/6539"},"comments":{"href":"https://api.github.com/repos/salesagility/SuiteCRM/issues/6539/comments"},"review_comments":{"href":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/6539/comments"},"review_comment":{"href":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/salesagility/SuiteCRM/pulls/6539/commits"},"statuses":{"href":"https://api.github.com/repos/salesagility/SuiteCRM/statuses/b8e682309d92e31d6ea85d20f172ce7150ce60fa"}},"author_association":"CONTRIBUTOR"}}
{"actor":{"display_login":"ApatheticCosmos"}}