Module.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413
  1. <?php
  2. /**
  3. * @link http://www.yiiframework.com/
  4. * @copyright Copyright (c) 2008 Yii Software LLC
  5. * @license http://www.yiiframework.com/license/
  6. */
  7. namespace yii\debug;
  8. use Yii;
  9. use yii\base\Application;
  10. use yii\base\BootstrapInterface;
  11. use yii\helpers\Html;
  12. use yii\helpers\Json;
  13. use yii\helpers\Url;
  14. use yii\web\ForbiddenHttpException;
  15. use yii\web\Response;
  16. use yii\web\View;
  17. /**
  18. * The Yii Debug Module provides the debug toolbar and debugger
  19. *
  20. * @author Qiang Xue <qiang.xue@gmail.com>
  21. * @since 2.0
  22. */
  23. class Module extends \yii\base\Module implements BootstrapInterface
  24. {
  25. const DEFAULT_IDE_TRACELINE = '<a href="ide://open?url=file://{file}&line={line}">{text}</a>';
  26. /**
  27. * @var array the list of IPs that are allowed to access this module.
  28. * Each array element represents a single IP filter which can be either an IP address
  29. * or an address with wildcard (e.g. 192.168.0.*) to represent a network segment.
  30. * The default value is `['127.0.0.1', '::1']`, which means the module can only be accessed
  31. * by localhost.
  32. */
  33. public $allowedIPs = ['127.0.0.1', '::1'];
  34. /**
  35. * @var array the list of hosts that are allowed to access this module.
  36. * Each array element is a hostname that will be resolved to an IP address that is compared
  37. * with the IP address of the user. A use case is to use a dynamic DNS (DDNS) to allow access.
  38. * The default value is `[]`.
  39. */
  40. public $allowedHosts = [];
  41. /**
  42. * @var callable A valid PHP callback that returns true if user is allowed to use web shell and false otherwise
  43. *
  44. * The signature is the following:
  45. *
  46. * function (Action|null $action) The action can be null when called from a non action context (like set debug header)
  47. *
  48. * @since 2.1.0
  49. */
  50. public $checkAccessCallback;
  51. /**
  52. * {@inheritdoc}
  53. */
  54. public $controllerNamespace = 'yii\debug\controllers';
  55. /**
  56. * @var LogTarget
  57. */
  58. public $logTarget;
  59. /**
  60. * @var array|Panel[] list of debug panels. The array keys are the panel IDs, and values are the corresponding
  61. * panel class names or configuration arrays. This will be merged with [[corePanels()]].
  62. * You may reconfigure a core panel via this property by using the same panel ID.
  63. * You may also disable a core panel by setting it to be false in this property.
  64. */
  65. public $panels = [];
  66. /**
  67. * @var string the name of the panel that should be visible when opening the debug panel.
  68. * The default value is 'log'.
  69. * @since 2.0.7
  70. */
  71. public $defaultPanel = 'log';
  72. /**
  73. * @var string the directory storing the debugger data files. This can be specified using a path alias.
  74. */
  75. public $dataPath = '@runtime/debug';
  76. /**
  77. * @var int the permission to be set for newly created debugger data files.
  78. * This value will be used by PHP [[chmod()]] function. No umask will be applied.
  79. * If not set, the permission will be determined by the current environment.
  80. * @since 2.0.6
  81. */
  82. public $fileMode;
  83. /**
  84. * @var int the permission to be set for newly created directories.
  85. * This value will be used by PHP [[chmod()]] function. No umask will be applied.
  86. * Defaults to 0775, meaning the directory is read-writable by owner and group,
  87. * but read-only for other users.
  88. * @since 2.0.6
  89. */
  90. public $dirMode = 0775;
  91. /**
  92. * @var int the maximum number of debug data files to keep. If there are more files generated,
  93. * the oldest ones will be removed.
  94. */
  95. public $historySize = 50;
  96. /**
  97. * @var bool whether to enable message logging for the requests about debug module actions.
  98. * You normally do not want to keep these logs because they may distract you from the logs about your applications.
  99. * You may want to enable the debug logs if you want to investigate how the debug module itself works.
  100. */
  101. public $enableDebugLogs = false;
  102. /**
  103. * @var bool whether to disable IP address restriction warning triggered by checkAccess function
  104. * @since 2.0.14
  105. */
  106. public $disableIpRestrictionWarning = false;
  107. /**
  108. * @var bool whether to disable access callback restriction warning triggered by checkAccess function
  109. * @since 2.1.0
  110. */
  111. public $disableCallbackRestrictionWarning = false;
  112. /**
  113. * @var mixed the string with placeholders to be be substituted or an anonymous function that returns the trace line string.
  114. * The placeholders are {file}, {line} and {text} and the string should be as follows:
  115. *
  116. * `File: {file} - Line: {line} - Text: {text}`
  117. *
  118. * The signature of the anonymous function should be as follows:
  119. *
  120. * ```php
  121. * function($trace, $panel) {
  122. * // compute line string
  123. * return $line;
  124. * }
  125. * ```
  126. * @since 2.0.7
  127. */
  128. public $traceLine = self::DEFAULT_IDE_TRACELINE;
  129. /**
  130. * @var string Yii logo URL
  131. */
  132. private static $_yiiLogo = 'data:image/png;base64,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';
  133. /**
  134. * Returns the logo URL to be used in `<img src="`
  135. *
  136. * @return string the logo URL
  137. */
  138. public static function getYiiLogo()
  139. {
  140. return self::$_yiiLogo;
  141. }
  142. /**
  143. * Sets the logo URL to be used in `<img src="`
  144. *
  145. * @param string $logo the logo URL
  146. */
  147. public static function setYiiLogo($logo)
  148. {
  149. self::$_yiiLogo = $logo;
  150. }
  151. /**
  152. * {@inheritdoc}
  153. * @throws \yii\base\InvalidConfigException
  154. */
  155. public function init()
  156. {
  157. parent::init();
  158. $this->dataPath = Yii::getAlias($this->dataPath);
  159. if (Yii::$app instanceof \yii\web\Application) {
  160. $this->initPanels();
  161. }
  162. }
  163. /**
  164. * Initializes panels.
  165. * @throws \yii\base\InvalidConfigException
  166. */
  167. protected function initPanels()
  168. {
  169. // merge custom panels and core panels so that they are ordered mainly by custom panels
  170. if (empty($this->panels)) {
  171. $this->panels = $this->corePanels();
  172. } else {
  173. $corePanels = $this->corePanels();
  174. foreach ($corePanels as $id => $config) {
  175. if (isset($this->panels[$id])) {
  176. unset($corePanels[$id]);
  177. }
  178. }
  179. $this->panels = array_filter(array_merge($corePanels, $this->panels));
  180. }
  181. foreach ($this->panels as $id => $config) {
  182. if (is_string($config)) {
  183. $config = ['class' => $config];
  184. }
  185. $config['module'] = $this;
  186. $config['id'] = $id;
  187. $this->panels[$id] = Yii::createObject($config);
  188. if ($this->panels[$id] instanceof Panel && !$this->panels[$id]->isEnabled()) {
  189. unset($this->panels[$id]);
  190. }
  191. }
  192. }
  193. /**
  194. * {@inheritdoc}
  195. */
  196. public function bootstrap($app)
  197. {
  198. /* @var $app \yii\base\Application */
  199. $this->logTarget = $app->getLog()->targets['debug'] = new LogTarget($this);
  200. // delay attaching event handler to the view component after it is fully configured
  201. $app->on(Application::EVENT_BEFORE_REQUEST, function () use ($app) {
  202. $app->getView()->on(View::EVENT_END_BODY, [$this, 'renderToolbar']);
  203. $app->getResponse()->on(Response::EVENT_AFTER_PREPARE, [$this, 'setDebugHeaders']);
  204. });
  205. $app->getUrlManager()->addRules([
  206. [
  207. 'class' => 'yii\web\UrlRule',
  208. 'route' => $this->id,
  209. 'pattern' => $this->id,
  210. 'suffix' => false
  211. ],
  212. [
  213. 'class' => 'yii\web\UrlRule',
  214. 'route' => $this->id . '/<controller>/<action>',
  215. 'pattern' => $this->id . '/<controller:[\w\-]+>/<action:[\w\-]+>',
  216. 'suffix' => false
  217. ]
  218. ], false);
  219. }
  220. /**
  221. * {@inheritdoc}
  222. * @throws \yii\base\InvalidConfigException
  223. * @throws ForbiddenHttpException
  224. */
  225. public function beforeAction($action)
  226. {
  227. if (!$this->enableDebugLogs) {
  228. foreach ($this->get('log')->targets as $target) {
  229. $target->enabled = false;
  230. }
  231. }
  232. if (!parent::beforeAction($action)) {
  233. return false;
  234. }
  235. // do not display debug toolbar when in debug view mode
  236. Yii::$app->getView()->off(View::EVENT_END_BODY, [$this, 'renderToolbar']);
  237. Yii::$app->getResponse()->off(Response::EVENT_AFTER_PREPARE, [$this, 'setDebugHeaders']);
  238. if ($this->checkAccess($action)) {
  239. $this->resetGlobalSettings();
  240. return true;
  241. }
  242. if ($action->id === 'toolbar') {
  243. // Accessing toolbar remotely is normal. Do not throw exception.
  244. return false;
  245. }
  246. throw new ForbiddenHttpException('You are not allowed to access this page.');
  247. }
  248. /**
  249. * Setting headers to transfer debug data in AJAX requests
  250. * without interfering with the request itself.
  251. *
  252. * @param \yii\base\Event $event
  253. * @since 2.0.7
  254. */
  255. public function setDebugHeaders($event)
  256. {
  257. if (!$this->checkAccess()) {
  258. return;
  259. }
  260. $url = Url::toRoute([
  261. '/' . $this->id . '/default/view',
  262. 'tag' => $this->logTarget->tag,
  263. ]);
  264. $event->sender->getHeaders()
  265. ->set('X-Debug-Tag', $this->logTarget->tag)
  266. ->set('X-Debug-Duration', number_format((microtime(true) - YII_BEGIN_TIME) * 1000 + 1))
  267. ->set('X-Debug-Link', $url);
  268. }
  269. /**
  270. * Resets potentially incompatible global settings done in app config.
  271. */
  272. protected function resetGlobalSettings()
  273. {
  274. Yii::$app->assetManager->bundles = [];
  275. }
  276. /**
  277. * Gets toolbar HTML
  278. * @since 2.0.7
  279. */
  280. public function getToolbarHtml()
  281. {
  282. $url = Url::toRoute([
  283. '/' . $this->id . '/default/toolbar',
  284. 'tag' => $this->logTarget->tag,
  285. ]);
  286. return '<div id="yii-debug-toolbar" data-url="' . Html::encode($url) . '" style="display:none" class="yii-debug-toolbar-bottom"></div>';
  287. }
  288. /**
  289. * Renders mini-toolbar at the end of page body.
  290. *
  291. * @param \yii\base\Event $event
  292. * @throws \Throwable
  293. */
  294. public function renderToolbar($event)
  295. {
  296. if (!$this->checkAccess() || Yii::$app->getRequest()->getIsAjax()) {
  297. return;
  298. }
  299. /* @var $view View */
  300. $view = $event->sender;
  301. echo $view->renderDynamic('return Yii::$app->getModule("' . $this->id . '")->getToolbarHtml();');
  302. // echo is used in order to support cases where asset manager is not available
  303. echo '<style>' . $view->renderPhpFile(__DIR__ . '/assets/css/toolbar.css') . '</style>';
  304. echo '<script>' . $view->renderPhpFile(__DIR__ . '/assets/js/toolbar.js') . '</script>';
  305. }
  306. /**
  307. * Checks if current user is allowed to access the module
  308. * @param \yii\base\Action|null $action the action to be executed. May be `null` when called from
  309. * a non action context
  310. * @return bool if access is granted
  311. */
  312. protected function checkAccess($action = null)
  313. {
  314. $allowed = false;
  315. $ip = Yii::$app->getRequest()->getUserIP();
  316. foreach ($this->allowedIPs as $filter) {
  317. if ($filter === '*' || $filter === $ip || (($pos = strpos($filter, '*')) !== false && !strncmp($ip, $filter, $pos))) {
  318. $allowed = true;
  319. break;
  320. }
  321. }
  322. if ($allowed === false) {
  323. foreach ($this->allowedHosts as $hostname) {
  324. $filter = gethostbyname($hostname);
  325. if ($filter === $ip) {
  326. $allowed = true;
  327. break;
  328. }
  329. }
  330. }
  331. if ($allowed === false) {
  332. if (!$this->disableIpRestrictionWarning) {
  333. Yii::warning('Access to debugger is denied due to IP address restriction. The requesting IP address is ' . $ip, __METHOD__);
  334. }
  335. return false;
  336. }
  337. if ($this->checkAccessCallback !== null && call_user_func($this->checkAccessCallback, $action) !== true) {
  338. if (!$this->disableCallbackRestrictionWarning) {
  339. Yii::warning('Access to debugger is denied due to checkAccessCallback.', __METHOD__);
  340. }
  341. return false;
  342. }
  343. return true;
  344. }
  345. /**
  346. * @return array default set of panels
  347. */
  348. protected function corePanels()
  349. {
  350. return [
  351. 'config' => ['class' => 'yii\debug\panels\ConfigPanel'],
  352. 'request' => ['class' => 'yii\debug\panels\RequestPanel'],
  353. 'router' => ['class' => 'yii\debug\panels\RouterPanel'],
  354. 'log' => ['class' => 'yii\debug\panels\LogPanel'],
  355. 'profiling' => ['class' => 'yii\debug\panels\ProfilingPanel'],
  356. 'db' => ['class' => 'yii\debug\panels\DbPanel'],
  357. 'event' => ['class' => 'yii\debug\panels\EventPanel'],
  358. 'assets' => ['class' => 'yii\debug\panels\AssetPanel'],
  359. 'mail' => ['class' => 'yii\debug\panels\MailPanel'],
  360. 'timeline' => ['class' => 'yii\debug\panels\TimelinePanel'],
  361. 'user' => ['class' => 'yii\debug\panels\UserPanel'],
  362. 'dump' => ['class' => 'yii\debug\panels\DumpPanel'],
  363. ];
  364. }
  365. /**
  366. * {@inheritdoc}
  367. * @since 2.0.7
  368. */
  369. protected function defaultVersion()
  370. {
  371. $packageInfo = Json::decode(file_get_contents(dirname(__DIR__) . DIRECTORY_SEPARATOR . 'composer.json'));
  372. $extensionName = $packageInfo['name'];
  373. if (isset(Yii::$app->extensions[$extensionName])) {
  374. return Yii::$app->extensions[$extensionName]['version'];
  375. }
  376. return parent::defaultVersion();
  377. }
  378. }