admin.php 53 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971
  1. <?php
  2. namespace Grav\Plugin\Admin;
  3. use DateTime;
  4. use Grav\Common\Data;
  5. use Grav\Common\File\CompiledYamlFile;
  6. use Grav\Common\GPM\GPM;
  7. use Grav\Common\GPM\Licenses;
  8. use Grav\Common\GPM\Response;
  9. use Grav\Common\Grav;
  10. use Grav\Common\Helpers\YamlLinter;
  11. use Grav\Common\Language\LanguageCodes;
  12. use Grav\Common\Page\Collection;
  13. use Grav\Common\Page\Interfaces\PageInterface;
  14. use Grav\Common\Page\Page;
  15. use Grav\Common\Page\Pages;
  16. use Grav\Common\Plugins;
  17. use Grav\Common\Security;
  18. use Grav\Common\Session;
  19. use Grav\Common\Themes;
  20. use Grav\Common\Uri;
  21. use Grav\Common\User\Interfaces\UserCollectionInterface;
  22. use Grav\Common\User\User;
  23. use Grav\Common\Utils;
  24. use Grav\Framework\Collection\ArrayCollection;
  25. use Grav\Plugin\Login\Login;
  26. use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
  27. use PicoFeed\Parser\MalformedXmlException;
  28. use RocketTheme\Toolbox\Event\Event;
  29. use RocketTheme\Toolbox\File\File;
  30. use RocketTheme\Toolbox\File\JsonFile;
  31. use RocketTheme\Toolbox\ResourceLocator\UniformResourceIterator;
  32. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  33. use RocketTheme\Toolbox\Session\Message;
  34. use Grav\Common\Yaml;
  35. use Composer\Semver\Semver;
  36. use PicoFeed\Reader\Reader;
  37. define('LOGIN_REDIRECT_COOKIE', 'grav-login-redirect');
  38. class Admin
  39. {
  40. const MEDIA_PAGINATION_INTERVAL = 20;
  41. const TMP_COOKIE_NAME = 'tmp-admin-message';
  42. /** @var Grav */
  43. public $grav;
  44. /** @var string */
  45. public $base;
  46. /** @var string */
  47. public $location;
  48. /** @var string */
  49. public $route;
  50. /** @var User */
  51. public $user;
  52. /** @var array */
  53. public $forgot;
  54. /** @var string */
  55. public $task;
  56. /** @var array */
  57. public $json_response;
  58. /** @var Collection */
  59. public $collection;
  60. /** @var bool */
  61. public $multilang;
  62. /** @var array */
  63. public $languages_enabled;
  64. /** @var Uri $uri */
  65. protected $uri;
  66. /** @var array */
  67. protected $pages = [];
  68. /** @var Session */
  69. protected $session;
  70. /** @var Data\Blueprints */
  71. protected $blueprints;
  72. /** @var GPM */
  73. protected $gpm;
  74. /** @var int */
  75. protected $pages_count;
  76. /** @var array */
  77. protected $permissions;
  78. /** @var bool */
  79. protected $load_additional_files_in_background = false;
  80. /** @var bool */
  81. protected $loading_additional_files_in_background = false;
  82. /** @var array */
  83. protected $temp_messages = [];
  84. /**
  85. * Constructor.
  86. *
  87. * @param Grav $grav
  88. * @param string $base
  89. * @param string $location
  90. * @param string $route
  91. */
  92. public function __construct(Grav $grav, $base, $location, $route)
  93. {
  94. // Register admin to grav because of calling $grav['user'] requires it.
  95. $grav['admin'] = $this;
  96. $this->grav = $grav;
  97. $this->base = $base;
  98. $this->location = $location;
  99. $this->route = $route;
  100. $this->uri = $grav['uri'];
  101. $this->session = $grav['session'];
  102. $this->user = $grav['user'];
  103. $this->permissions = [];
  104. $language = $grav['language'];
  105. // Load utility class
  106. if ($language->enabled()) {
  107. $this->multilang = true;
  108. $this->languages_enabled = (array)$this->grav['config']->get('system.languages.supported', []);
  109. //Set the currently active language for the admin
  110. $language = $this->grav['uri']->param('lang');
  111. if (!$language) {
  112. if (!$this->session->admin_lang) {
  113. $this->session->admin_lang = $this->grav['language']->getLanguage();
  114. }
  115. $language = $this->session->admin_lang;
  116. }
  117. $this->grav['language']->setActive($language ?: 'en');
  118. } else {
  119. $this->grav['language']->setActive('en');
  120. $this->multilang = false;
  121. }
  122. }
  123. /**
  124. * Return the languages available in the admin
  125. *
  126. * @return array
  127. */
  128. public static function adminLanguages()
  129. {
  130. $languages = [];
  131. $path = Grav::instance()['locator']->findResource('plugins://admin/languages');
  132. /** @var \DirectoryIterator $directory */
  133. foreach (new \DirectoryIterator($path) as $file) {
  134. if ($file->isDir() || $file->isDot() || Utils::startsWith($file->getFilename(), '.')) {
  135. continue;
  136. }
  137. $lang = $file->getBasename('.yaml');
  138. $languages[$lang] = LanguageCodes::getNativeName($lang);
  139. }
  140. // sort languages
  141. asort($languages);
  142. return $languages;
  143. }
  144. /**
  145. * Return the found configuration blueprints
  146. *
  147. * @return array
  148. */
  149. public static function configurations()
  150. {
  151. $configurations = [];
  152. /** @var UniformResourceIterator $iterator */
  153. $iterator = Grav::instance()['locator']->getIterator('blueprints://config');
  154. foreach ($iterator as $file) {
  155. if ($file->isDir() || !preg_match('/^[^.].*.yaml$/', $file->getFilename())) {
  156. continue;
  157. }
  158. $configurations[] = $file->getBasename('.yaml');
  159. }
  160. return $configurations;
  161. }
  162. /**
  163. * Return the tools found
  164. *
  165. * @return array
  166. */
  167. public static function tools()
  168. {
  169. $tools = [];
  170. Grav::instance()->fireEvent('onAdminTools', new Event(['tools' => &$tools]));
  171. return $tools;
  172. }
  173. public static function toolsPermissions()
  174. {
  175. $tools = static::tools();
  176. $perms = [];
  177. foreach ($tools as $tool) {
  178. $perms = array_merge($perms, $tool[0]);
  179. }
  180. return array_unique($perms);
  181. }
  182. /**
  183. * Return the languages available in the site
  184. *
  185. * @return array
  186. */
  187. public static function siteLanguages()
  188. {
  189. $languages = [];
  190. $lang_data = (array) Grav::instance()['config']->get('system.languages.supported', []);
  191. foreach ($lang_data as $index => $lang) {
  192. $languages[$lang] = LanguageCodes::getNativeName($lang);
  193. }
  194. return $languages;
  195. }
  196. /**
  197. * Static helper method to return the admin form nonce
  198. *
  199. * @return string
  200. */
  201. public static function getNonce()
  202. {
  203. $action = 'admin-form';
  204. return Utils::getNonce($action);
  205. }
  206. /**
  207. * Static helper method to return the last used page name
  208. *
  209. * @return string
  210. */
  211. public static function getLastPageName()
  212. {
  213. return Grav::instance()['session']->lastPageName ?: 'default';
  214. }
  215. /**
  216. * Static helper method to return the last used page route
  217. *
  218. * @return string
  219. */
  220. public static function getLastPageRoute()
  221. {
  222. return Grav::instance()['session']->lastPageRoute ?: self::route();
  223. }
  224. /**
  225. * Static helper method to return current route.
  226. *
  227. * @return string
  228. */
  229. public static function route()
  230. {
  231. $pages = Grav::instance()['pages'];
  232. $route = '/' . ltrim(Grav::instance()['admin']->route, '/');
  233. /** @var PageInterface $page */
  234. $page = $pages->dispatch($route);
  235. $parent_route = null;
  236. if ($page) {
  237. /** @var PageInterface $parent */
  238. $parent = $page->parent();
  239. $parent_route = $parent->rawRoute();
  240. }
  241. return $parent_route;
  242. }
  243. public static function getTempDir()
  244. {
  245. try {
  246. $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
  247. } catch (\Exception $e) {
  248. $tmp_dir = Grav::instance()['locator']->findResource('cache://', true, true) . '/tmp';
  249. }
  250. return $tmp_dir;
  251. }
  252. public static function getPageMedia()
  253. {
  254. $files = [];
  255. $grav = Grav::instance();
  256. $pages = $grav['pages'];
  257. $route = '/' . ltrim($grav['admin']->route, '/');
  258. /** @var PageInterface $page */
  259. $page = $pages->dispatch($route);
  260. $parent_route = null;
  261. if ($page) {
  262. $media = $page->media()->all();
  263. $files = array_keys($media);
  264. }
  265. return $files;
  266. }
  267. /**
  268. * Get current session.
  269. *
  270. * @return Session
  271. */
  272. public function session()
  273. {
  274. return $this->session;
  275. }
  276. /**
  277. * Fetch and delete messages from the session queue.
  278. *
  279. * @param string $type
  280. *
  281. * @return array
  282. */
  283. public function messages($type = null)
  284. {
  285. /** @var Message $messages */
  286. $messages = $this->grav['messages'];
  287. return $messages->fetch($type);
  288. }
  289. /**
  290. * Authenticate user.
  291. *
  292. * @param array $credentials User credentials.
  293. */
  294. public function authenticate($credentials, $post)
  295. {
  296. /** @var Login $login */
  297. $login = $this->grav['login'];
  298. // Remove login nonce from the form.
  299. $credentials = array_diff_key($credentials, ['admin-nonce' => true]);
  300. $twofa = $this->grav['config']->get('plugins.admin.twofa_enabled', false);
  301. $rateLimiter = $login->getRateLimiter('login_attempts');
  302. $userKey = (string)($credentials['username'] ?? '');
  303. $ipKey = Uri::ip();
  304. $redirect = $post['redirect'] ?? $this->base . $this->route;
  305. // Pseudonymization of the IP
  306. $ipKey = sha1($ipKey . $this->grav['config']->get('security.salt'));
  307. // Check if the current IP has been used in failed login attempts.
  308. $attempts = count($rateLimiter->getAttempts($ipKey, 'ip'));
  309. $rateLimiter->registerRateLimitedAction($ipKey, 'ip')->registerRateLimitedAction($userKey);
  310. // Check rate limit for both IP and user, but allow each IP a single try even if user is already rate limited.
  311. if ($rateLimiter->isRateLimited($ipKey, 'ip') || ($attempts && $rateLimiter->isRateLimited($userKey))) {
  312. $this->setMessage(static::translate(['PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $rateLimiter->getInterval()]), 'error');
  313. $this->grav->redirect('/');
  314. }
  315. // Fire Login process.
  316. $event = $login->login(
  317. $credentials,
  318. ['admin' => true, 'twofa' => $twofa],
  319. ['authorize' => 'admin.login', 'return_event' => true]
  320. );
  321. $user = $event->getUser();
  322. if ($user->authenticated) {
  323. $rateLimiter->resetRateLimit($ipKey, 'ip')->resetRateLimit($userKey);
  324. if ($user->authorized) {
  325. $event->defMessage('PLUGIN_ADMIN.LOGIN_LOGGED_IN', 'info');
  326. $event->defRedirect($post['redirect'] ?? $redirect);
  327. } else {
  328. $this->session->redirect = $redirect;
  329. }
  330. } else {
  331. if ($user->authorized) {
  332. $event->defMessage('PLUGIN_LOGIN.ACCESS_DENIED', 'error');
  333. } else {
  334. $event->defMessage('PLUGIN_LOGIN.LOGIN_FAILED', 'error');
  335. }
  336. }
  337. $event->defRedirect($redirect);
  338. $message = $event->getMessage();
  339. if ($message) {
  340. $this->setMessage(static::translate($message), $event->getMessageType());
  341. }
  342. $redirect = $event->getRedirect();
  343. $this->grav->redirect($redirect, $event->getRedirectCode());
  344. }
  345. /**
  346. * Check Two-Factor Authentication.
  347. */
  348. public function twoFa($data, $post)
  349. {
  350. /** @var Login $login */
  351. $login = $this->grav['login'];
  352. /** @var TwoFactorAuth $twoFa */
  353. $twoFa = $login->twoFactorAuth();
  354. $user = $this->grav['user'];
  355. $code = $data['2fa_code'] ?? null;
  356. $secret = $user->twofa_secret ?? null;
  357. if (!$code || !$secret || !$twoFa->verifyCode($secret, $code)) {
  358. $login->logout(['admin' => true]);
  359. $this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate('PLUGIN_ADMIN.2FA_FAILED'), 'status' => 'error']);
  360. $this->grav->redirect($this->uri->route(), 303);
  361. }
  362. $this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'), 'info');
  363. $user->authorized = true;
  364. $this->grav->redirect($post['redirect']);
  365. }
  366. /**
  367. * Logout from admin.
  368. */
  369. public function logout($data, $post)
  370. {
  371. /** @var Login $login */
  372. $login = $this->grav['login'];
  373. $event = $login->logout(['admin' => true], ['return_event' => true]);
  374. $event->defMessage('PLUGIN_ADMIN.LOGGED_OUT', 'info');
  375. $message = $event->getMessage();
  376. if ($message) {
  377. $this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate($message), 'status' => $event->getMessageType()]);
  378. }
  379. $this->grav->redirect($this->base);
  380. }
  381. /**
  382. * @return bool
  383. */
  384. public static function doAnyUsersExist()
  385. {
  386. $accounts = Grav::instance()['accounts'] ?? null;
  387. if ($accounts instanceof \Countable) {
  388. return $accounts->count() > 0;
  389. }
  390. // TODO: remove old way to check for existence of a user account (Grav < v1.6.9)
  391. $account_dir = $file_path = Grav::instance()['locator']->findResource('account://');
  392. $user_check = glob($account_dir . '/*.yaml');
  393. return $user_check;
  394. }
  395. /**
  396. * Add message into the session queue.
  397. *
  398. * @param string $msg
  399. * @param string $type
  400. */
  401. public function setMessage($msg, $type = 'info')
  402. {
  403. /** @var Message $messages */
  404. $messages = $this->grav['messages'];
  405. $messages->add($msg, $type);
  406. }
  407. public function addTempMessage($msg, $type)
  408. {
  409. $this->temp_messages[] = ['message' => $msg, 'scope' => $type];
  410. }
  411. public function getTempMessages()
  412. {
  413. return $this->temp_messages;
  414. }
  415. /**
  416. * Translate a string to the user-defined language
  417. *
  418. * @param array|mixed $args
  419. *
  420. * @param mixed $languages
  421. *
  422. * @return string
  423. */
  424. public static function translate($args, $languages = null)
  425. {
  426. $grav = Grav::instance();
  427. if (is_array($args)) {
  428. $lookup = array_shift($args);
  429. } else {
  430. $lookup = $args;
  431. $args = [];
  432. }
  433. if (!$languages) {
  434. if ($grav['config']->get('system.languages.translations_fallback', true)) {
  435. $languages = $grav['language']->getFallbackLanguages();
  436. } else {
  437. $languages = (array)$grav['language']->getDefault();
  438. }
  439. $languages = $grav['user']->authenticated ? [ $grav['user']->language ] : $languages;
  440. } else {
  441. $languages = (array)$languages;
  442. }
  443. foreach ((array)$languages as $lang) {
  444. $translation = $grav['language']->getTranslation($lang, $lookup, true);
  445. if (!$translation) {
  446. $language = $grav['language']->getDefault() ?: 'en';
  447. $translation = $grav['language']->getTranslation($language, $lookup, true);
  448. }
  449. if (!$translation) {
  450. $language = 'en';
  451. $translation = $grav['language']->getTranslation($language, $lookup, true);
  452. }
  453. if ($translation) {
  454. if (count($args) >= 1) {
  455. return vsprintf($translation, $args);
  456. }
  457. return $translation;
  458. }
  459. }
  460. return $lookup;
  461. }
  462. /**
  463. * Checks user authorisation to the action.
  464. *
  465. * @param string|string[] $action
  466. *
  467. * @return bool
  468. */
  469. public function authorize($action = 'admin.login')
  470. {
  471. $action = (array)$action;
  472. foreach ($action as $a) {
  473. if ($this->user->authorize($a)) {
  474. return true;
  475. }
  476. }
  477. return false;
  478. }
  479. /**
  480. * Gets configuration data.
  481. *
  482. * @param string $type
  483. * @param array $post
  484. *
  485. * @return mixed
  486. * @throws \RuntimeException
  487. */
  488. public function data($type, array $post = [])
  489. {
  490. static $data = [];
  491. if (isset($data[$type])) {
  492. return $data[$type];
  493. }
  494. if (!$post) {
  495. $post = $this->grav['uri']->post();
  496. $post = $post['data'] ?? [];
  497. }
  498. // Check to see if a data type is plugin-provided, before looking into core ones
  499. $event = $this->grav->fireEvent('onAdminData', new Event(['type' => &$type]));
  500. if ($event) {
  501. if (isset($event['data_type'])) {
  502. return $event['data_type'];
  503. }
  504. if (is_string($event['type'])) {
  505. $type = $event['type'];
  506. }
  507. }
  508. /** @var UniformResourceLocator $locator */
  509. $locator = $this->grav['locator'];
  510. $filename = $locator->findResource("config://{$type}.yaml", true, true);
  511. $file = CompiledYamlFile::instance($filename);
  512. if (preg_match('|plugins/|', $type)) {
  513. /** @var Plugins $plugins */
  514. $plugins = $this->grav['plugins'];
  515. $obj = $plugins->get(preg_replace('|plugins/|', '', $type));
  516. if (!$obj) {
  517. return [];
  518. }
  519. $obj->merge($post);
  520. $obj->file($file);
  521. $data[$type] = $obj;
  522. } elseif (preg_match('|themes/|', $type)) {
  523. /** @var Themes $themes */
  524. $themes = $this->grav['themes'];
  525. $obj = $themes->get(preg_replace('|themes/|', '', $type));
  526. if (!$obj) {
  527. return [];
  528. }
  529. $obj->merge($post);
  530. $obj->file($file);
  531. $data[$type] = $obj;
  532. } elseif (preg_match('|users?/|', $type)) {
  533. /** @var UserCollectionInterface $users */
  534. $users = $this->grav['accounts'];
  535. $obj = $users->load(preg_replace('|users?/|', '', $type));
  536. $obj->update($this->cleanUserPost($post));
  537. $data[$type] = $obj;
  538. } elseif (preg_match('|config/|', $type)) {
  539. $type = preg_replace('|config/|', '', $type);
  540. $blueprints = $this->blueprints("config/{$type}");
  541. $config = $this->grav['config'];
  542. $obj = new Data\Data($config->get($type, []), $blueprints);
  543. $obj->merge($post);
  544. // FIXME: We shouldn't allow user to change configuration files in system folder!
  545. $filename = $this->grav['locator']->findResource("config://{$type}.yaml")
  546. ?: $this->grav['locator']->findResource("config://{$type}.yaml", true, true);
  547. $file = CompiledYamlFile::instance($filename);
  548. $obj->file($file);
  549. $data[$type] = $obj;
  550. } elseif (preg_match('|media-manager/|', $type)) {
  551. $filename = base64_decode(preg_replace('|media-manager/|', '', $type));
  552. $file = File::instance($filename);
  553. $obj = new \stdClass();
  554. $obj->title = $file->basename();
  555. $obj->path = $file->filename();
  556. $obj->file = $file;
  557. $obj->page = $this->grav['pages']->get(dirname($obj->path));
  558. $fileInfo = pathinfo($obj->title);
  559. $filename = str_replace(['@3x', '@2x'], '', $fileInfo['filename']);
  560. if (isset($fileInfo['extension'])) {
  561. $filename .= '.' . $fileInfo['extension'];
  562. }
  563. if ($obj->page && isset($obj->page->media()[$filename])) {
  564. $obj->metadata = new Data\Data($obj->page->media()[$filename]->metadata());
  565. }
  566. $data[$type] = $obj;
  567. } else {
  568. throw new \RuntimeException("Data type '{$type}' doesn't exist!");
  569. }
  570. return $data[$type];
  571. }
  572. /**
  573. * Clean user form post and remove extra stuff that may be passed along
  574. *
  575. * @param array $post
  576. * @return array
  577. */
  578. public function cleanUserPost($post)
  579. {
  580. // Clean fields for all users
  581. unset($post['hashed_password']);
  582. // Clean field for users who shouldn't be able to modify these fields
  583. if (!$this->authorize(['admin.user', 'admin.super'])) {
  584. unset($post['access'], $post['state']);
  585. }
  586. return $post;
  587. }
  588. protected function hasErrorMessage()
  589. {
  590. $msgs = $this->grav['messages']->all();
  591. foreach ($msgs as $msg) {
  592. if (isset($msg['scope']) && $msg['scope'] === 'error') {
  593. return true;
  594. }
  595. }
  596. return false;
  597. }
  598. /**
  599. * Returns blueprints for the given type.
  600. *
  601. * @param string $type
  602. *
  603. * @return Data\Blueprint
  604. */
  605. public function blueprints($type)
  606. {
  607. if ($this->blueprints === null) {
  608. $this->blueprints = new Data\Blueprints('blueprints://');
  609. }
  610. return $this->blueprints->get($type);
  611. }
  612. /**
  613. * Converts dot notation to array notation.
  614. *
  615. * @param string $name
  616. *
  617. * @return string
  618. */
  619. public function field($name)
  620. {
  621. $path = explode('.', $name);
  622. return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : '');
  623. }
  624. /**
  625. * Get all routes.
  626. *
  627. * @param bool $unique
  628. *
  629. * @return array
  630. */
  631. public function routes($unique = false)
  632. {
  633. /** @var Pages $pages */
  634. $pages = $this->grav['pages'];
  635. if ($unique) {
  636. $routes = array_unique($pages->routes());
  637. } else {
  638. $routes = $pages->routes();
  639. }
  640. return $routes;
  641. }
  642. /**
  643. * Count the pages
  644. *
  645. * @return int
  646. */
  647. public function pagesCount()
  648. {
  649. if (!$this->pages_count) {
  650. $this->pages_count = count($this->grav['pages']->all());
  651. }
  652. return $this->pages_count;
  653. }
  654. /**
  655. * Get all template types
  656. *
  657. * @return array
  658. */
  659. public function types()
  660. {
  661. return Pages::types();
  662. }
  663. /**
  664. * Get all modular template types
  665. *
  666. * @return array
  667. */
  668. public function modularTypes()
  669. {
  670. return Pages::modularTypes();
  671. }
  672. /**
  673. * Get all access levels
  674. *
  675. * @return array
  676. */
  677. public function accessLevels()
  678. {
  679. if (method_exists($this->grav['pages'], 'accessLevels')) {
  680. return $this->grav['pages']->accessLevels();
  681. }
  682. return [];
  683. }
  684. public function license($package_slug)
  685. {
  686. return Licenses::get($package_slug);
  687. }
  688. /**
  689. * Generate an array of dependencies for a package, used to generate a list of
  690. * packages that can be removed when removing a package.
  691. *
  692. * @param string $slug The package slug
  693. *
  694. * @return array|bool
  695. */
  696. public function dependenciesThatCanBeRemovedWhenRemoving($slug)
  697. {
  698. $gpm = $this->gpm();
  699. if (!$gpm) {
  700. return false;
  701. }
  702. $dependencies = [];
  703. $package = $this->getPackageFromGPM($slug);
  704. if ($package) {
  705. if ($package->dependencies) {
  706. foreach ($package->dependencies as $dependency) {
  707. // if (count($gpm->getPackagesThatDependOnPackage($dependency)) > 1) {
  708. // continue;
  709. // }
  710. if (isset($dependency['name'])) {
  711. $dependency = $dependency['name'];
  712. }
  713. if (!in_array($dependency, $dependencies, true)) {
  714. if (!in_array($dependency, ['admin', 'form', 'login', 'email', 'php'])) {
  715. $dependencies[] = $dependency;
  716. }
  717. }
  718. }
  719. }
  720. }
  721. return $dependencies;
  722. }
  723. /**
  724. * Get the GPM instance
  725. *
  726. * @return GPM The GPM instance
  727. */
  728. public function gpm()
  729. {
  730. if (!$this->gpm) {
  731. try {
  732. $this->gpm = new GPM();
  733. } catch (\Exception $e) {
  734. $this->setMessage($e->getMessage(), 'error');
  735. }
  736. }
  737. return $this->gpm;
  738. }
  739. public function getPackageFromGPM($package_slug)
  740. {
  741. $package = $this->plugins(true)[$package_slug];
  742. if (!$package) {
  743. $package = $this->themes(true)[$package_slug];
  744. }
  745. return $package;
  746. }
  747. /**
  748. * Get all plugins.
  749. *
  750. * @param bool $local
  751. *
  752. * @return mixed
  753. */
  754. public function plugins($local = true)
  755. {
  756. $gpm = $this->gpm();
  757. if (!$gpm) {
  758. return false;
  759. }
  760. if ($local) {
  761. return $gpm->getInstalledPlugins();
  762. }
  763. $plugins = $gpm->getRepositoryPlugins();
  764. if ($plugins) {
  765. return $plugins->filter(function ($package, $slug) use ($gpm) {
  766. return !$gpm->isPluginInstalled($slug);
  767. });
  768. }
  769. return [];
  770. }
  771. /**
  772. * Get all themes.
  773. *
  774. * @param bool $local
  775. *
  776. * @return mixed
  777. */
  778. public function themes($local = true)
  779. {
  780. $gpm = $this->gpm();
  781. if (!$gpm) {
  782. return false;
  783. }
  784. if ($local) {
  785. return $gpm->getInstalledThemes();
  786. }
  787. $themes = $gpm->getRepositoryThemes();
  788. if ($themes) {
  789. return $themes->filter(function ($package, $slug) use ($gpm) {
  790. return !$gpm->isThemeInstalled($slug);
  791. });
  792. }
  793. return [];
  794. }
  795. /**
  796. * Get list of packages that depend on the passed package slug
  797. *
  798. * @param string $slug The package slug
  799. *
  800. * @return array|bool
  801. */
  802. public function getPackagesThatDependOnPackage($slug)
  803. {
  804. $gpm = $this->gpm();
  805. if (!$gpm) {
  806. return false;
  807. }
  808. return $gpm->getPackagesThatDependOnPackage($slug);
  809. }
  810. /**
  811. * Check the passed packages list can be updated
  812. *
  813. * @param array $packages
  814. *
  815. * @throws \Exception
  816. * @return bool
  817. */
  818. public function checkPackagesCanBeInstalled($packages)
  819. {
  820. $gpm = $this->gpm();
  821. if (!$gpm) {
  822. return false;
  823. }
  824. $this->gpm->checkPackagesCanBeInstalled($packages);
  825. return true;
  826. }
  827. /**
  828. * Get an array of dependencies needed to be installed or updated for a list of packages
  829. * to be installed.
  830. *
  831. * @param array $packages The packages slugs
  832. *
  833. * @return array|bool
  834. */
  835. public function getDependenciesNeededToInstall($packages)
  836. {
  837. $gpm = $this->gpm();
  838. if (!$gpm) {
  839. return false;
  840. }
  841. return $this->gpm->getDependencies($packages);
  842. }
  843. /**
  844. * Used by the Dashboard in the admin to display the X latest pages
  845. * that have been modified
  846. *
  847. * @param integer $count number of pages to pull back
  848. *
  849. * @return array|null
  850. */
  851. public function latestPages($count = 10)
  852. {
  853. /** @var Pages $pages */
  854. $pages = $this->grav['pages'];
  855. $latest = [];
  856. if (null === $pages->routes()) {
  857. return null;
  858. }
  859. foreach ($pages->routes() as $url => $path) {
  860. $page = $pages->dispatch($url, true);
  861. if ($page && $page->routable()) {
  862. $latest[$page->route()] = ['modified' => $page->modified(), 'page' => $page];
  863. }
  864. }
  865. // sort based on modified
  866. uasort($latest, function ($a, $b) {
  867. if ($a['modified'] == $b['modified']) {
  868. return 0;
  869. }
  870. return ($a['modified'] > $b['modified']) ? -1 : 1;
  871. });
  872. // build new array with just pages in it
  873. $list = [];
  874. foreach ($latest as $item) {
  875. $list[] = $item['page'];
  876. }
  877. return array_slice($list, 0, $count);
  878. }
  879. /**
  880. * Get log file for fatal errors.
  881. *
  882. * @return string
  883. */
  884. public function logEntry()
  885. {
  886. $file = File::instance($this->grav['locator']->findResource("log://{$this->route}.html"));
  887. $content = $file->content();
  888. $file->free();
  889. return $content;
  890. }
  891. /**
  892. * Search in the logs when was the latest backup made
  893. *
  894. * @return array Array containing the latest backup information
  895. */
  896. public function lastBackup()
  897. {
  898. $file = JsonFile::instance($this->grav['locator']->findResource('log://backup.log'));
  899. $content = $file->content();
  900. if (empty($content)) {
  901. return [
  902. 'days' => '&infin;',
  903. 'chart_fill' => 100,
  904. 'chart_empty' => 0
  905. ];
  906. }
  907. $backup = new \DateTime();
  908. $backup->setTimestamp($content['time']);
  909. $diff = $backup->diff(new \DateTime());
  910. $days = $diff->days;
  911. $chart_fill = $days > 30 ? 100 : round($days / 30 * 100);
  912. return [
  913. 'days' => $days,
  914. 'chart_fill' => $chart_fill,
  915. 'chart_empty' => 100 - $chart_fill
  916. ];
  917. }
  918. /**
  919. * Determine if the plugin or theme info passed is from Team Grav
  920. *
  921. * @param object $info Plugin or Theme info object
  922. *
  923. * @return bool
  924. */
  925. public function isTeamGrav($info)
  926. {
  927. return isset($info['author']['name']) && ($info['author']['name'] === 'Team Grav' || Utils::contains($info['author']['name'], 'Trilby Media'));
  928. }
  929. /**
  930. * Determine if the plugin or theme info passed is premium
  931. *
  932. * @param object $info Plugin or Theme info object
  933. *
  934. * @return bool
  935. */
  936. public function isPremiumProduct($info)
  937. {
  938. return isset($info['premium']);
  939. }
  940. /**
  941. * Renders phpinfo
  942. *
  943. * @return string The phpinfo() output
  944. */
  945. function phpinfo()
  946. {
  947. if (function_exists('phpinfo')) {
  948. ob_start();
  949. phpinfo();
  950. $pinfo = ob_get_contents();
  951. ob_end_clean();
  952. $pinfo = preg_replace('%^.*<body>(.*)</body>.*$%ms', '$1', $pinfo);
  953. return $pinfo;
  954. }
  955. return 'phpinfo() method is not available on this server.';
  956. }
  957. /**
  958. * Guest date format based on euro/US
  959. *
  960. * @param string $date
  961. *
  962. * @return string
  963. */
  964. public function guessDateFormat($date)
  965. {
  966. static $guess;
  967. $date_formats = [
  968. 'm/d/y',
  969. 'm/d/Y',
  970. 'n/d/y',
  971. 'n/d/Y',
  972. 'd-m-Y',
  973. 'd-m-y',
  974. ];
  975. $time_formats = [
  976. 'H:i',
  977. 'G:i',
  978. 'h:ia',
  979. 'g:ia'
  980. ];
  981. if (!isset($guess[$date])) {
  982. foreach ($date_formats as $date_format) {
  983. foreach ($time_formats as $time_format) {
  984. if ($this->validateDate($date, "$date_format $time_format")) {
  985. $guess[$date] = "$date_format $time_format";
  986. break 2;
  987. }
  988. if ($this->validateDate($date, "$time_format $date_format")) {
  989. $guess[$date] = "$time_format $date_format";
  990. break 2;
  991. }
  992. }
  993. }
  994. if (!isset($guess[$date])) {
  995. $guess[$date] = 'd-m-Y H:i';
  996. }
  997. }
  998. return $guess[$date];
  999. }
  1000. public function validateDate($date, $format)
  1001. {
  1002. $d = DateTime::createFromFormat($format, $date);
  1003. return $d && $d->format($format) == $date;
  1004. }
  1005. /**
  1006. * @param string $php_format
  1007. *
  1008. * @return string
  1009. */
  1010. public function dateformatToMomentJS($php_format)
  1011. {
  1012. $SYMBOLS_MATCHING = [
  1013. // Day
  1014. 'd' => 'DD',
  1015. 'D' => 'ddd',
  1016. 'j' => 'D',
  1017. 'l' => 'dddd',
  1018. 'N' => 'E',
  1019. 'S' => 'Do',
  1020. 'w' => 'd',
  1021. 'z' => 'DDD',
  1022. // Week
  1023. 'W' => 'W',
  1024. // Month
  1025. 'F' => 'MMMM',
  1026. 'm' => 'MM',
  1027. 'M' => 'MMM',
  1028. 'n' => 'M',
  1029. 't' => '',
  1030. // Year
  1031. 'L' => '',
  1032. 'o' => 'GGGG',
  1033. 'Y' => 'YYYY',
  1034. 'y' => 'yy',
  1035. // Time
  1036. 'a' => 'a',
  1037. 'A' => 'A',
  1038. 'B' => 'SSS',
  1039. 'g' => 'h',
  1040. 'G' => 'H',
  1041. 'h' => 'hh',
  1042. 'H' => 'HH',
  1043. 'i' => 'mm',
  1044. 's' => 'ss',
  1045. 'u' => '',
  1046. // Timezone
  1047. 'e' => '',
  1048. 'I' => '',
  1049. 'O' => 'ZZ',
  1050. 'P' => 'Z',
  1051. 'T' => 'z',
  1052. 'Z' => '',
  1053. // Full Date/Time
  1054. 'c' => '',
  1055. 'r' => 'llll ZZ',
  1056. 'U' => 'X'
  1057. ];
  1058. $js_format = '';
  1059. $escaping = false;
  1060. $len = strlen($php_format);
  1061. for ($i = 0; $i < $len; $i++) {
  1062. $char = $php_format[$i];
  1063. if ($char === '\\') // PHP date format escaping character
  1064. {
  1065. $i++;
  1066. if ($escaping) {
  1067. $js_format .= $php_format[$i];
  1068. } else {
  1069. $js_format .= '\'' . $php_format[$i];
  1070. }
  1071. $escaping = true;
  1072. } else {
  1073. if ($escaping) {
  1074. $js_format .= "'";
  1075. $escaping = false;
  1076. }
  1077. if (isset($SYMBOLS_MATCHING[$char])) {
  1078. $js_format .= $SYMBOLS_MATCHING[$char];
  1079. } else {
  1080. $js_format .= $char;
  1081. }
  1082. }
  1083. }
  1084. return $js_format;
  1085. }
  1086. /**
  1087. * Gets the entire permissions array
  1088. *
  1089. * @return array
  1090. */
  1091. public function getPermissions()
  1092. {
  1093. return $this->permissions;
  1094. }
  1095. /**
  1096. * Sets the entire permissions array
  1097. *
  1098. * @param array $permissions
  1099. */
  1100. public function setPermissions($permissions)
  1101. {
  1102. $this->permissions = $permissions;
  1103. }
  1104. /**
  1105. * Adds a permission to the permissions array
  1106. *
  1107. * @param array $permissions
  1108. */
  1109. public function addPermissions($permissions)
  1110. {
  1111. $this->permissions = array_merge($this->permissions, $permissions);
  1112. }
  1113. public function getNotifications($force = false)
  1114. {
  1115. $last_checked = null;
  1116. $filename = $this->grav['locator']->findResource('user://data/notifications/' . md5($this->grav['user']->username) . YAML_EXT, true, true);
  1117. $notifications_file = CompiledYamlFile::instance($filename);
  1118. $notifications_content = (array)$notifications_file->content();
  1119. $last_checked = $notifications_content['last_checked'] ?? null;
  1120. $notifications = $notifications_content['data'] ?? array();
  1121. $timeout = $this->grav['config']->get('system.session.timeout', 1800);
  1122. if ($force || !$last_checked || empty($notifications) || (time() - $last_checked > $timeout)) {
  1123. $body = Response::get('https://getgrav.org/notifications.json?' . time());
  1124. // $body = Response::get('http://localhost/notifications.json?' . time());
  1125. $notifications = json_decode($body, true);
  1126. // Sort by date
  1127. usort($notifications, function ($a, $b) {
  1128. return strcmp($a['date'], $b['date']);
  1129. });
  1130. // Reverse order and create a new array
  1131. $notifications = array_reverse($notifications);
  1132. $cleaned_notifications = [];
  1133. foreach ($notifications as $key => $notification) {
  1134. if (isset($notification['permissions']) && !$this->authorize($notification['permissions'])) {
  1135. continue;
  1136. }
  1137. if (isset($notification['dependencies'])) {
  1138. foreach ($notification['dependencies'] as $dependency => $constraints) {
  1139. if ($dependency === 'grav') {
  1140. if (!Semver::satisfies(GRAV_VERSION, $constraints)) {
  1141. continue;
  1142. }
  1143. } else {
  1144. $packages = array_merge($this->plugins()->toArray(), $this->themes()->toArray());
  1145. if (!isset($packages[$dependency])) {
  1146. continue;
  1147. } else {
  1148. $version = $packages[$dependency]['version'];
  1149. if (!Semver::satisfies($version, $constraints)) {
  1150. continue;
  1151. }
  1152. }
  1153. }
  1154. }
  1155. }
  1156. $cleaned_notifications[] = $notification;
  1157. }
  1158. // reset notifications
  1159. $notifications = [];
  1160. foreach($cleaned_notifications as $notification) {
  1161. foreach ($notification['location'] as $location) {
  1162. $notifications = array_merge_recursive($notifications, [$location => [$notification]]);
  1163. }
  1164. }
  1165. $notifications_file->content(['last_checked' => time(), 'data' => $notifications]);
  1166. $notifications_file->save();
  1167. }
  1168. return $notifications;
  1169. }
  1170. /**
  1171. * Get https://getgrav.org news feed
  1172. *
  1173. * @return mixed
  1174. * @throws MalformedXmlException
  1175. */
  1176. public function getFeed($force = false)
  1177. {
  1178. $last_checked = null;
  1179. $filename = $this->grav['locator']->findResource('user://data/feed/' . md5($this->grav['user']->username) . YAML_EXT, true, true);
  1180. $feed_file = CompiledYamlFile::instance($filename);
  1181. $feed_content = (array)$feed_file->content();
  1182. $last_checked = $feed_content['last_checked'] ?? null;
  1183. $feed = $feed_content['data'] ?? array();
  1184. $timeout = $this->grav['config']->get('system.session.timeout', 1800);
  1185. if ($force || !$last_checked || empty($feed) || ($last_checked && (time() - $last_checked > $timeout))) {
  1186. $feed_url = 'https://getgrav.org/blog.atom';
  1187. $body = Response::get($feed_url);
  1188. $reader = new Reader();
  1189. $parser = $reader->getParser($feed_url, $body, 'utf-8');
  1190. $data = $parser->execute()->getItems();
  1191. // Get top 10
  1192. $data = array_slice($data, 0, 10);
  1193. $feed = array_map(function ($entry) {
  1194. $simple_entry['title'] = $entry->getTitle();
  1195. $simple_entry['url'] = $entry->getUrl();
  1196. $simple_entry['date'] = $entry->getDate()->getTimestamp();
  1197. $simple_entry['nicetime'] = $this->adminNiceTime($simple_entry['date']);
  1198. return $simple_entry;
  1199. }, $data);
  1200. $feed_file->content(['last_checked' => time(), 'data' => $feed]);
  1201. $feed_file->save();
  1202. }
  1203. return $feed;
  1204. }
  1205. public function adminNiceTime($date, $long_strings = true)
  1206. {
  1207. if (empty($date)) {
  1208. return $this->translate('GRAV.NICETIME.NO_DATE_PROVIDED', null);
  1209. }
  1210. if ($long_strings) {
  1211. $periods = [
  1212. 'NICETIME.SECOND',
  1213. 'NICETIME.MINUTE',
  1214. 'NICETIME.HOUR',
  1215. 'NICETIME.DAY',
  1216. 'NICETIME.WEEK',
  1217. 'NICETIME.MONTH',
  1218. 'NICETIME.YEAR',
  1219. 'NICETIME.DECADE'
  1220. ];
  1221. } else {
  1222. $periods = [
  1223. 'NICETIME.SEC',
  1224. 'NICETIME.MIN',
  1225. 'NICETIME.HR',
  1226. 'NICETIME.DAY',
  1227. 'NICETIME.WK',
  1228. 'NICETIME.MO',
  1229. 'NICETIME.YR',
  1230. 'NICETIME.DEC'
  1231. ];
  1232. }
  1233. $lengths = ['60', '60', '24', '7', '4.35', '12', '10'];
  1234. $now = time();
  1235. // check if unix timestamp
  1236. if ((string)(int)$date === (string)$date) {
  1237. $unix_date = $date;
  1238. } else {
  1239. $unix_date = strtotime($date);
  1240. }
  1241. // check validity of date
  1242. if (empty($unix_date)) {
  1243. return $this->translate('GRAV.NICETIME.BAD_DATE', null);
  1244. }
  1245. // is it future date or past date
  1246. if ($now > $unix_date) {
  1247. $difference = $now - $unix_date;
  1248. $tense = $this->translate('GRAV.NICETIME.AGO', null);
  1249. } else {
  1250. $difference = $unix_date - $now;
  1251. $tense = $this->translate('GRAV.NICETIME.FROM_NOW', null);
  1252. }
  1253. $len = count($lengths) - 1;
  1254. for ($j = 0; $difference >= $lengths[$j] && $j < $len; $j++) {
  1255. $difference /= $lengths[$j];
  1256. }
  1257. $difference = round($difference);
  1258. if ($difference !== 1) {
  1259. $periods[$j] .= '_PLURAL';
  1260. }
  1261. if ($this->grav['language']->getTranslation($this->grav['user']->language,
  1262. $periods[$j] . '_MORE_THAN_TWO')
  1263. ) {
  1264. if ($difference > 2) {
  1265. $periods[$j] .= '_MORE_THAN_TWO';
  1266. }
  1267. }
  1268. $periods[$j] = $this->translate('GRAV.'.$periods[$j], null);
  1269. return "{$difference} {$periods[$j]} {$tense}";
  1270. }
  1271. public function findFormFields($type, $fields, $found_fields = [])
  1272. {
  1273. foreach ($fields as $key => $field) {
  1274. if (isset($field['type']) && $field['type'] == $type) {
  1275. $found_fields[$key] = $field;
  1276. } elseif (isset($field['fields'])) {
  1277. $result = $this->findFormFields($type, $field['fields'], $found_fields);
  1278. if (!empty($result)) {
  1279. $found_fields = array_merge($found_fields, $result);
  1280. }
  1281. }
  1282. }
  1283. return $found_fields;
  1284. }
  1285. public function getPagePathFromToken($path, $page = null)
  1286. {
  1287. return Utils::getPagePathFromToken($path, $page ?: $this->page(true));
  1288. }
  1289. /**
  1290. * Returns edited page.
  1291. *
  1292. * @param bool $route
  1293. *
  1294. * @param null $path
  1295. *
  1296. * @return PageInterface
  1297. */
  1298. public function page($route = false, $path = null)
  1299. {
  1300. if (!$path) {
  1301. $path = $this->route;
  1302. }
  1303. if ($route && !$path) {
  1304. $path = '/';
  1305. }
  1306. if (!isset($this->pages[$path])) {
  1307. $this->pages[$path] = $this->getPage($path);
  1308. }
  1309. return $this->pages[$path];
  1310. }
  1311. /**
  1312. * Returns the page creating it if it does not exist.
  1313. *
  1314. * @param string $path
  1315. *
  1316. * @return PageInterface|null
  1317. */
  1318. public function getPage($path)
  1319. {
  1320. /** @var Pages $pages */
  1321. $pages = $this->grav['pages'];
  1322. if ($path && $path[0] !== '/') {
  1323. $path = "/{$path}";
  1324. }
  1325. // Fix for entities in path causing looping...
  1326. $path = urldecode($path);
  1327. $page = $path ? $pages->dispatch($path, true) : $pages->root();
  1328. if (!$page) {
  1329. $slug = basename($path);
  1330. if ($slug === '') {
  1331. return null;
  1332. }
  1333. $ppath = str_replace('\\', '/', dirname($path));
  1334. // Find or create parent(s).
  1335. $parent = $this->getPage($ppath !== '/' ? $ppath : '');
  1336. // Create page.
  1337. $page = new Page();
  1338. $page->parent($parent);
  1339. $page->filePath($parent->path() . '/' . $slug . '/' . $page->name());
  1340. // Add routing information.
  1341. $pages->addPage($page, $path);
  1342. // Set if Modular
  1343. $page->modularTwig($slug[0] === '_');
  1344. // Determine page type.
  1345. if (isset($this->session->{$page->route()})) {
  1346. // Found the type and header from the session.
  1347. $data = $this->session->{$page->route()};
  1348. // Set the key header value
  1349. $header = ['title' => $data['title']];
  1350. if (isset($data['visible'])) {
  1351. if ($data['visible'] === '' || $data['visible']) {
  1352. // if auto (ie '')
  1353. $pageParent = $page->parent();
  1354. $children = $pageParent ? $pageParent->children() : [];
  1355. foreach ($children as $child) {
  1356. if ($child->order()) {
  1357. // set page order
  1358. $page->order(AdminController::getNextOrderInFolder($pageParent->path()));
  1359. break;
  1360. }
  1361. }
  1362. }
  1363. if ((int)$data['visible'] === 1 && !$page->order()) {
  1364. $header['visible'] = $data['visible'];
  1365. }
  1366. }
  1367. if ($data['name'] === 'modular') {
  1368. $header['body_classes'] = 'modular';
  1369. }
  1370. $name = $page->modular() ? str_replace('modular/', '', $data['name']) : $data['name'];
  1371. $page->name($name . '.md');
  1372. // Fire new event to allow plugins to manipulate page frontmatter
  1373. $this->grav->fireEvent('onAdminCreatePageFrontmatter', new Event(['header' => &$header,
  1374. 'data' => $data]));
  1375. $page->header($header);
  1376. $page->frontmatter(Yaml::dump((array)$page->header(), 20));
  1377. } else {
  1378. // Find out the type by looking at the parent.
  1379. $type = $parent->childType() ?: $parent->blueprints()->get('child_type', 'default');
  1380. $page->name($type . CONTENT_EXT);
  1381. $page->header();
  1382. }
  1383. }
  1384. return $page;
  1385. }
  1386. public function generateReports()
  1387. {
  1388. $reports = new ArrayCollection();
  1389. /** @var Pages $pages */
  1390. $pages = $this->grav['pages'];
  1391. // Default to XSS Security Report
  1392. $result = Security::detectXssFromPages($pages, true);
  1393. $reports['Grav Security Check'] = $this->grav['twig']->processTemplate('reports/security.html.twig', [
  1394. 'result' => $result,
  1395. ]);
  1396. // Linting Issues
  1397. $result = YamlLinter::lint();
  1398. $reports['Grav Yaml Linter'] = $this->grav['twig']->processTemplate('reports/yamllinter.html.twig', [
  1399. 'result' => $result,
  1400. ]);
  1401. // Fire new event to allow plugins to manipulate page frontmatter
  1402. $this->grav->fireEvent('onAdminGenerateReports', new Event(['reports' => $reports]));
  1403. return $reports;
  1404. }
  1405. public function getRouteDetails()
  1406. {
  1407. return [$this->base, $this->location, $this->route];
  1408. }
  1409. /**
  1410. * Get the files list
  1411. *
  1412. * @param bool $filtered
  1413. * @param int $page_index
  1414. * @return array|null
  1415. * @todo allow pagination
  1416. */
  1417. public function files($filtered = true, $page_index = 0)
  1418. {
  1419. $param_type = $this->grav['uri']->param('type');
  1420. $param_date = $this->grav['uri']->param('date');
  1421. $param_page = $this->grav['uri']->param('page');
  1422. $param_page = str_replace('\\', '/', $param_page);
  1423. $files_cache_key = 'media-manager-files';
  1424. if ($param_type) {
  1425. $files_cache_key .= "-{$param_type}";
  1426. }
  1427. if ($param_date) {
  1428. $files_cache_key .= "-{$param_date}";
  1429. }
  1430. if ($param_page) {
  1431. $files_cache_key .= "-{$param_page}";
  1432. }
  1433. $page_files = null;
  1434. $cache_enabled = $this->grav['config']->get('plugins.admin.cache_enabled');
  1435. if (!$cache_enabled) {
  1436. $this->grav['cache']->setEnabled(true);
  1437. }
  1438. $page_files = $this->grav['cache']->fetch(md5($files_cache_key));
  1439. if (!$cache_enabled) {
  1440. $this->grav['cache']->setEnabled(false);
  1441. }
  1442. if (!$page_files) {
  1443. $page_files = [];
  1444. $pages = $this->grav['pages'];
  1445. if ($param_page) {
  1446. $page = $pages->dispatch($param_page);
  1447. $page_files = $this->getFiles('images', $page, $page_files, $filtered);
  1448. $page_files = $this->getFiles('videos', $page, $page_files, $filtered);
  1449. $page_files = $this->getFiles('audios', $page, $page_files, $filtered);
  1450. $page_files = $this->getFiles('files', $page, $page_files, $filtered);
  1451. } else {
  1452. $allPages = $pages->all();
  1453. if ($allPages) foreach ($allPages as $page) {
  1454. $page_files = $this->getFiles('images', $page, $page_files, $filtered);
  1455. $page_files = $this->getFiles('videos', $page, $page_files, $filtered);
  1456. $page_files = $this->getFiles('audios', $page, $page_files, $filtered);
  1457. $page_files = $this->getFiles('files', $page, $page_files, $filtered);
  1458. }
  1459. }
  1460. if (count($page_files) >= self::MEDIA_PAGINATION_INTERVAL) {
  1461. $this->shouldLoadAdditionalFilesInBackground(true);
  1462. }
  1463. if (!$cache_enabled) {
  1464. $this->grav['cache']->setEnabled(true);
  1465. }
  1466. $this->grav['cache']->save(md5($files_cache_key), $page_files, 600); //cache for 10 minutes
  1467. if (!$cache_enabled) {
  1468. $this->grav['cache']->setEnabled(false);
  1469. }
  1470. }
  1471. if (count($page_files) >= self::MEDIA_PAGINATION_INTERVAL) {
  1472. $page_files = array_slice($page_files, $page_index * self::MEDIA_PAGINATION_INTERVAL, self::MEDIA_PAGINATION_INTERVAL);
  1473. }
  1474. return $page_files;
  1475. }
  1476. public function shouldLoadAdditionalFilesInBackground($status = null)
  1477. {
  1478. if ($status) {
  1479. $this->load_additional_files_in_background = true;
  1480. }
  1481. return $this->load_additional_files_in_background;
  1482. }
  1483. public function loadAdditionalFilesInBackground($status = null)
  1484. {
  1485. if (!$this->loading_additional_files_in_background) {
  1486. $this->loading_additional_files_in_background = true;
  1487. $this->files(false, false);
  1488. $this->shouldLoadAdditionalFilesInBackground(false);
  1489. $this->loading_additional_files_in_background = false;
  1490. }
  1491. }
  1492. private function getFiles($type, $page, $page_files, $filtered)
  1493. {
  1494. $page_files = $this->getMediaOfType($type, $page, $page_files);
  1495. if ($filtered) {
  1496. $page_files = $this->filterByType($page_files);
  1497. $page_files = $this->filterByDate($page_files);
  1498. }
  1499. return $page_files;
  1500. }
  1501. /**
  1502. * Get all the media of a type ('images' | 'audios' | 'videos' | 'files')
  1503. *
  1504. * @param string $type
  1505. * @param PageInterface|null $page
  1506. * @param array $files
  1507. *
  1508. * @return array
  1509. */
  1510. private function getMediaOfType($type, ?PageInterface $page, array $files)
  1511. {
  1512. if ($page) {
  1513. $media = $page->media();
  1514. $mediaOfType = $media->$type();
  1515. foreach($mediaOfType as $title => $file) {
  1516. $files[] = [
  1517. 'title' => $title,
  1518. 'type' => $type,
  1519. 'page_route' => $page->route(),
  1520. 'file' => $file->higherQualityAlternative()
  1521. ];
  1522. }
  1523. return $files;
  1524. }
  1525. return [];
  1526. }
  1527. /**
  1528. * Filter media by type
  1529. *
  1530. * @param array $filesFiltered
  1531. *
  1532. * @return array
  1533. */
  1534. private function filterByType($filesFiltered)
  1535. {
  1536. $filter_type = $this->grav['uri']->param('type');
  1537. if (!$filter_type) {
  1538. return $filesFiltered;
  1539. }
  1540. $filesFiltered = array_filter($filesFiltered, function ($file) use ($filter_type) {
  1541. return $file['type'] == $filter_type;
  1542. });
  1543. return $filesFiltered;
  1544. }
  1545. /**
  1546. * Filter media by date
  1547. *
  1548. * @param array $filesFiltered
  1549. *
  1550. * @return array
  1551. */
  1552. private function filterByDate($filesFiltered)
  1553. {
  1554. $filter_date = $this->grav['uri']->param('date');
  1555. if (!$filter_date) {
  1556. return $filesFiltered;
  1557. }
  1558. $year = substr($filter_date, 0, 4);
  1559. $month = substr($filter_date, 5, 2);
  1560. $filesFilteredByDate = [];
  1561. foreach($filesFiltered as $file) {
  1562. $filedate = $this->fileDate($file['file']);
  1563. $fileYear = $filedate->format('Y');
  1564. $fileMonth = $filedate->format('m');
  1565. if ($fileYear == $year && $fileMonth == $month) {
  1566. $filesFilteredByDate[] = $file;
  1567. }
  1568. }
  1569. return $filesFilteredByDate;
  1570. }
  1571. /**
  1572. * Return the DateTime object representation of a file modified date
  1573. *
  1574. * @param File $file
  1575. *
  1576. * @return DateTime
  1577. */
  1578. private function fileDate($file) {
  1579. $datetime = new \DateTime();
  1580. $datetime->setTimestamp($file->toArray()['modified']);
  1581. return $datetime;
  1582. }
  1583. /**
  1584. * Get the files dates list to be used in the Media Files filter
  1585. *
  1586. * @return array
  1587. */
  1588. public function filesDates()
  1589. {
  1590. $files = $this->files(false);
  1591. $dates = [];
  1592. foreach ($files as $file) {
  1593. $datetime = $this->fileDate($file['file']);
  1594. $year = $datetime->format('Y');
  1595. $month = $datetime->format('m');
  1596. if (!isset($dates[$year])) {
  1597. $dates[$year] = [];
  1598. }
  1599. if (!isset($dates[$year][$month])) {
  1600. $dates[$year][$month] = 1;
  1601. } else {
  1602. $dates[$year][$month]++;
  1603. }
  1604. }
  1605. return $dates;
  1606. }
  1607. /**
  1608. * Get the pages list to be used in the Media Files filter
  1609. *
  1610. * @return array
  1611. */
  1612. public function pages()
  1613. {
  1614. /** @var Collection $pages */
  1615. $pages = $this->grav['pages']->all();
  1616. $pagesWithFiles = [];
  1617. foreach ($pages as $page) {
  1618. if (count($page->media()->all())) {
  1619. $pagesWithFiles[] = $page;
  1620. }
  1621. }
  1622. return $pagesWithFiles;
  1623. }
  1624. /**
  1625. * Return HTTP_REFERRER if set
  1626. *
  1627. * @return null
  1628. */
  1629. public function getReferrer()
  1630. {
  1631. return $_SERVER['HTTP_REFERER'] ?? null;
  1632. }
  1633. }