[ ['autoload', 100000], ['onPluginsInitialized', 1000] ], 'onPageInitialized' => ['onPageInitialized', 0], 'onFormProcessed' => ['onFormProcessed', 0], 'onSchedulerInitialized' => ['onSchedulerInitialized', 0] ]; } /** * [onPluginsInitialized:100000] Composer autoload. * * @return ClassLoader */ public function autoload() : ClassLoader { return require __DIR__ . '/vendor/autoload.php'; } /** * @return string */ public static function generateWebhookSecret() { return static::generateHash(24); } /** * @return string */ public static function generateRandomWebhook() { return '/_git-sync-' . static::generateHash(6); } /** * Initialize the plugin */ public function onPluginsInitialized() { $this->enable(['gitsync' => ['synchronize', 0]]); $this->init(); if ($this->isAdmin()) { $this->enable([ 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], 'onTwigSiteVariables' => ['onTwigSiteVariables', 0], 'onAdminMenu' => ['onAdminMenu', 0], 'onAdminSave' => ['onAdminSave', 0], 'onAdminAfterSave' => ['onAdminAfterSave', 0], 'onAdminAfterSaveAs' => ['onAdminAfterSaveAs', 0], 'onAdminAfterDelete' => ['onAdminAfterDelete', 0], 'onAdminAfterAddMedia' => ['onAdminAfterMedia', 0], 'onAdminAfterDelMedia' => ['onAdminAfterMedia', 0], ]); return; } $config = $this->config->get('plugins.' . $this->name); $route = $this->grav['uri']->route(); $webhook = $config['webhook'] ?? false; $secret = $config['webhook_secret'] ?? false; $enabled = $config['webhook_enabled'] ?? false; if ($route === $webhook && $_SERVER['REQUEST_METHOD'] === 'POST') { if ($secret && $enabled) { if (!$this->isRequestAuthorized($secret)) { http_response_code(401); header('Content-Type: application/json'); echo json_encode([ 'status' => 'error', 'message' => 'Unauthorized request' ]); exit; } } try { $this->synchronize(); header('Content-Type: application/json'); echo json_encode([ 'status' => 'success', 'message' => 'GitSync completed the synchronization' ]); } catch (\Exception $e) { http_response_code(500); header('Content-Type: application/json'); echo json_encode([ 'status' => 'error', 'message' => 'GitSync failed to synchronize' ]); } exit; } } /** * Returns true if the request contains a valid signature or token * @param string $secret local secret * @return bool whether or not the request is authorized */ public function isRequestAuthorized($secret) { if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) { $payload = file_get_contents('php://input') ?: ''; return $this->isGithubSignatureValid($secret, $_SERVER['HTTP_X_HUB_SIGNATURE'], $payload); } if (isset($_SERVER['HTTP_X_GITLAB_TOKEN'])) { return $this->isGitlabTokenValid($secret, $_SERVER['HTTP_X_GITLAB_TOKEN']); } else { $payload = file_get_contents('php://input'); return $this->isGiteaSecretValid($secret, $payload); } return false; } /** * Hashes the webhook request body with the client secret and * checks if it matches the webhook signature header * @param string $secret The webhook secret * @param string $signatureHeader The signature of the webhook request * @param string $payload The webhook request body * @return bool Whether the signature is valid or not */ public function isGithubSignatureValid($secret, $signatureHeader, $payload) { [$algorithm, $signature] = explode('=', $signatureHeader); return $signature === hash_hmac($algorithm, $payload, $secret); } /** * Returns true if given Gitlab token matches secret * @param string $secret local secret * @param string $token token received from Gitlab webhook request * @return bool whether or not secret and token match */ public function isGitlabTokenValid($secret, $token) { return $secret === $token; } /** * Returns true if secret contained in the payload matches the client * secret * @param string $secret The webhook secret * @param string $payload The webhook request body * @return boolean Whether the client secret matches the payload secret or * not */ public function isGiteaSecretValid($secret, $payload) { $payload = json_decode($payload, true); if (!empty($payload) && isset($payload['secret'])) { return $secret === $payload['secret']; } return false; } public function onAdminMenu() { $base = rtrim($this->grav['base_url'], '/') . '/' . trim($this->grav['admin']->base, '/'); $options = [ 'hint' => Helper::isGitInitialized() ? 'Synchronize GitSync' : 'Configure GitSync', 'class' => 'gitsync-sync', 'location' => 'pages', 'route' => Helper::isGitInitialized() ? 'admin' : 'admin/plugins/git-sync', 'icon' => 'fa-' . $this->grav['plugins']->get('git-sync')->blueprints()->get('icon') ]; if (Helper::isGitInstalled()) { if (Helper::isGitInitialized()) { $options['data'] = [ 'gitsync-useraction' => 'sync', 'gitsync-uri' => $base . '/plugins/git-sync' ]; } $this->grav['twig']->plugins_quick_tray['GitSync'] = $options; } } public function init() { if ($this->isAdmin()) { /** @var AdminController controller */ $this->controller = new AdminController($this); $this->git = &$this->controller->git; } else { $this->git = new GitSync(); } } /** * @return bool */ public function synchronize() { if (!Helper::isGitInstalled() || !Helper::isGitInitialized()) { return true; } $this->grav->fireEvent('onGitSyncBeforeSynchronize'); if ($this->git->hasChangesToCommit()) { $this->git->commit(); } // synchronize with remote $this->git->sync(); $this->grav->fireEvent('onGitSyncAfterSynchronize'); return true; } public function onSchedulerInitialized(Event $event) { /** @var Config $config */ $config = Grav::instance()['config']; $run_at = $config->get('plugins.git-sync.sync.cron_at', '0 12,23 * * *'); if ($config->get('plugins.git-sync.sync.cron_enable', false)) { /** @var Scheduler $scheduler */ $scheduler = $event['scheduler']; $job = $scheduler->addFunction('Grav\Plugin\GitSync\Helper::synchronize', [], 'GitSync'); $job->at($run_at); } } /** * @return bool */ public function reset() { if (!Helper::isGitInstalled() || !Helper::isGitInitialized()) { return true; } $this->grav->fireEvent('onGitSyncBeforeReset'); $this->git->reset(); $this->grav->fireEvent('onGitSyncAfterReset'); return true; } /** * Add current directory to twig lookup paths. */ public function onTwigTemplatePaths() { $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; } /** * Set needed variables to display cart. * * @return bool */ public function onTwigSiteVariables() { // workaround for admin plugin issue that doesn't properly unsubscribe events upon plugin uninstall if (!class_exists(Helper::class)) { return false; } $user = $this->grav['user']; if (!$user->authenticated) { return false; } $settings = [ 'first_time' => !Helper::isGitInitialized(), 'git_installed' => Helper::isGitInstalled() ]; $this->grav['twig']->twig_vars['git_sync'] = $settings; $adminPath = trim($this->grav['admin']->base, '/'); if ($this->grav['uri']->path() === "/$adminPath/plugins/git-sync") { $this->grav['assets']->addCss('plugin://git-sync/css-compiled/git-sync.css'); } else { $this->grav['assets']->addInlineJs('var GitSync = ' . json_encode($settings) . ';'); } $this->grav['assets']->addJs('plugin://git-sync/js/vendor.js', ['loading' => 'defer', 'priority' => 0]); $this->grav['assets']->addJs('plugin://git-sync/js/app.js', ['loading' => 'defer', 'priority' => 0]); $this->grav['assets']->addCss('plugin://git-sync/css-compiled/git-sync-icon.css'); return true; } public function onPageInitialized() { if ($this->controller && $this->controller->isActive()) { $this->controller->execute(); $this->controller->redirect(); } } /** * @param Event $event * @return Data|true */ public function onAdminSave(Event $event) { $obj = $event['object']; $adminPath = trim($this->grav['admin']->base, '/'); $isPluginRoute = $this->grav['uri']->path() === "/$adminPath/plugins/" . $this->name; if ($obj instanceof Data) { if (!$isPluginRoute || !Helper::isGitInstalled()) { return true; } // empty password, keep current one or encrypt if haven't already $password = $obj->get('password', false); if (!$password) { // set to !() $current_password = $this->git->getPassword(); // password exists but was never encrypted if ($current_password && strpos($current_password, 'gitsync-') !== 0) { $current_password = Helper::encrypt($current_password); } } else { // password is getting changed $current_password = Helper::encrypt($password); } $obj->set('password', $current_password); } return $obj; } /** * @param Event $event */ public function onAdminAfterSave(Event $event) { if (!$this->grav['config']->get('plugins.git-sync.sync.on_save', true)) { return; } $obj = $event['object']; $adminPath = trim($this->grav['admin']->base, '/'); $uriPath = $this->grav['uri']->path(); $isPluginRoute = $uriPath === "/$adminPath/plugins/" . $this->name; if ($obj instanceof Data) { $folders = $this->git->getConfig('folders', $event['object']->get('folders', [])); $data_type = preg_replace('#^/' . preg_quote($adminPath, '#') . '/#', '', $uriPath); $data_type = explode('/', $data_type); $data_type = array_shift($data_type); if (null === $data_type || !Helper::isGitInstalled() || (!$isPluginRoute && !in_array($this->getFolderMapping($data_type), $folders, true))) { return; } if ($isPluginRoute) { $this->git->setConfig($obj->toArray()); // initialize git if not done yet $this->git->initializeRepository(); // set committer and remote data $this->git->setUser(); $this->git->addRemote(); } } $this->synchronize(); } public function onAdminAfterSaveAs() { if ($this->grav['config']->get('plugins.git-sync.sync.on_save', true)) { $this->synchronize(); } } public function onAdminAfterDelete() { if ($this->grav['config']->get('plugins.git-sync.sync.on_delete', true)) { $this->synchronize(); } } public function onAdminAfterMedia() { if ($this->grav['config']->get('plugins.git-sync.sync.on_media', true)) { $this->synchronize(); } } /** * @param Event $event */ public function onFormProcessed(Event $event) { $action = $event['action']; if ($action === 'gitsync') { $this->synchronize(); } } /** * @param string $data_type * @return string|null */ public function getFolderMapping($data_type) { switch ($data_type) { case 'user': return 'accounts'; case 'themes': return 'config'; case 'config': case 'data': case 'plugins': case 'pages': return $data_type; } return null; } /** * @param int $len * @return string */ protected static function generateHash(int $len): string { $bytes = openssl_random_pseudo_bytes($len, $isStrong); if ($bytes === false) { throw new \RuntimeException('Could not generate hash'); } if ($isStrong === false) { // It's ok not to be strong [EA]. $isStrong = true; } return bin2hex($bytes); } }