diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..0b708ada --- /dev/null +++ b/.editorconfig @@ -0,0 +1,22 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{js,yml}] +indent_size = 2 +indent_style = space + +[*.json] +indent_size = 4 +indent_style = space + +[*.{css,ctp,php}] +indent_style = tab + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 6da43217..22b9bf56 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -37,7 +37,7 @@ jobs: NC3_BUILD_DIR: "/opt/nc3" NC3_DOCKER_DIR: "/opt/docker" NC3_GIT_URL: "git://github.com/NetCommons3/NetCommons3.git" - NC3_GIT_BRANCH: "master" + NC3_GIT_BRANCH: "availability" PLUGIN_BUILD_DIR: ${{ github.workspace }} PHP_VERSION: ${{ matrix.php }} MYSQL_VERSION: ${{ matrix.mysql }} diff --git a/.gitignore b/.gitignore index bd743c4d..4a354409 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ app/tmp/* app/[Cc]onfig/core.php app/[Cc]onfig/database.php !empty + +.idea/ diff --git a/Config/bootstrap.php b/Config/bootstrap.php index fcf4ec86..8e9f8036 100644 --- a/Config/bootstrap.php +++ b/Config/bootstrap.php @@ -16,13 +16,16 @@ // Load application configurations $conf = array(); -$files = array('application.yml', 'application.local.yml'); +$files = array('application.yml', + 'application.local.yml', + 'application.' . env('HTTP_HOST') . '.yml', + 'application.' . env('HTTP_X_FORWARDED_HOST') . '.yml'); foreach ($files as $file) { if (file_exists(APP . 'Config' . DS . $file)) { - $conf = array_merge($conf, Spyc::YAMLLoad(APP . 'Config' . DS . $file)); - Configure::write($conf); + $conf = array_replace_recursive($conf, Spyc::YAMLLoad(APP . 'Config' . DS . $file)); } } +Configure::write($conf); if (! defined('NC3_VERSION')) { App::uses('NetCommonsCache', 'NetCommons.Utility'); diff --git a/Config/routes.php b/Config/routes.php index 24590e11..295f8648 100644 --- a/Config/routes.php +++ b/Config/routes.php @@ -11,6 +11,12 @@ App::uses('SlugRoute', 'Pages.Routing/Route'); App::uses('Current', 'NetCommons.Utility'); +Router::connect( + '/invalidate', + array('plugin' => 'pages', 'controller' => 'pages', 'action' => 'invalidate'), + array('routeClass' => 'SlugRoute') +); + Router::connect( '/' . Current::SETTING_MODE_WORD . '/', array('plugin' => 'pages', 'controller' => 'pages', 'action' => 'index'), diff --git a/Controller/Component/NetCommonsComponent.php b/Controller/Component/NetCommonsComponent.php index 009c32f4..b82e1e82 100644 --- a/Controller/Component/NetCommonsComponent.php +++ b/Controller/Component/NetCommonsComponent.php @@ -106,6 +106,8 @@ public function handleValidationError($errors, $message = null) { * @return void */ public function setFlashNotification($message, $params = array(), $status = 200) { + $this->controller->response->header('Pragma', 'no-cache'); + if (is_string($params)) { $params = array('class' => $params); } diff --git a/Controller/NetCommonsAppController.php b/Controller/NetCommonsAppController.php index 01914da7..ddee2d7e 100644 --- a/Controller/NetCommonsAppController.php +++ b/Controller/NetCommonsAppController.php @@ -3,6 +3,7 @@ * NetCommonsApp Controller * * @author Shohei Nakajima + * @author Kazunori Sakamoto * @link http://www.netcommons.org NetCommons Project * @license http://www.netcommons.org/license.txt NetCommons License * @copyright Copyright 2014, NetCommons Project @@ -21,8 +22,10 @@ * NetCommonsApp Controller * * @author Shohei Nakajima + * @author Kazunori Sakamoto * @package NetCommons\NetCommons\Controller * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) + * @SuppressWarnings(PHPMD.CouplingBetweenObjects) */ class NetCommonsAppController extends Controller { @@ -103,8 +106,9 @@ class NetCommonsAppController extends Controller { 'M17n.M17n', 'NetCommons.BackTo', 'NetCommons.Button', - 'NetCommons.LinkButton', + 'NetCommons.CDNCache', 'NetCommons.Date', + 'NetCommons.LinkButton', 'NetCommons.MessageFlash', 'NetCommons.NetCommonsForm', 'NetCommons.NetCommonsHtml', @@ -165,6 +169,7 @@ public function __construct($request = null, $response = null) { * 事前準備 * * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function __prepare() { if (Current::read('Block') && @@ -197,16 +202,67 @@ private function __prepare() { } $this->request->params['named'] = $named; } + + if ($this->__updateFullBaseUrl()) { + $this->redirect($this->request->here); + } + } + +/** + * set and return member or non member url if redirect needed + * + * @return bool + */ + private function __updateFullBaseUrl() { + // ログイン済みの場合は、memberにリダイレクトしない + if (Current::isLogin()) { + return false; + } + + // NetCommonsプラグインの処理では、memberにリダイレクトしない + if ($this->request->plugin == 'net_commons') { + return false; + } + + $memberUrl = Configure::read('App.memberUrl'); + if (!isset($memberUrl)) { + return false; + } + + $nonMemberUrl = preg_replace("#^(http(s)?://)member[.-]#", "$1", $memberUrl); + $authPlugins = array('auth', 'auth_general'); + $isAuthPlugin = in_array($this->request->plugin, $authPlugins, true); + // Auth 関連の Plugin の場合は memberUrl を fullBaseUrl にセットし、 + // Auth 関連以外の Plugin の場合は nonMemberUrl を fullBaseUrl にセットする + if ($isAuthPlugin && Router::fullBaseUrl() !== $memberUrl) { + Router::fullBaseUrl($memberUrl); + return true; + } elseif (!$isAuthPlugin && Router::fullBaseUrl() === $memberUrl) { + Router::fullBaseUrl($nonMemberUrl); + return true; + } + return false; } /** * beforeFilter * * @return void + * @SuppressWarnings(PHPMD.CyclomaticComplexity) + * @SuppressWarnings(PHPMD.NPathComplexity) */ public function beforeFilter() { parent::beforeFilter(); + if (!empty($this->request->params['ext']) && + $this->request->params['ext'] === 'json') { + $this->request->addDetector('ajax', ['param' => 'ext', 'value' => 'json']); + } + + if ($this->request->is('ajax') || $this->request->query('no-cache')) { + $this->response->header('Pragma', 'no-cache'); + } + if (empty($this->request->params['requested'])) { $this->request->allowMethod($this->_allowMethods); } @@ -295,6 +351,10 @@ public function afterFilter() { $this->CurrentLib->terminate($this); } + if (Current::isLogin()) { + $this->response->header('Pragma', 'no-cache'); + } + parent::afterFilter(); } diff --git a/Model/NetCommonsAppModel.php b/Model/NetCommonsAppModel.php index bd8c0906..67165aee 100644 --- a/Model/NetCommonsAppModel.php +++ b/Model/NetCommonsAppModel.php @@ -5,12 +5,15 @@ * @author Shohei Nakajima * @author Jun Nishikawa * @author Takako Miyagawa + * @author Wataru Nishimoto + * @author Kazunori Sakamoto * @link http://www.netcommons.org NetCommons Project * @license http://www.netcommons.org/license.txt NetCommons License * @copyright Copyright 2014, NetCommons Project */ App::uses('Model', 'Model'); +App::uses('NetCommonsCDNCache', 'NetCommons.Utility'); App::uses('ValidateMerge', 'NetCommons.Utility'); /** @@ -29,6 +32,8 @@ * @author Shohei Nakajima * @author Jun Nishikawa * @author Takako Miyagawa + * @author Wataru Nishimoto + * @author Kazunori Sakamoto * @package NetCommons\NetCommons\Model * @SuppressWarnings(PHPMD.NumberOfChildren) * @SuppressWarnings(PHPMD.ExcessiveClassComplexity) @@ -97,6 +102,14 @@ class NetCommonsAppModel extends Model { */ public $contentKey = null; +/** + * invalidateCDN cache + * DB 保存/削除時に CDN のキャッシュを invalidate するか + * + * @var bool + */ + public $invalidateCDN = true; + /** * Constructor. DataSourceの選択 * @@ -319,6 +332,11 @@ public function begin() { public function commit() { $dataSource = $this->getDataSource(); $dataSource->commit(); + + if ($this->invalidateCDN) { + $cdnCache = new NetCommonsCDNCache(); + $cdnCache->invalidate(); + } } /** diff --git a/Utility/NetCommonsCDNCache.php b/Utility/NetCommonsCDNCache.php new file mode 100644 index 00000000..7f00ba36 --- /dev/null +++ b/Utility/NetCommonsCDNCache.php @@ -0,0 +1,83 @@ + + * @author Shohei Nakajima + * @author Wataru Nishimoto + * @author Kazunori Sakamoto + * @link http://www.netcommons.org NetCommons Project + * @license http://www.netcommons.org/license.txt NetCommons License + * @copyright Copyright 2014, NetCommons Project + */ + +App::uses('NetCommonsCache', 'NetCommons.Utility'); + +/** + * NetCommons用CDNキャッシュ Utility + * + * @author Shohei Nakajima + * @author Wataru Nishimoto + * @author Kazunori Sakamoto + * @package NetCommons\NetCommons\Utility + */ +class NetCommonsCDNCache { + +/** + * 高頻度なキャッシュ無効化を防ぐために、無効化のリクエストを無視する期間(秒) + * + * @var float + */ + const NO_CACHE_INVALIDATION_DURATION_SEC = 1.0; + +/** + * Invalidate CDN Cache checking the invalidation frequency + * + * @return void + */ + public function invalidate() { + $ncCache = new NetCommonsCache('cdn_cache_invalidated_at', false, 'netcommons_core'); + $lastTime = floatval($ncCache->read()); + $now = microtime(true); + if ($now - $lastTime > self::NO_CACHE_INVALIDATION_DURATION_SEC) { + $ncCache->write($now); + $this->__postInvalidationRequest(); + } + } + +/** + * Invalidate CDN Cache + * + * @return void + */ + private function __postInvalidationRequest() { + $cacheDomain = Configure::read('App.cacheDomain'); + if (!isset($cacheDomain)) { + return; + } + + $accessToken = Configure::read('Cdn.AccessToken'); + $accessTokenSecret = Configure::read('Cdn.Secret'); + if (!(isset($accessToken) && isset($accessTokenSecret))) { + return; + } + + $curl = curl_init(); + curl_setopt($curl, CURLOPT_URL, + 'https://secure.sakura.ad.jp/cloud/zone/is1a/api/webaccel/1.0/deleteallcache'); + curl_setopt($curl, CURLOPT_POST, true); + curl_setopt($curl, CURLOPT_USERPWD, "$accessToken:$accessTokenSecret"); + curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + $data = array( + "Site" => array( + "Domain" => $cacheDomain + ) + ); + curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data)); + + curl_exec($curl); + curl_close($curl); + } +} diff --git a/View/Elements/common_header.ctp b/View/Elements/common_header.ctp index e7b47f57..32c5bb68 100644 --- a/View/Elements/common_header.ctp +++ b/View/Elements/common_header.ctp @@ -123,7 +123,8 @@ if (empty($navbarStyle)) {
  • - NetCommonsHtml->link(__d('net_commons', 'Login'), '/auth/login'); ?> + NetCommonsHtml->link(__d('net_commons', 'Login'), + Configure::read('App.memberUrl') . '/auth/login'); ?>
  • diff --git a/View/Helper/CDNCacheHelper.php b/View/Helper/CDNCacheHelper.php new file mode 100644 index 00000000..663537c7 --- /dev/null +++ b/View/Helper/CDNCacheHelper.php @@ -0,0 +1,31 @@ + + * @author Kazunori Sakamoto + * @link http://www.netcommons.org NetCommons Project + * @license http://www.netcommons.org/license.txt NetCommons License + * @copyright Copyright 2020, NetCommons Project + */ + +/** + * CDNCache Helper + * + */ +class CDNCacheHelper extends AppHelper { + +/** + * Return a boolean value whether the page is cacheable or not. + * + * @return bool a boolean value whether the page is cacheable or not. + */ + public function isCacheable() { + $nonOrigin = strncmp('origin-', $_SERVER['HTTP_HOST'], 7) !== 0 && + strncmp('origin.', $_SERVER['HTTP_HOST'], 7) !== 0; + $nonCacheable = (isset($this->_View->response->header()['Pragma']) && + $this->_View->response->header()['Pragma'] === 'no-cache') || + $nonOrigin; + return !$nonCacheable; + } +} diff --git a/View/Helper/NetCommonsHtmlHelper.php b/View/Helper/NetCommonsHtmlHelper.php index 6e8d1bd3..1d77b92e 100644 --- a/View/Helper/NetCommonsHtmlHelper.php +++ b/View/Helper/NetCommonsHtmlHelper.php @@ -140,6 +140,7 @@ public function script($url, $options = array()) { ); $url = $this->__convertWebrootPath($url); + $url = $this->__convertCDNUrls($url); return $this->Html->script($url, array_merge($defaultOptions, $options)); } @@ -159,6 +160,7 @@ public function css($path, $options = array()) { ); $path = $this->__convertWebrootPath($path); + $path = $this->__convertCDNUrls($path); return $this->Html->css($path, array_merge($defaultOptions, $options)); } @@ -237,6 +239,7 @@ public function image($path, $options = array()) { $paths = $this->__convertWebrootPath($path); $path = $paths[0]; } + $path = $this->__convertCDNUrls($path); $path = $this->__getUrl($path, $options); $output = $this->Html->image($path, $options); return $output; @@ -357,6 +360,43 @@ private function __parseLink($title, $url, &$options) { return ['title' => $title, 'url' => $url]; } +/** + * CDNを介するようなURLに変換 + * + * @param string|array $urls 変換するURL + * @return string + */ + private function __convertCDNUrls($urls) { + $httpHost = $_SERVER['HTTP_HOST'] ?? ''; + $nonMember = strncmp($httpHost, 'member-', 7) !== 0 && + strncmp($httpHost, 'member.', 7) !== 0; + if ($nonMember) { + return $urls; + } + if (is_string($urls)) { + return $this->__convertCDNUrl($urls); + } + foreach ($urls as &$url) { + $url = $this->__convertCDNUrl($url); + } + return $urls; + } + +/** + * CDNを介するようなURLに変換 + * + * @param string $url 変換するURL + * @return string + */ + private function __convertCDNUrl($url) { + if (strncmp($url, '/', 1) === 0 && strpos($url, 'bootstrap.min.css') == false + && strpos($url, 'tinymce.min.js') == false) { + $httpHost = $_SERVER['HTTP_HOST'] ?? ''; + $url = 'https://' . substr($httpHost, 7) . $url; + } + return $url; + } + /** * ``タグの出力 * diff --git a/composer.json b/composer.json index 080bce68..5dfadb80 100644 --- a/composer.json +++ b/composer.json @@ -50,7 +50,8 @@ "netcommons/mails": "@dev", "netcommons/topics": "@dev", "netcommons/menus": "@dev", - "netcommons/clean-up": "@dev" + "netcommons/clean-up": "@dev", + "ext-curl": "*" }, "config": { "vendor-dir": "vendors", diff --git a/webroot/js/base.js b/webroot/js/base.js index 0ce5b3f5..814bd39b 100644 --- a/webroot/js/base.js +++ b/webroot/js/base.js @@ -1,6 +1,7 @@ /** * @fileoverview NetCommonsApp Javascript * @author nakajimashouhei@gmail.com (Shohei Nakajima) + * @author exkazuu@willbooster.com (Kazunori Sakamoto) */ var NetCommonsApp = angular.module('NetCommonsApp', ['ngAnimate', 'ui.bootstrap']); @@ -8,8 +9,14 @@ var NetCommonsApp = angular.module('NetCommonsApp', ['ngAnimate', 'ui.bootstrap' //CakePHPがX-Requested-Withで判断しているため NetCommonsApp.config(['$httpProvider', function($httpProvider) { $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; - $httpProvider.defaults.headers.common['If-Modified-Since'] = - new Date() . toUTCString(); + + // Disable cache in GET requests via Ajax + if (!$httpProvider.defaults.headers.get) { + $httpProvider.defaults.headers.get = {}; + } + $httpProvider.defaults.headers.get['If-Modified-Since'] = 'Mon, 26 Jul 1997 05:00:00 GMT'; + $httpProvider.defaults.headers.get['Cache-Control'] = 'no-cache'; + $httpProvider.defaults.headers.get['Pragma'] = 'no-cache'; }]); @@ -162,120 +169,197 @@ NetCommonsApp.factory('ajaxSendPost', ['$http', '$q', 'NC3_URL', function($http, * base controller */ NetCommonsApp.controller('NetCommons.base', - ['$scope', '$location', '$window', 'NC3_URL', function($scope, $location, $window, NC3_URL) { - - /** - * Base URL - * - * @type {string} - */ - $scope.baseUrl = NC3_URL; - - /** - * sending - * - * @type {boolean} - */ - $scope.sending = false; - - /** - * messages - * - * @type {Object} - */ - $scope.messages = {}; - - /** - * 検索後に戻るボタンでもどったときにsendingマークが表示されっぱなしになるのを抑止 - */ - $window.onpageshow = function() { - $scope.$apply(function() { - $scope.sending = false; - }); - }; + ['$scope', '$location', '$window', '$http', 'NC3_URL', '$q', + function($scope, $location, $window, $http, NC3_URL, $q) { + /** + * Base URL + * + * @type {string} + */ + $scope.baseUrl = NC3_URL; - /** - * top - * - * @type {function} - */ - $scope.top = function() { - $location.hash('nc-modal-top'); - $anchorScroll(); - }; + /** + * sending + * + * @type {boolean} + */ + $scope.sending = false; - /** - * flash message method - * - * @param {string} message - * @param {string} messageClass - * @param {int} interval - * @return {void} - */ - $scope.flashMessage = function(message, messageClass, interval) { - $scope.flash = { - message: message, - class: messageClass + /** + * messages + * + * @type {Object} + */ + $scope.messages = {}; + + /** + * 検索後に戻るボタンでもどったときにsendingマークが表示されっぱなしになるのを抑止 + */ + $window.onpageshow = function() { + $scope.$apply(function() { + $scope.sending = false; + }); }; - $('#nc-ajax-flash-message').removeClass('hidden'); - if (interval > 0) { - $('#nc-ajax-flash-message').fadeIn(500).fadeTo(500, 1).fadeOut(interval); - } else { - $('#nc-ajax-flash-message').fadeIn(500); - } - }; - /** - * submit - * - * @type {function} - */ - $scope.submit = function($event) { - if ($scope.sending) { - $event.preventDefault(); - } - $scope.sending = true; - }; + /** + * top + * + * @type {function} + */ + $scope.top = function() { + $location.hash('nc-modal-top'); + $anchorScroll(); + }; - /** - * cancel - * - * @type {function} - */ - $scope.cancel = function(url) { - $scope.sending = true; - if ($window.location.href === url) { - $window.location.reload(); - } else { - $window.location.href = url; - } - }; + /** + * flash message method + * + * @param {string} message + * @param {string} messageClass + * @param {int} interval + * @return {void} + */ + $scope.flashMessage = function(message, messageClass, interval) { + $scope.flash = { + message: message, + class: messageClass + }; + $('#nc-ajax-flash-message').removeClass('hidden'); + if (interval > 0) { + $('#nc-ajax-flash-message').fadeIn(500).fadeTo(500, 1).fadeOut(interval); + } else { + $('#nc-ajax-flash-message').fadeIn(500); + } + }; - /** - * hashChange - * - * @return {void} - */ - $scope.hashChange = function() { - $($window).bind('hashchange', function() { - var hash = $location.hash(); - var frameId = $window.location.href.match('frame_id=([0-9]+)'); - var element = null; - try { - if (hash) { - if (hash.substr(0, 1) === '/') { - element = $('#' + hash.substr(1)); - } else { - element = $('#' + hash); + /** + * submit + * + * @type {function} + */ + $scope.submit = function($event) { + if ($scope.sending) { + $event.preventDefault(); + } + $scope.sending = true; + }; + + /** + * cancel + * + * @type {function} + */ + $scope.cancel = function(url) { + $scope.sending = true; + if ($window.location.href === url) { + $window.location.reload(); + } else { + $window.location.href = url; + } + }; + + /** + * hashChange + * + * @return {void} + */ + $scope.hashChange = function() { + $($window).bind('hashchange', function() { + var hash = $location.hash(); + var frameId = $window.location.href.match('frame_id=([0-9]+)'); + var element = null; + try { + if (hash) { + if (hash.substr(0, 1) === '/') { + element = $('#' + hash.substr(1)); + } else { + element = $('#' + hash); + } + } else if (frameId) { + element = $('#frame-' + frameId[1]); } - } else if (frameId) { - element = $('#frame-' + frameId[1]); + var pos = element.offset().top; + $window.scrollTo(0, pos - 100); + } catch (err) { + $window.scrollTo(0, 0); } - var pos = element.offset().top; - $window.scrollTo(0, pos - 100); - } catch (err) { - $window.scrollTo(0, 0); + }).trigger('hashchange'); + }; + + /** + * updateLikes + * + * @return {void} + */ + $scope.updateLikes = function() { + var $buttons = $('span.like-button'); + var condsStrsObj = {}; + $buttons.each(function() { condsStrsObj[this.className.split(' ')[0]] = 0; }); + var condsStrs = Object.keys(condsStrsObj); + if (!condsStrs.length) return; + + var iCondsStrs = 0; + var condsStrsList = []; + do { + var list = []; + var strLength = 0; + // Since the maximum length of a URL is about 2000 + for (; iCondsStrs < condsStrs.length && strLength < 1900; iCondsStrs++) { + list.push(condsStrs[iCondsStrs]); + strLength += condsStrs[iCondsStrs].length; + } + condsStrsList.push(list); + } while (iCondsStrs < condsStrs.length); + + var deferred = $q.defer(); + var promise = deferred.promise; + for (var iList = 0; iList < condsStrsList.length; iList++) { + // Pass iList to the following anonymous function to keep the current value + (function(iList) { + var condsStrs = condsStrsList[iList]; + // Chain promise.then() to call API sequentially + promise = promise.then(function() { + var params = '?like_conds_strs=' + condsStrs.join(','); + return $http.get(NC3_URL + '/likes/likes/load.json' + params).then( + function(response) { + var likes = response.data.likes; + for (var i = 0; i < condsStrs.length; i++) { + var condsStr = condsStrs[i]; + var like = likes[condsStr] || { + like_count: 0, + unlike_count: 0, + disabled: false, + }; + var queryPrefix = '.' + condsStr; + $(queryPrefix + ' .like-count').text(like.like_count); + $(queryPrefix + ' .unlike-count').text(like.unlike_count); + $(queryPrefix + ' > a').css('display', like.disabled ? 'none' : ''); + $(queryPrefix + ' > .text-muted') + .css('display', like.disabled ? '' : 'none'); + } + }); + }); + }(iList)); } - }).trigger('hashchange'); - }; - }]); + deferred.resolve(); + }; + + /** + * updateTokens + * + * @return {void} + */ + $scope.updateTokens = function() { + var $token = $('input[name="data[_Token][key]"]'); + var $submit = $token.closest('form').find(':submit'); + $submit.attr('disabled', 'disabled'); + $http.get(NC3_URL + '/net_commons/net_commons/csrfToken.json') + .then(function(response) { + var token = response.data; + $token.val(token.data._Token.key); + $submit.removeAttr('disabled'); + }, + function() { + }); + }; + }]);