Skouter mortgage estimates. Web application with view written in PHP and Vue, but controller and models in Go.
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

Admin.php 70 KiB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507
  1. <?php
  2. /**
  3. * @package Grav\Plugin\Admin
  4. *
  5. * @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Plugin\Admin;
  9. use DateTime;
  10. use Grav\Common\Data;
  11. use Grav\Common\Data\Data as GravData;
  12. use Grav\Common\Debugger;
  13. use Grav\Common\File\CompiledYamlFile;
  14. use Grav\Common\Flex\Types\Users\UserObject;
  15. use Grav\Common\GPM\GPM;
  16. use Grav\Common\GPM\Licenses;
  17. use Grav\Common\Grav;
  18. use Grav\Common\Helpers\YamlLinter;
  19. use Grav\Common\HTTP\Response;
  20. use Grav\Common\Language\Language;
  21. use Grav\Common\Language\LanguageCodes;
  22. use Grav\Common\Page\Collection;
  23. use Grav\Common\Page\Interfaces\PageInterface;
  24. use Grav\Common\Page\Page;
  25. use Grav\Common\Page\Pages;
  26. use Grav\Common\Plugins;
  27. use Grav\Common\Security;
  28. use Grav\Common\Session;
  29. use Grav\Common\Themes;
  30. use Grav\Common\Uri;
  31. use Grav\Common\User\Interfaces\UserCollectionInterface;
  32. use Grav\Common\User\Interfaces\UserInterface;
  33. use Grav\Common\Utils;
  34. use Grav\Framework\Acl\Action;
  35. use Grav\Framework\Acl\Permissions;
  36. use Grav\Framework\Collection\ArrayCollection;
  37. use Grav\Framework\Flex\Flex;
  38. use Grav\Framework\Flex\Interfaces\FlexInterface;
  39. use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
  40. use Grav\Framework\Route\Route;
  41. use Grav\Framework\Route\RouteFactory;
  42. use Grav\Plugin\AdminPlugin;
  43. use Grav\Plugin\Login\Login;
  44. use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
  45. use JsonException;
  46. use PicoFeed\Parser\MalformedXmlException;
  47. use Psr\Http\Message\ServerRequestInterface;
  48. use RocketTheme\Toolbox\Event\Event;
  49. use RocketTheme\Toolbox\File\File;
  50. use RocketTheme\Toolbox\File\JsonFile;
  51. use RocketTheme\Toolbox\ResourceLocator\UniformResourceIterator;
  52. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  53. use RocketTheme\Toolbox\Session\Message;
  54. use Grav\Common\Yaml;
  55. use Composer\Semver\Semver;
  56. use PicoFeed\Reader\Reader;
  57. define('LOGIN_REDIRECT_COOKIE', 'grav-login-redirect');
  58. /**
  59. * Class Admin
  60. * @package Grav\Plugin\Admin
  61. */
  62. class Admin
  63. {
  64. /** @var int */
  65. public const DEBUG = 1;
  66. /** @var int */
  67. public const MEDIA_PAGINATION_INTERVAL = 20;
  68. /** @var string */
  69. public const TMP_COOKIE_NAME = 'tmp-admin-message';
  70. /** @var Grav */
  71. public $grav;
  72. /** @var ServerRequestInterface|null */
  73. public $request;
  74. /** @var AdminForm */
  75. public $form;
  76. /** @var string */
  77. public $base;
  78. /** @var string */
  79. public $location;
  80. /** @var string */
  81. public $route;
  82. /** @var UserInterface */
  83. public $user;
  84. /** @var array */
  85. public $forgot;
  86. /** @var string */
  87. public $task;
  88. /** @var array */
  89. public $json_response;
  90. /** @var Collection */
  91. public $collection;
  92. /** @var bool */
  93. public $multilang;
  94. /** @var string */
  95. public $language;
  96. /** @var array */
  97. public $languages_enabled = [];
  98. /** @var Uri $uri */
  99. /** @var array */
  100. public $routes = [];
  101. protected $uri;
  102. /** @var array */
  103. protected $pages = [];
  104. /** @var Session */
  105. protected $session;
  106. /** @var Data\Blueprints */
  107. protected $blueprints;
  108. /** @var GPM */
  109. protected $gpm;
  110. /** @var int */
  111. protected $pages_count;
  112. /** @var bool */
  113. protected $load_additional_files_in_background = false;
  114. /** @var bool */
  115. protected $loading_additional_files_in_background = false;
  116. /** @var array */
  117. protected $temp_messages = [];
  118. /**
  119. * Constructor.
  120. *
  121. * @param Grav $grav
  122. * @param string $base
  123. * @param string $location
  124. * @param string|null $route
  125. */
  126. public function __construct(Grav $grav, $base, $location, $route)
  127. {
  128. // Register admin to grav because of calling $grav['user'] requires it.
  129. $grav['admin'] = $this;
  130. $this->grav = $grav;
  131. $this->base = $base;
  132. $this->location = $location;
  133. $this->route = $route ?? '';
  134. $this->uri = $grav['uri'];
  135. $this->session = $grav['session'];
  136. /** @var FlexInterface|null $flex */
  137. $flex = $grav['flex_objects'] ?? null;
  138. /** @var UserInterface $user */
  139. $user = $grav['user'];
  140. // Convert old user to Flex User if Flex Objects plugin has been enabled.
  141. if ($flex && !$user instanceof FlexObjectInterface) {
  142. $managed = !method_exists($flex, 'isManaged') || $flex->isManaged('user-accounts');
  143. $directory = $managed ? $flex->getDirectory('user-accounts') : null;
  144. /** @var UserObject|null $test */
  145. $test = $directory ? $directory->getObject(mb_strtolower($user->username)) : null;
  146. if ($test) {
  147. $test = clone $test;
  148. $test->access = $user->access;
  149. $test->groups = $user->groups;
  150. $test->authenticated = $user->authenticated;
  151. $test->authorized = $user->authorized;
  152. $user = $test;
  153. }
  154. }
  155. $this->user = $user;
  156. /** @var Language $language */
  157. $language = $grav['language'];
  158. $this->multilang = $language->enabled();
  159. // Load utility class
  160. if ($this->multilang) {
  161. $this->language = $language->getActive() ?? '';
  162. $this->languages_enabled = (array)$this->grav['config']->get('system.languages.supported', []);
  163. //Set the currently active language for the admin
  164. $languageCode = $this->uri->param('lang');
  165. if (null === $languageCode && !$this->session->admin_lang) {
  166. $this->session->admin_lang = $language->getActive() ?? '';
  167. }
  168. } else {
  169. $this->language = '';
  170. }
  171. // Set admin route language.
  172. RouteFactory::setLanguage($this->language);
  173. }
  174. /**
  175. * @param string $message
  176. * @param array|object $data
  177. * @return void
  178. */
  179. public static function addDebugMessage(string $message, $data = [])
  180. {
  181. /** @var Debugger $debugger */
  182. $debugger = Grav::instance()['debugger'];
  183. $debugger->addMessage($message, 'debug', $data);
  184. }
  185. /**
  186. * @return string[]
  187. */
  188. public static function contentEditor()
  189. {
  190. $options = [
  191. 'default' => 'Default',
  192. 'codemirror' => 'CodeMirror'
  193. ];
  194. $event = new Event(['options' => &$options]);
  195. Grav::instance()->fireEvent('onAdminListContentEditors', $event);
  196. return $options;
  197. }
  198. /**
  199. * Return the languages available in the admin
  200. *
  201. * @return array
  202. */
  203. public static function adminLanguages()
  204. {
  205. $languages = [];
  206. $path = Grav::instance()['locator']->findResource('plugins://admin/languages');
  207. foreach (new \DirectoryIterator($path) as $file) {
  208. if ($file->isDir() || $file->isDot() || Utils::startsWith($file->getFilename(), '.')) {
  209. continue;
  210. }
  211. $lang = $file->getBasename('.yaml');
  212. $languages[$lang] = LanguageCodes::getNativeName($lang);
  213. }
  214. // sort languages
  215. asort($languages);
  216. return $languages;
  217. }
  218. /**
  219. * @return string
  220. */
  221. public function getLanguage(): string
  222. {
  223. return $this->language ?: $this->grav['language']->getLanguage() ?: 'en';
  224. }
  225. /**
  226. * Return the found configuration blueprints
  227. *
  228. * @param bool $checkAccess
  229. * @return array
  230. */
  231. public static function configurations(bool $checkAccess = false): array
  232. {
  233. $grav = Grav::instance();
  234. /** @var Admin $admin */
  235. $admin = $grav['admin'];
  236. /** @var UniformResourceIterator $iterator */
  237. $iterator = $grav['locator']->getIterator('blueprints://config');
  238. // Find all main level configuration files.
  239. $configurations = [];
  240. foreach ($iterator as $file) {
  241. if ($file->isDir() || !preg_match('/^[^.].*.yaml$/', $file->getFilename())) {
  242. continue;
  243. }
  244. $name = $file->getBasename('.yaml');
  245. // Check that blueprint exists and is not hidden.
  246. $data = $admin->getConfigurationData('config/'. $name);
  247. if (!is_callable([$data, 'blueprints'])) {
  248. continue;
  249. }
  250. $blueprint = $data->blueprints();
  251. if (!$blueprint) {
  252. continue;
  253. }
  254. $test = $blueprint->toArray();
  255. if (empty($test['form']['hidden']) && (!empty($test['form']['field']) || !empty($test['form']['fields']))) {
  256. $configurations[$name] = true;
  257. }
  258. }
  259. // Remove scheduler and backups configs (they belong to the tools).
  260. unset($configurations['scheduler'], $configurations['backups']);
  261. // Sort configurations.
  262. ksort($configurations);
  263. $configurations = ['system' => true, 'site' => true] + $configurations + ['info' => true];
  264. if ($checkAccess) {
  265. // ACL checks.
  266. foreach ($configurations as $name => $value) {
  267. if (!$admin->authorize(['admin.configuration.' . $name, 'admin.super'])) {
  268. unset($configurations[$name]);
  269. }
  270. }
  271. }
  272. return array_keys($configurations);
  273. }
  274. /**
  275. * Return the tools found
  276. *
  277. * @return array
  278. */
  279. public static function tools()
  280. {
  281. $tools = [];
  282. Grav::instance()->fireEvent('onAdminTools', new Event(['tools' => &$tools]));
  283. return $tools;
  284. }
  285. /**
  286. * @return array
  287. */
  288. public static function toolsPermissions()
  289. {
  290. $tools = static::tools();
  291. $perms = [];
  292. foreach ($tools as $tool) {
  293. $perms = array_merge($perms, $tool[0]);
  294. }
  295. return array_unique($perms);
  296. }
  297. /**
  298. * Return the languages available in the site
  299. *
  300. * @return array
  301. */
  302. public static function siteLanguages()
  303. {
  304. $languages = [];
  305. $lang_data = (array) Grav::instance()['config']->get('system.languages.supported', []);
  306. foreach ($lang_data as $index => $lang) {
  307. $languages[$lang] = LanguageCodes::getNativeName($lang);
  308. }
  309. return $languages;
  310. }
  311. /**
  312. * Static helper method to return the admin form nonce
  313. *
  314. * @param string $action
  315. * @return string
  316. */
  317. public static function getNonce(string $action = 'admin-form')
  318. {
  319. return Utils::getNonce($action);
  320. }
  321. /**
  322. * Static helper method to return the last used page name
  323. *
  324. * @return string
  325. */
  326. public static function getLastPageName()
  327. {
  328. return Grav::instance()['session']->lastPageName ?: 'default';
  329. }
  330. /**
  331. * Static helper method to return the last used page route
  332. *
  333. * @return string
  334. */
  335. public static function getLastPageRoute()
  336. {
  337. /** @var Session $session */
  338. $session = Grav::instance()['session'];
  339. $route = $session->lastPageRoute;
  340. if ($route) {
  341. return $route;
  342. }
  343. /** @var Admin $admin */
  344. $admin = Grav::instance()['admin'];
  345. return $admin->getCurrentRoute();
  346. }
  347. /**
  348. * @param string $path
  349. * @param string|null $languageCode
  350. * @return Route
  351. */
  352. public function getAdminRoute(string $path = '', $languageCode = null): Route
  353. {
  354. /** @var Language $language */
  355. $language = $this->grav['language'];
  356. $languageCode = $languageCode ?? ($language->getActive() ?: null);
  357. $languagePrefix = $languageCode ? '/' . $languageCode : '';
  358. $root = $this->grav['uri']->rootUrl();
  359. $subRoute = rtrim($this->grav['pages']->base(), '/');
  360. $adminRoute = rtrim($this->grav['config']->get('plugins.admin.route'), '/');
  361. $parts = [
  362. 'path' => $path,
  363. 'query' => '',
  364. 'query_params' => [],
  365. 'grav' => [
  366. // TODO: Make URL to be /admin/en, not /en/admin.
  367. 'root' => preg_replace('`//+`', '/', $root . $subRoute . $languagePrefix . $adminRoute),
  368. 'language' => '', //$languageCode,
  369. 'route' => ltrim($path, '/'),
  370. 'params' => ''
  371. ],
  372. ];
  373. return RouteFactory::createFromParts($parts);
  374. }
  375. /**
  376. * @param string $route
  377. * @param string|null $languageCode
  378. * @return string
  379. */
  380. public function adminUrl(string $route = '', $languageCode = null)
  381. {
  382. return $this->getAdminRoute($route, $languageCode)->toString(true);
  383. }
  384. /**
  385. * Static helper method to return current route.
  386. *
  387. * @return string
  388. * @deprecated 1.10 Use $admin->getCurrentRoute() instead
  389. */
  390. public static function route()
  391. {
  392. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Admin 1.9.7, use $admin->getCurrentRoute() instead', E_USER_DEPRECATED);
  393. $admin = Grav::instance()['admin'];
  394. return $admin->getCurrentRoute();
  395. }
  396. /**
  397. * @return string|null
  398. */
  399. public function getCurrentRoute()
  400. {
  401. $pages = static::enablePages();
  402. $route = '/' . ltrim($this->route, '/');
  403. /** @var PageInterface $page */
  404. $page = $pages->find($route);
  405. $parent_route = null;
  406. if ($page) {
  407. /** @var PageInterface $parent */
  408. $parent = $page->parent();
  409. $parent_route = $parent->rawRoute();
  410. }
  411. return $parent_route;
  412. }
  413. /**
  414. * Redirect to the route stored in $this->redirect
  415. *
  416. * Route may or may not be prefixed by /en or /admin or /en/admin.
  417. *
  418. * @param string $redirect
  419. * @param int $redirectCode
  420. * @return void
  421. */
  422. public function redirect($redirect, $redirectCode = 303)
  423. {
  424. // No redirect, do nothing.
  425. if (!$redirect) {
  426. return;
  427. }
  428. Admin::DEBUG && Admin::addDebugMessage("Admin redirect: {$redirectCode} {$redirect}");
  429. $redirect = '/' . ltrim(preg_replace('`//+`', '/', $redirect), '/');
  430. $base = $this->base;
  431. $root = Grav::instance()['uri']->rootUrl();
  432. if ($root === '/') {
  433. $root = '';
  434. }
  435. $pattern = '`^((' . preg_quote($root, '`') . ')?(/[^/]+)?)' . preg_quote($base, '`') . '`ui';
  436. // Check if we already have an admin path: /admin, /en/admin, /root/admin or /root/en/admin.
  437. if (preg_match($pattern, $redirect)) {
  438. $redirect = preg_replace('|^' . preg_quote($root, '|') . '|', '', $redirect);
  439. $this->grav->redirect($redirect, $redirectCode);
  440. }
  441. if ($this->isMultilang()) {
  442. // Check if URL does not have language prefix.
  443. if (!Utils::pathPrefixedByLangCode($redirect)) {
  444. /** @var Language $language */
  445. $language = $this->grav['language'];
  446. // Prefix path with language prefix: /en
  447. // TODO: Use /admin/en instead of /en/admin in the future.
  448. $redirect = $language->getLanguageURLPrefix($this->grav['session']->admin_lang) . $base . $redirect;
  449. } else {
  450. // TODO: Use /admin/en instead of /en/admin in the future.
  451. //$redirect = preg_replace('`^(/[^/]+)/admin`', '\\1', $redirect);
  452. // Check if we already have language prefixed admin path: /en/admin
  453. $this->grav->redirect($redirect, $redirectCode);
  454. }
  455. } else {
  456. // TODO: Use /admin/en instead of /en/admin in the future.
  457. // Prefix path with /admin
  458. $redirect = $base . $redirect;
  459. }
  460. $this->grav->redirect($redirect, $redirectCode);
  461. }
  462. /**
  463. * Return true if multilang is active
  464. *
  465. * @return bool True if multilang is active
  466. */
  467. protected function isMultilang()
  468. {
  469. return count($this->grav['config']->get('system.languages.supported', [])) > 1;
  470. }
  471. /**
  472. * @return string
  473. */
  474. public static function getTempDir()
  475. {
  476. try {
  477. $tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
  478. } catch (\Exception $e) {
  479. $tmp_dir = Grav::instance()['locator']->findResource('cache://', true, true) . '/tmp';
  480. }
  481. return $tmp_dir;
  482. }
  483. /**
  484. * @return array
  485. */
  486. public static function getPageMedia()
  487. {
  488. $files = [];
  489. $grav = Grav::instance();
  490. $pages = static::enablePages();
  491. $route = '/' . ltrim($grav['admin']->route, '/');
  492. /** @var PageInterface $page */
  493. $page = $pages->find($route);
  494. $parent_route = null;
  495. if ($page) {
  496. $media = $page->media()->all();
  497. $files = array_keys($media);
  498. }
  499. return $files;
  500. }
  501. /**
  502. * Get current session.
  503. *
  504. * @return Session
  505. */
  506. public function session()
  507. {
  508. return $this->session;
  509. }
  510. /**
  511. * Fetch and delete messages from the session queue.
  512. *
  513. * @param string|null $type
  514. * @return array
  515. */
  516. public function messages($type = null)
  517. {
  518. /** @var Message $messages */
  519. $messages = $this->grav['messages'];
  520. return $messages->fetch($type);
  521. }
  522. /**
  523. * Authenticate user.
  524. *
  525. * @param array $credentials User credentials.
  526. * @param array $post
  527. * @return never-return
  528. */
  529. public function authenticate($credentials, $post)
  530. {
  531. /** @var Login $login */
  532. $login = $this->grav['login'];
  533. // Remove login nonce from the form.
  534. $credentials = array_diff_key($credentials, ['admin-nonce' => true]);
  535. $twofa = $this->grav['config']->get('plugins.admin.twofa_enabled', false);
  536. $rateLimiter = $login->getRateLimiter('login_attempts');
  537. $userKey = (string)($credentials['username'] ?? '');
  538. $ipKey = Uri::ip();
  539. $redirect = $post['redirect'] ?? $this->base . $this->route;
  540. // Pseudonymization of the IP
  541. $ipKey = sha1($ipKey . $this->grav['config']->get('security.salt'));
  542. // Check if the current IP has been used in failed login attempts.
  543. $attempts = count($rateLimiter->getAttempts($ipKey, 'ip'));
  544. $rateLimiter->registerRateLimitedAction($ipKey, 'ip')->registerRateLimitedAction($userKey);
  545. // Check rate limit for both IP and user, but allow each IP a single try even if user is already rate limited.
  546. if ($rateLimiter->isRateLimited($ipKey, 'ip') || ($attempts && $rateLimiter->isRateLimited($userKey))) {
  547. Admin::DEBUG && Admin::addDebugMessage('Admin login: rate limit, redirecting', $credentials);
  548. $this->setMessage(static::translate(['PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $rateLimiter->getInterval()]), 'error');
  549. $this->grav->redirect('/');
  550. }
  551. Admin::DEBUG && Admin::addDebugMessage('Admin login', $credentials);
  552. // Fire Login process.
  553. $event = $login->login(
  554. $credentials,
  555. ['admin' => true, 'twofa' => $twofa],
  556. ['authorize' => 'admin.login', 'return_event' => true]
  557. );
  558. $user = $event->getUser();
  559. Admin::DEBUG && Admin::addDebugMessage('Admin login: user', $user);
  560. if ($user->authenticated) {
  561. $rateLimiter->resetRateLimit($ipKey, 'ip')->resetRateLimit($userKey);
  562. if ($user->authorized) {
  563. $event->defMessage('PLUGIN_ADMIN.LOGIN_LOGGED_IN', 'info');
  564. $event->defRedirect($post['redirect'] ?? $redirect);
  565. } else {
  566. $this->session->redirect = $redirect;
  567. }
  568. } else {
  569. if ($user->authorized) {
  570. $event->defMessage('PLUGIN_LOGIN.ACCESS_DENIED', 'error');
  571. } else {
  572. $event->defMessage('PLUGIN_LOGIN.LOGIN_FAILED', 'error');
  573. }
  574. }
  575. $event->defRedirect($redirect);
  576. $message = $event->getMessage();
  577. if ($message) {
  578. $this->setMessage(static::translate($message), $event->getMessageType());
  579. }
  580. /** @var Pages $pages */
  581. $pages = $this->grav['pages'];
  582. $redirect = $pages->baseRoute() . $event->getRedirect();
  583. $this->grav->redirect($redirect, $event->getRedirectCode());
  584. }
  585. /**
  586. * Check Two-Factor Authentication.
  587. *
  588. * @param array $data
  589. * @param array $post
  590. * @return never-return
  591. */
  592. public function twoFa($data, $post)
  593. {
  594. /** @var Pages $pages */
  595. $pages = $this->grav['pages'];
  596. $baseRoute = $pages->baseRoute();
  597. /** @var Login $login */
  598. $login = $this->grav['login'];
  599. /** @var TwoFactorAuth $twoFa */
  600. $twoFa = $login->twoFactorAuth();
  601. $user = $this->grav['user'];
  602. $code = $data['2fa_code'] ?? null;
  603. $secret = $user->twofa_secret ?? null;
  604. if (!$code || !$secret || !$twoFa->verifyCode($secret, $code)) {
  605. $login->logout(['admin' => true]);
  606. $this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate('PLUGIN_ADMIN.2FA_FAILED'), 'status' => 'error']);
  607. $this->grav->redirect($baseRoute . $this->uri->route(), 303);
  608. }
  609. $this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'), 'info');
  610. $user->authorized = true;
  611. $redirect = $baseRoute . $post['redirect'];
  612. $this->grav->redirect($redirect);
  613. }
  614. /**
  615. * Logout from admin.
  616. *
  617. * @param array $data
  618. * @param array $post
  619. * @return never-return
  620. */
  621. public function logout($data, $post)
  622. {
  623. /** @var Login $login */
  624. $login = $this->grav['login'];
  625. $event = $login->logout(['admin' => true], ['return_event' => true]);
  626. $event->defMessage('PLUGIN_ADMIN.LOGGED_OUT', 'info');
  627. $message = $event->getMessage();
  628. if ($message) {
  629. $this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate($message), 'status' => $event->getMessageType()]);
  630. }
  631. $this->grav->redirect($this->base);
  632. }
  633. /**
  634. * @return bool
  635. */
  636. public static function doAnyUsersExist()
  637. {
  638. $accounts = Grav::instance()['accounts'] ?? null;
  639. return $accounts && $accounts->count() > 0;
  640. }
  641. /**
  642. * Add message into the session queue.
  643. *
  644. * @param string $msg
  645. * @param string $type
  646. * @return void
  647. */
  648. public function setMessage($msg, $type = 'info')
  649. {
  650. /** @var Message $messages */
  651. $messages = $this->grav['messages'];
  652. $messages->add($msg, $type);
  653. }
  654. /**
  655. * @param string $msg
  656. * @param string $type
  657. * @return void
  658. */
  659. public function addTempMessage($msg, $type)
  660. {
  661. $this->temp_messages[] = ['message' => $msg, 'scope' => $type];
  662. }
  663. /**
  664. * @return array
  665. */
  666. public function getTempMessages()
  667. {
  668. return $this->temp_messages;
  669. }
  670. /**
  671. * Translate a string to the user-defined language
  672. *
  673. * @param array|string $args
  674. * @param array|null $languages
  675. * @return string|string[]|null
  676. */
  677. public static function translate($args, $languages = null)
  678. {
  679. $grav = Grav::instance();
  680. if (is_array($args)) {
  681. $lookup = array_shift($args);
  682. } else {
  683. $lookup = $args;
  684. $args = [];
  685. }
  686. if (!$languages) {
  687. if ($grav['config']->get('system.languages.translations_fallback', true)) {
  688. $languages = $grav['language']->getFallbackLanguages();
  689. } else {
  690. $languages = (array)$grav['language']->getDefault();
  691. }
  692. $languages = $grav['user']->authenticated ? [$grav['user']->language] : $languages;
  693. } else {
  694. $languages = (array)$languages;
  695. }
  696. foreach ((array)$languages as $lang) {
  697. $translation = $grav['language']->getTranslation($lang, $lookup, true);
  698. if (!$translation) {
  699. $language = $grav['language']->getDefault() ?: 'en';
  700. $translation = $grav['language']->getTranslation($language, $lookup, true);
  701. }
  702. if (!$translation) {
  703. $language = 'en';
  704. $translation = $grav['language']->getTranslation($language, $lookup, true);
  705. }
  706. if ($translation) {
  707. if (count($args) >= 1) {
  708. return vsprintf($translation, $args);
  709. }
  710. return $translation;
  711. }
  712. }
  713. return $lookup;
  714. }
  715. /**
  716. * Checks user authorisation to the action.
  717. *
  718. * @param string|string[] $action
  719. * @return bool
  720. */
  721. public function authorize($action = 'admin.login')
  722. {
  723. $action = (array)$action;
  724. $user = $this->user;
  725. foreach ($action as $a) {
  726. // Ignore 'admin.super' if it's not the only value to be checked.
  727. if ($a === 'admin.super' && count($action) > 1 && $user instanceof FlexObjectInterface) {
  728. continue;
  729. }
  730. if ($user->authorize($a)) {
  731. return true;
  732. }
  733. }
  734. return false;
  735. }
  736. /**
  737. * Gets configuration data.
  738. *
  739. * @param string $type
  740. * @param array $post
  741. * @return object
  742. * @throws \RuntimeException
  743. */
  744. public function data($type, array $post = [])
  745. {
  746. if (!$post) {
  747. $post = $this->preparePost($this->grav['uri']->post()['data'] ?? []);
  748. }
  749. try {
  750. return $this->getConfigurationData($type, $post);
  751. } catch (\RuntimeException $e) {
  752. return new Data\Data();
  753. }
  754. }
  755. /**
  756. * Get configuration data.
  757. *
  758. * Note: If you pass $post, make sure you pass all the fields in the blueprint or data gets lost!
  759. *
  760. * @param string $type
  761. * @param array|null $post
  762. * @return object
  763. * @throws \RuntimeException
  764. */
  765. public function getConfigurationData($type, array $post = null)
  766. {
  767. static $data = [];
  768. if (isset($data[$type])) {
  769. $obj = $data[$type];
  770. if ($post) {
  771. if ($obj instanceof Data\Data) {
  772. $obj = $this->mergePost($obj, $post);
  773. } elseif ($obj instanceof UserInterface) {
  774. $obj->update($this->cleanUserPost($post));
  775. }
  776. }
  777. return $obj;
  778. }
  779. // Check to see if a data type is plugin-provided, before looking into core ones
  780. $event = $this->grav->fireEvent('onAdminData', new Event(['type' => &$type]));
  781. if ($event) {
  782. if (isset($event['data_type'])) {
  783. return $event['data_type'];
  784. }
  785. if (is_string($event['type'])) {
  786. $type = $event['type'];
  787. }
  788. }
  789. /** @var UniformResourceLocator $locator */
  790. $locator = $this->grav['locator'];
  791. // Configuration file will be saved to the existing config stream.
  792. $filename = $locator->findResource('config://') . "/{$type}.yaml";
  793. $file = CompiledYamlFile::instance($filename);
  794. if (preg_match('|plugins/|', $type)) {
  795. $obj = Plugins::get(preg_replace('|plugins/|', '', $type));
  796. if (null === $obj) {
  797. throw new \RuntimeException("Plugin '{$type}' doesn't exist!");
  798. }
  799. $obj->file($file);
  800. } elseif (preg_match('|themes/|', $type)) {
  801. /** @var Themes $themes */
  802. $themes = $this->grav['themes'];
  803. $obj = $themes->get(preg_replace('|themes/|', '', $type));
  804. if (null === $obj) {
  805. throw new \RuntimeException("Theme '{$type}' doesn't exist!");
  806. }
  807. $obj->file($file);
  808. } elseif (preg_match('|users?/|', $type)) {
  809. /** @var UserCollectionInterface $users */
  810. $users = $this->grav['accounts'];
  811. $obj = $users->load(preg_replace('|users?/|', '', $type));
  812. } elseif (preg_match('|config/|', $type)) {
  813. $type = preg_replace('|config/|', '', $type);
  814. $blueprints = $this->blueprints("config/{$type}");
  815. if (!$blueprints->form()) {
  816. throw new \RuntimeException("Configuration type '{$type}' doesn't exist!");
  817. }
  818. // Configuration file will be saved to the existing config stream.
  819. $filename = $locator->findResource('config://') . "/{$type}.yaml";
  820. $file = CompiledYamlFile::instance($filename);
  821. $config = $this->grav['config'];
  822. $obj = new Data\Data($config->get($type, []), $blueprints);
  823. $obj->file($file);
  824. } elseif (preg_match('|media-manager/|', $type)) {
  825. $filename = base64_decode(preg_replace('|media-manager/|', '', $type));
  826. $file = File::instance($filename);
  827. $pages = static::enablePages();
  828. $obj = new \stdClass();
  829. $obj->title = $file->basename();
  830. $obj->path = $file->filename();
  831. $obj->file = $file;
  832. $obj->page = $pages->get(dirname($obj->path));
  833. $fileInfo = Utils::pathinfo($obj->title);
  834. $filename = str_replace(['@3x', '@2x'], '', $fileInfo['filename']);
  835. if (isset($fileInfo['extension'])) {
  836. $filename .= '.' . $fileInfo['extension'];
  837. }
  838. if ($obj->page && isset($obj->page->media()[$filename])) {
  839. $obj->metadata = new Data\Data($obj->page->media()[$filename]->metadata());
  840. }
  841. } else {
  842. throw new \RuntimeException("Data type '{$type}' doesn't exist!");
  843. }
  844. $data[$type] = $obj;
  845. if ($post) {
  846. if ($obj instanceof Data\Data) {
  847. $obj = $this->mergePost($obj, $post);
  848. } elseif ($obj instanceof UserInterface) {
  849. $obj->update($this->cleanUserPost($post));
  850. }
  851. }
  852. return $obj;
  853. }
  854. /**
  855. * @param Data\Data $object
  856. * @param array $post
  857. * @return Data\Data
  858. */
  859. protected function mergePost(Data\Data $object, array $post)
  860. {
  861. $object->merge($post);
  862. $blueprint = $object->blueprints();
  863. $data = $blueprint->flattenData($post, true);
  864. foreach ($data as $key => $val) {
  865. if ($val === null) {
  866. $object->set($key, $val);
  867. }
  868. }
  869. return $object;
  870. }
  871. /**
  872. * Clean user form post and remove extra stuff that may be passed along
  873. *
  874. * @param array $post
  875. * @return array
  876. */
  877. public function cleanUserPost($post)
  878. {
  879. // Clean fields for all users
  880. unset($post['hashed_password']);
  881. // Clean field for users who shouldn't be able to modify these fields
  882. if (!$this->authorize(['admin.user', 'admin.super'])) {
  883. unset($post['access'], $post['state']);
  884. }
  885. return $post;
  886. }
  887. /**
  888. * @return bool
  889. */
  890. protected function hasErrorMessage()
  891. {
  892. $msgs = $this->grav['messages']->all();
  893. foreach ($msgs as $msg) {
  894. if (isset($msg['scope']) && $msg['scope'] === 'error') {
  895. return true;
  896. }
  897. }
  898. return false;
  899. }
  900. /**
  901. * Returns blueprints for the given type.
  902. *
  903. * @param string $type
  904. * @return Data\Blueprint
  905. */
  906. public function blueprints($type)
  907. {
  908. if ($this->blueprints === null) {
  909. $this->blueprints = new Data\Blueprints('blueprints://');
  910. }
  911. return $this->blueprints->get($type);
  912. }
  913. /**
  914. * Converts dot notation to array notation.
  915. *
  916. * @param string $name
  917. * @return string
  918. */
  919. public function field($name)
  920. {
  921. $path = explode('.', $name);
  922. return array_shift($path) . ($path ? '[' . implode('][', $path) . ']' : '');
  923. }
  924. /**
  925. * Get all routes.
  926. *
  927. * @param bool $unique
  928. * @return array
  929. */
  930. public function routes($unique = false)
  931. {
  932. $pages = static::enablePages();
  933. if ($unique) {
  934. $routes = array_unique($pages->routes());
  935. } else {
  936. $routes = $pages->routes();
  937. }
  938. return $routes;
  939. }
  940. /**
  941. * Count the pages
  942. *
  943. * @return int
  944. */
  945. public function pagesCount()
  946. {
  947. if (!$this->pages_count) {
  948. $pages = static::enablePages();
  949. $this->pages_count = count($pages->all());
  950. }
  951. return $this->pages_count;
  952. }
  953. /**
  954. * Get all template types
  955. *
  956. * @param array|null $ignore
  957. * @return array
  958. */
  959. public function types(?array $ignore = [])
  960. {
  961. if (null === $ignore) {
  962. return AdminPlugin::pagesTypes();
  963. }
  964. $types = Pages::types();
  965. return $ignore ? array_diff_key($types, array_flip($ignore)) : $types;
  966. }
  967. /**
  968. * Get all modular template types
  969. *
  970. * @param array|null $ignore
  971. * @return array
  972. */
  973. public function modularTypes(?array $ignore = [])
  974. {
  975. if (null === $ignore) {
  976. return AdminPlugin::pagesModularTypes();
  977. }
  978. $types = Pages::modularTypes();
  979. return $ignore ? array_diff_key($types, array_flip($ignore)) : $types;
  980. }
  981. /**
  982. * Get all access levels
  983. *
  984. * @return array
  985. */
  986. public function accessLevels()
  987. {
  988. $pages = static::enablePages();
  989. if (method_exists($pages, 'accessLevels')) {
  990. return $pages->accessLevels();
  991. }
  992. return [];
  993. }
  994. /**
  995. * @param string|null $package_slug
  996. * @return string[]|string
  997. */
  998. public function license($package_slug)
  999. {
  1000. return Licenses::get($package_slug);
  1001. }
  1002. /**
  1003. * Generate an array of dependencies for a package, used to generate a list of
  1004. * packages that can be removed when removing a package.
  1005. *
  1006. * @param string $slug The package slug
  1007. * @return array|bool
  1008. */
  1009. public function dependenciesThatCanBeRemovedWhenRemoving($slug)
  1010. {
  1011. $gpm = $this->gpm();
  1012. if (!$gpm) {
  1013. return false;
  1014. }
  1015. $dependencies = [];
  1016. $package = $this->getPackageFromGPM($slug);
  1017. if ($package && $package->dependencies) {
  1018. foreach ($package->dependencies as $dependency) {
  1019. // if (count($gpm->getPackagesThatDependOnPackage($dependency)) > 1) {
  1020. // continue;
  1021. // }
  1022. if (isset($dependency['name'])) {
  1023. $dependency = $dependency['name'];
  1024. }
  1025. if (!in_array($dependency, $dependencies, true) && !in_array($dependency, ['admin', 'form', 'login', 'email', 'php'])) {
  1026. $dependencies[] = $dependency;
  1027. }
  1028. }
  1029. }
  1030. return $dependencies;
  1031. }
  1032. /**
  1033. * Get the GPM instance
  1034. *
  1035. * @return GPM The GPM instance
  1036. */
  1037. public function gpm()
  1038. {
  1039. if (!$this->gpm) {
  1040. try {
  1041. $this->gpm = new GPM();
  1042. } catch (\Exception $e) {
  1043. $this->setMessage($e->getMessage(), 'error');
  1044. }
  1045. }
  1046. return $this->gpm;
  1047. }
  1048. /**
  1049. * @param string $package_slug
  1050. * @return mixed
  1051. */
  1052. public function getPackageFromGPM($package_slug)
  1053. {
  1054. $package = $this->plugins(true)[$package_slug];
  1055. if (!$package) {
  1056. $package = $this->themes(true)[$package_slug];
  1057. }
  1058. return $package;
  1059. }
  1060. /**
  1061. * Get all plugins.
  1062. *
  1063. * @param bool $local
  1064. * @return mixed
  1065. */
  1066. public function plugins($local = true)
  1067. {
  1068. $gpm = $this->gpm();
  1069. if (!$gpm) {
  1070. return false;
  1071. }
  1072. if ($local) {
  1073. return $gpm->getInstalledPlugins();
  1074. }
  1075. $plugins = $gpm->getRepositoryPlugins();
  1076. if ($plugins) {
  1077. return $plugins->filter(function ($package, $slug) use ($gpm) {
  1078. return !$gpm->isPluginInstalled($slug);
  1079. });
  1080. }
  1081. return [];
  1082. }
  1083. /**
  1084. * Get all themes.
  1085. *
  1086. * @param bool $local
  1087. * @return mixed
  1088. */
  1089. public function themes($local = true)
  1090. {
  1091. $gpm = $this->gpm();
  1092. if (!$gpm) {
  1093. return false;
  1094. }
  1095. if ($local) {
  1096. return $gpm->getInstalledThemes();
  1097. }
  1098. $themes = $gpm->getRepositoryThemes();
  1099. if ($themes) {
  1100. return $themes->filter(function ($package, $slug) use ($gpm) {
  1101. return !$gpm->isThemeInstalled($slug);
  1102. });
  1103. }
  1104. return [];
  1105. }
  1106. /**
  1107. * Get list of packages that depend on the passed package slug
  1108. *
  1109. * @param string $slug The package slug
  1110. *
  1111. * @return array|bool
  1112. */
  1113. public function getPackagesThatDependOnPackage($slug)
  1114. {
  1115. $gpm = $this->gpm();
  1116. if (!$gpm) {
  1117. return false;
  1118. }
  1119. return $gpm->getPackagesThatDependOnPackage($slug);
  1120. }
  1121. /**
  1122. * Check the passed packages list can be updated
  1123. *
  1124. * @param array $packages
  1125. * @return bool
  1126. * @throws \Exception
  1127. */
  1128. public function checkPackagesCanBeInstalled($packages)
  1129. {
  1130. $gpm = $this->gpm();
  1131. if (!$gpm) {
  1132. return false;
  1133. }
  1134. $this->gpm->checkPackagesCanBeInstalled($packages);
  1135. return true;
  1136. }
  1137. /**
  1138. * Get an array of dependencies needed to be installed or updated for a list of packages
  1139. * to be installed.
  1140. *
  1141. * @param array $packages The packages slugs
  1142. * @return array|bool
  1143. */
  1144. public function getDependenciesNeededToInstall($packages)
  1145. {
  1146. $gpm = $this->gpm();
  1147. if (!$gpm) {
  1148. return false;
  1149. }
  1150. return $this->gpm->getDependencies($packages);
  1151. }
  1152. /**
  1153. * Used by the Dashboard in the admin to display the X latest pages
  1154. * that have been modified
  1155. *
  1156. * @param int $count number of pages to pull back
  1157. * @return array|null
  1158. */
  1159. public function latestPages($count = 10)
  1160. {
  1161. /** @var Flex $flex */
  1162. $flex = $this->grav['flex_objects'] ?? null;
  1163. $directory = $flex ? $flex->getDirectory('pages') : null;
  1164. if ($directory) {
  1165. return $directory->getIndex()->sort(['timestamp' => 'DESC'])->slice(0, $count);
  1166. }
  1167. $pages = static::enablePages();
  1168. $latest = [];
  1169. if (null === $pages->routes()) {
  1170. return null;
  1171. }
  1172. foreach ($pages->routes() as $url => $path) {
  1173. $page = $pages->find($url, true);
  1174. if ($page && $page->routable()) {
  1175. $latest[$page->route()] = ['modified' => $page->modified(), 'page' => $page];
  1176. }
  1177. }
  1178. // sort based on modified
  1179. uasort($latest, function ($a, $b) {
  1180. if ($a['modified'] == $b['modified']) {
  1181. return 0;
  1182. }
  1183. return ($a['modified'] > $b['modified']) ? -1 : 1;
  1184. });
  1185. // build new array with just pages in it
  1186. $list = [];
  1187. foreach ($latest as $item) {
  1188. $list[] = $item['page'];
  1189. }
  1190. return array_slice($list, 0, $count);
  1191. }
  1192. /**
  1193. * Get log file for fatal errors.
  1194. *
  1195. * @return string
  1196. */
  1197. public function logEntry()
  1198. {
  1199. $file = File::instance($this->grav['locator']->findResource("log://{$this->route}.html"));
  1200. $content = $file->content();
  1201. $file->free();
  1202. return $content;
  1203. }
  1204. /**
  1205. * Search in the logs when was the latest backup made
  1206. *
  1207. * @return array Array containing the latest backup information
  1208. */
  1209. public function lastBackup()
  1210. {
  1211. $backup_file = $this->grav['locator']->findResource('log://backup.log');
  1212. $content = null;
  1213. if ($backup_file) {
  1214. $file = JsonFile::instance((string) $backup_file);
  1215. $content = $file->content() ?? null;
  1216. }
  1217. if (!file_exists($backup_file) || is_null($content) || !isset($content['time'])) {
  1218. return [
  1219. 'days' => '&infin;',
  1220. 'chart_fill' => 100,
  1221. 'chart_empty' => 0
  1222. ];
  1223. }
  1224. $backup = new \DateTime();
  1225. $backup->setTimestamp($content['time']);
  1226. $diff = $backup->diff(new \DateTime());
  1227. $days = $diff->days;
  1228. $chart_fill = $days > 30 ? 100 : round($days / 30 * 100);
  1229. return [
  1230. 'days' => $days,
  1231. 'chart_fill' => $chart_fill,
  1232. 'chart_empty' => 100 - $chart_fill
  1233. ];
  1234. }
  1235. /**
  1236. * Determine if the plugin or theme info passed is from Team Grav
  1237. *
  1238. * @param object $info Plugin or Theme info object
  1239. * @return bool
  1240. */
  1241. public function isTeamGrav($info)
  1242. {
  1243. return isset($info['author']['name']) && ($info['author']['name'] === 'Team Grav' || Utils::contains($info['author']['name'], 'Trilby Media'));
  1244. }
  1245. /**
  1246. * Determine if the plugin or theme info passed is premium
  1247. *
  1248. * @param object $info Plugin or Theme info object
  1249. * @return bool
  1250. */
  1251. public function isPremiumProduct($info)
  1252. {
  1253. return isset($info['premium']);
  1254. }
  1255. /**
  1256. * Renders phpinfo
  1257. *
  1258. * @return string The phpinfo() output
  1259. */
  1260. public function phpinfo()
  1261. {
  1262. if (function_exists('phpinfo')) {
  1263. ob_start();
  1264. phpinfo();
  1265. $pinfo = ob_get_clean();
  1266. $pinfo = preg_replace('%^.*<body>(.*)</body>.*$%ms', '$1', $pinfo);
  1267. return $pinfo;
  1268. }
  1269. return 'phpinfo() method is not available on this server.';
  1270. }
  1271. /**
  1272. * Guest date format based on euro/US
  1273. *
  1274. * @param string|null $date
  1275. * @return string
  1276. */
  1277. public function guessDateFormat($date)
  1278. {
  1279. static $guess;
  1280. $date_formats = [
  1281. 'm/d/y',
  1282. 'm/d/Y',
  1283. 'n/d/y',
  1284. 'n/d/Y',
  1285. 'd-m-Y',
  1286. 'd-m-y',
  1287. ];
  1288. $time_formats = [
  1289. 'H:i',
  1290. 'G:i',
  1291. 'h:ia',
  1292. 'g:ia'
  1293. ];
  1294. $date = (string)$date;
  1295. if (!isset($guess[$date])) {
  1296. $guess[$date] = 'd-m-Y H:i';
  1297. foreach ($date_formats as $date_format) {
  1298. foreach ($time_formats as $time_format) {
  1299. $full_format = "{$date_format} {$time_format}";
  1300. if ($this->validateDate($date, $full_format)) {
  1301. $guess[$date] = $full_format;
  1302. break 2;
  1303. }
  1304. $full_format = "{$time_format} {$date_format}";
  1305. if ($this->validateDate($date, $full_format)) {
  1306. $guess[$date] = $full_format;
  1307. break 2;
  1308. }
  1309. }
  1310. }
  1311. }
  1312. return $guess[$date];
  1313. }
  1314. /**
  1315. * @param string $date
  1316. * @param string $format
  1317. * @return bool
  1318. */
  1319. public function validateDate($date, $format)
  1320. {
  1321. $d = DateTime::createFromFormat($format, $date);
  1322. return $d && $d->format($format) == $date;
  1323. }
  1324. /**
  1325. * @param string $php_format
  1326. * @return string
  1327. */
  1328. public function dateformatToMomentJS($php_format)
  1329. {
  1330. $SYMBOLS_MATCHING = [
  1331. // Day
  1332. 'd' => 'DD',
  1333. 'D' => 'ddd',
  1334. 'j' => 'D',
  1335. 'l' => 'dddd',
  1336. 'N' => 'E',
  1337. 'S' => 'Do',
  1338. 'w' => 'd',
  1339. 'z' => 'DDD',
  1340. // Week
  1341. 'W' => 'W',
  1342. // Month
  1343. 'F' => 'MMMM',
  1344. 'm' => 'MM',
  1345. 'M' => 'MMM',
  1346. 'n' => 'M',
  1347. 't' => '',
  1348. // Year
  1349. 'L' => '',
  1350. 'o' => 'GGGG',
  1351. 'Y' => 'YYYY',
  1352. 'y' => 'yy',
  1353. // Time
  1354. 'a' => 'a',
  1355. 'A' => 'A',
  1356. 'B' => 'SSS',
  1357. 'g' => 'h',
  1358. 'G' => 'H',
  1359. 'h' => 'hh',
  1360. 'H' => 'HH',
  1361. 'i' => 'mm',
  1362. 's' => 'ss',
  1363. 'u' => '',
  1364. // Timezone
  1365. 'e' => '',
  1366. 'I' => '',
  1367. 'O' => 'ZZ',
  1368. 'P' => 'Z',
  1369. 'T' => 'z',
  1370. 'Z' => '',
  1371. // Full Date/Time
  1372. 'c' => '',
  1373. 'r' => 'llll ZZ',
  1374. 'U' => 'X'
  1375. ];
  1376. $js_format = '';
  1377. $escaping = false;
  1378. $len = strlen($php_format);
  1379. for ($i = 0; $i < $len; $i++) {
  1380. $char = $php_format[$i];
  1381. if ($char === '\\') // PHP date format escaping character
  1382. {
  1383. $i++;
  1384. if ($escaping) {
  1385. $js_format .= $php_format[$i];
  1386. } else {
  1387. $js_format .= '\'' . $php_format[$i];
  1388. }
  1389. $escaping = true;
  1390. } else {
  1391. if ($escaping) {
  1392. $js_format .= "'";
  1393. $escaping = false;
  1394. }
  1395. if (isset($SYMBOLS_MATCHING[$char])) {
  1396. $js_format .= $SYMBOLS_MATCHING[$char];
  1397. } else {
  1398. $js_format .= $char;
  1399. }
  1400. }
  1401. }
  1402. return $js_format;
  1403. }
  1404. /**
  1405. * Gets the entire permissions array
  1406. *
  1407. * @return array
  1408. * @deprecated 1.10 Use $grav['permissions']->getInstances() instead.
  1409. */
  1410. public function getPermissions()
  1411. {
  1412. user_error(__METHOD__ . '() is deprecated since Admin 1.10, use $grav[\'permissions\']->getInstances() instead', E_USER_DEPRECATED);
  1413. $grav = $this->grav;
  1414. /** @var Permissions $permissions */
  1415. $permissions = $grav['permissions'];
  1416. return array_fill_keys(array_keys($permissions->getInstances()), 'boolean');
  1417. }
  1418. /**
  1419. * Sets the entire permissions array
  1420. *
  1421. * @param array $permissions
  1422. * @deprecated 1.10 Use PermissionsRegisterEvent::class event instead.
  1423. */
  1424. public function setPermissions($permissions)
  1425. {
  1426. user_error(__METHOD__ . '() is deprecated since Admin 1.10, use PermissionsRegisterEvent::class event instead', E_USER_DEPRECATED);
  1427. $this->addPermissions($permissions);
  1428. }
  1429. /**
  1430. * Adds a permission to the permissions array
  1431. *
  1432. * @param array $permissions
  1433. * @deprecated 1.10 Use RegisterPermissionsEvent::class event instead.
  1434. */
  1435. public function addPermissions($permissions)
  1436. {
  1437. user_error(__METHOD__ . '() is deprecated since Admin 1.10, use RegisterPermissionsEvent::class event instead', E_USER_DEPRECATED);
  1438. $grav = $this->grav;
  1439. /** @var Permissions $object */
  1440. $object = $grav['permissions'];
  1441. foreach ($permissions as $name => $type) {
  1442. if (!$object->hasAction($name)) {
  1443. $action = new Action($name);
  1444. $object->addAction($action);
  1445. }
  1446. }
  1447. }
  1448. public function getNotifications($force = false)
  1449. {
  1450. $last_checked = null;
  1451. $filename = $this->grav['locator']->findResource('user://data/notifications/' . md5($this->grav['user']->username) . YAML_EXT, true, true);
  1452. $userStatus = $this->grav['locator']->findResource('user://data/notifications/' . $this->grav['user']->username . YAML_EXT, true, true);
  1453. $notifications_file = CompiledYamlFile::instance($filename);
  1454. $notifications_content = (array)$notifications_file->content();
  1455. $userStatus_file = CompiledYamlFile::instance($userStatus);
  1456. $userStatus_content = (array)$userStatus_file->content();
  1457. $last_checked = $notifications_content['last_checked'] ?? null;
  1458. $notifications = $notifications_content['data'] ?? array();
  1459. $timeout = $this->grav['config']->get('system.session.timeout', 1800);
  1460. if ($force || !$last_checked || empty($notifications) || (time() - $last_checked > $timeout)) {
  1461. $body = Response::get('https://getgrav.org/notifications.json?' . time());
  1462. // $body = Response::get('http://localhost/notifications.json?' . time());
  1463. $notifications = json_decode($body, true);
  1464. // Sort by date
  1465. usort($notifications, function ($a, $b) {
  1466. return strcmp($a['date'], $b['date']);
  1467. });
  1468. // Reverse order and create a new array
  1469. $notifications = array_reverse($notifications);
  1470. $cleaned_notifications = [];
  1471. foreach ($notifications as $key => $notification) {
  1472. if (isset($notification['permissions']) && !$this->authorize($notification['permissions'])) {
  1473. continue;
  1474. }
  1475. if (isset($notification['dependencies'])) {
  1476. foreach ($notification['dependencies'] as $dependency => $constraints) {
  1477. if ($dependency === 'grav') {
  1478. if (!Semver::satisfies(GRAV_VERSION, $constraints)) {
  1479. continue 2;
  1480. }
  1481. } else {
  1482. $packages = array_merge($this->plugins()->toArray(), $this->themes()->toArray());
  1483. if (!isset($packages[$dependency])) {
  1484. continue 2;
  1485. } else {
  1486. $version = $packages[$dependency]['version'];
  1487. if (!Semver::satisfies($version, $constraints)) {
  1488. continue 2;
  1489. }
  1490. }
  1491. }
  1492. }
  1493. }
  1494. $cleaned_notifications[] = $notification;
  1495. }
  1496. // reset notifications
  1497. $notifications = [];
  1498. foreach($cleaned_notifications as $notification) {
  1499. foreach ($notification['location'] as $location) {
  1500. $notifications = array_merge_recursive($notifications, [$location => [$notification]]);
  1501. }
  1502. }
  1503. $notifications_file->content(['last_checked' => time(), 'data' => $notifications]);
  1504. $notifications_file->save();
  1505. }
  1506. foreach ($notifications as $location => $list) {
  1507. $notifications[$location] = array_filter($list, function ($notification) use ($userStatus_content) {
  1508. $element = $userStatus_content[$notification['id']] ?? null;
  1509. if (isset($element)) {
  1510. if (isset($notification['reappear_after'])) {
  1511. $now = new \DateTime();
  1512. $hidden_on = new \DateTime($element);
  1513. $hidden_on->modify($notification['reappear_after']);
  1514. if ($now >= $hidden_on) {
  1515. return true;
  1516. }
  1517. }
  1518. return false;
  1519. }
  1520. return true;
  1521. });
  1522. }
  1523. return $notifications;
  1524. }
  1525. /**
  1526. * Get https://getgrav.org news feed
  1527. *
  1528. * @return mixed
  1529. * @throws MalformedXmlException
  1530. */
  1531. public function getFeed($force = false)
  1532. {
  1533. $last_checked = null;
  1534. $filename = $this->grav['locator']->findResource('user://data/feed/' . md5($this->grav['user']->username) . YAML_EXT, true, true);
  1535. $feed_file = CompiledYamlFile::instance($filename);
  1536. $feed_content = (array)$feed_file->content();
  1537. $last_checked = $feed_content['last_checked'] ?? null;
  1538. $feed = $feed_content['data'] ?? array();
  1539. $timeout = $this->grav['config']->get('system.session.timeout', 1800);
  1540. if ($force || !$last_checked || empty($feed) || ($last_checked && (time() - $last_checked > $timeout))) {
  1541. $feed_url = 'https://getgrav.org/blog.atom';
  1542. $body = Response::get($feed_url);
  1543. $reader = new Reader();
  1544. $parser = $reader->getParser($feed_url, $body, 'utf-8');
  1545. $data = $parser->execute()->getItems();
  1546. // Get top 10
  1547. $data = array_slice($data, 0, 10);
  1548. $feed = array_map(function ($entry) {
  1549. $simple_entry['title'] = $entry->getTitle();
  1550. $simple_entry['url'] = $entry->getUrl();
  1551. $simple_entry['date'] = $entry->getDate()->getTimestamp();
  1552. $simple_entry['nicetime'] = $this->adminNiceTime($simple_entry['date']);
  1553. return $simple_entry;
  1554. }, $data);
  1555. $feed_file->content(['last_checked' => time(), 'data' => $feed]);
  1556. $feed_file->save();
  1557. }
  1558. return $feed;
  1559. }
  1560. public function adminNiceTime($date, $long_strings = true)
  1561. {
  1562. if (empty($date)) {
  1563. return $this->translate('GRAV.NICETIME.NO_DATE_PROVIDED', null);
  1564. }
  1565. if ($long_strings) {
  1566. $periods = [
  1567. 'NICETIME.SECOND',
  1568. 'NICETIME.MINUTE',
  1569. 'NICETIME.HOUR',
  1570. 'NICETIME.DAY',
  1571. 'NICETIME.WEEK',
  1572. 'NICETIME.MONTH',
  1573. 'NICETIME.YEAR',
  1574. 'NICETIME.DECADE'
  1575. ];
  1576. } else {
  1577. $periods = [
  1578. 'NICETIME.SEC',
  1579. 'NICETIME.MIN',
  1580. 'NICETIME.HR',
  1581. 'NICETIME.DAY',
  1582. 'NICETIME.WK',
  1583. 'NICETIME.MO',
  1584. 'NICETIME.YR',
  1585. 'NICETIME.DEC'
  1586. ];
  1587. }
  1588. $lengths = ['60', '60', '24', '7', '4.35', '12', '10'];
  1589. $now = time();
  1590. // check if unix timestamp
  1591. if ((string)(int)$date === (string)$date) {
  1592. $unix_date = $date;
  1593. } else {
  1594. $unix_date = strtotime($date);
  1595. }
  1596. // check validity of date
  1597. if (empty($unix_date)) {
  1598. return $this->translate('GRAV.NICETIME.BAD_DATE', null);
  1599. }
  1600. // is it future date or past date
  1601. if ($now > $unix_date) {
  1602. $difference = $now - $unix_date;
  1603. $tense = $this->translate('GRAV.NICETIME.AGO', null);
  1604. } else {
  1605. $difference = $unix_date - $now;
  1606. $tense = $this->translate('GRAV.NICETIME.FROM_NOW', null);
  1607. }
  1608. $len = count($lengths) - 1;
  1609. for ($j = 0; $difference >= $lengths[$j] && $j < $len; $j++) {
  1610. $difference /= $lengths[$j];
  1611. }
  1612. $difference = round($difference);
  1613. if ($difference !== 1) {
  1614. $periods[$j] .= '_PLURAL';
  1615. }
  1616. if ($this->grav['language']->getTranslation($this->grav['user']->language,
  1617. $periods[$j] . '_MORE_THAN_TWO')
  1618. ) {
  1619. if ($difference > 2) {
  1620. $periods[$j] .= '_MORE_THAN_TWO';
  1621. }
  1622. }
  1623. $periods[$j] = $this->translate('GRAV.'.$periods[$j], null);
  1624. return "{$difference} {$periods[$j]} {$tense}";
  1625. }
  1626. public function findFormFields($type, $fields, $found_fields = [])
  1627. {
  1628. foreach ($fields as $key => $field) {
  1629. if (isset($field['type']) && $field['type'] == $type) {
  1630. $found_fields[$key] = $field;
  1631. } elseif (isset($field['fields'])) {
  1632. $result = $this->findFormFields($type, $field['fields'], $found_fields);
  1633. if (!empty($result)) {
  1634. $found_fields = array_merge($found_fields, $result);
  1635. }
  1636. }
  1637. }
  1638. return $found_fields;
  1639. }
  1640. public function getPagePathFromToken($path, $page = null)
  1641. {
  1642. return Utils::getPagePathFromToken($path, $page ?: $this->page(true));
  1643. }
  1644. /**
  1645. * Returns edited page.
  1646. *
  1647. * @param bool $route
  1648. *
  1649. * @param null $path
  1650. *
  1651. * @return PageInterface
  1652. */
  1653. public function page($route = false, $path = null)
  1654. {
  1655. if (!$path) {
  1656. $path = $this->route;
  1657. }
  1658. if ($route && !$path) {
  1659. $path = '/';
  1660. }
  1661. if (!isset($this->pages[$path])) {
  1662. $this->pages[$path] = $this->getPage($path);
  1663. }
  1664. return $this->pages[$path];
  1665. }
  1666. /**
  1667. * Returns the page creating it if it does not exist.
  1668. *
  1669. * @param string $path
  1670. *
  1671. * @return PageInterface|null
  1672. */
  1673. public function getPage($path)
  1674. {
  1675. $pages = static::enablePages();
  1676. if ($path && $path[0] !== '/') {
  1677. $path = "/{$path}";
  1678. }
  1679. // Fix for entities in path causing looping...
  1680. $path = urldecode($path);
  1681. $page = $path ? $pages->find($path, true) : $pages->root();
  1682. if (!$page) {
  1683. $slug = Utils::basename($path);
  1684. if ($slug === '') {
  1685. return null;
  1686. }
  1687. $ppath = str_replace('\\', '/', dirname($path));
  1688. // Find or create parent(s).
  1689. $parent = $this->getPage($ppath !== '/' ? $ppath : '');
  1690. // Create page.
  1691. $page = new Page();
  1692. $page->parent($parent);
  1693. $page->filePath($parent->path() . '/' . $slug . '/' . $page->name());
  1694. // Add routing information.
  1695. $pages->addPage($page, $path);
  1696. // Set if Modular
  1697. $page->modularTwig($slug[0] === '_');
  1698. // Determine page type.
  1699. if (isset($this->session->{$page->route()})) {
  1700. // Found the type and header from the session.
  1701. $data = $this->session->{$page->route()};
  1702. // Set the key header value
  1703. $header = ['title' => $data['title']];
  1704. if (isset($data['visible'])) {
  1705. if ($data['visible'] === '' || $data['visible']) {
  1706. // if auto (ie '')
  1707. $pageParent = $page->parent();
  1708. $children = $pageParent ? $pageParent->children() : [];
  1709. foreach ($children as $child) {
  1710. if ($child->order()) {
  1711. // set page order
  1712. $page->order(AdminController::getNextOrderInFolder($pageParent->path()));
  1713. break;
  1714. }
  1715. }
  1716. }
  1717. if ((int)$data['visible'] === 1 && !$page->order()) {
  1718. $header['visible'] = $data['visible'];
  1719. }
  1720. }
  1721. if ($data['name'] === 'modular') {
  1722. $header['body_classes'] = 'modular';
  1723. }
  1724. $name = $page->isModule() ? str_replace('modular/', '', $data['name']) : $data['name'];
  1725. $page->name($name . '.md');
  1726. // Fire new event to allow plugins to manipulate page frontmatter
  1727. $this->grav->fireEvent('onAdminCreatePageFrontmatter', new Event(['header' => &$header,
  1728. 'data' => $data]));
  1729. $page->header($header);
  1730. $page->frontmatter(Yaml::dump((array)$page->header(), 20));
  1731. } else {
  1732. // Find out the type by looking at the parent.
  1733. $type = $parent->childType() ?: $parent->blueprints()->get('child_type', 'default');
  1734. $page->name($type . CONTENT_EXT);
  1735. $page->header();
  1736. }
  1737. }
  1738. return $page;
  1739. }
  1740. public function generateReports()
  1741. {
  1742. $reports = new ArrayCollection();
  1743. $pages = static::enablePages();
  1744. // Default to XSS Security Report
  1745. $result = Security::detectXssFromPages($pages, true);
  1746. $reports['Grav Security Check'] = $this->grav['twig']->processTemplate('reports/security.html.twig', [
  1747. 'result' => $result,
  1748. ]);
  1749. // Linting Issues
  1750. $result = YamlLinter::lint();
  1751. $reports['Grav Yaml Linter'] = $this->grav['twig']->processTemplate('reports/yamllinter.html.twig', [
  1752. 'result' => $result,
  1753. ]);
  1754. // Fire new event to allow plugins to manipulate page frontmatter
  1755. $this->grav->fireEvent('onAdminGenerateReports', new Event(['reports' => $reports]));
  1756. return $reports;
  1757. }
  1758. public function getRouteDetails()
  1759. {
  1760. return [$this->base, $this->location, $this->route];
  1761. }
  1762. /**
  1763. * Get the files list
  1764. *
  1765. * @param bool $filtered
  1766. * @param int $page_index
  1767. * @return array|null
  1768. * @todo allow pagination
  1769. */
  1770. public function files($filtered = true, $page_index = 0)
  1771. {
  1772. $param_type = $this->grav['uri']->param('type');
  1773. $param_date = $this->grav['uri']->param('date');
  1774. $param_page = $this->grav['uri']->param('page');
  1775. $param_page = str_replace('\\', '/', $param_page);
  1776. $files_cache_key = 'media-manager-files';
  1777. if ($param_type) {
  1778. $files_cache_key .= "-{$param_type}";
  1779. }
  1780. if ($param_date) {
  1781. $files_cache_key .= "-{$param_date}";
  1782. }
  1783. if ($param_page) {
  1784. $files_cache_key .= "-{$param_page}";
  1785. }
  1786. $page_files = null;
  1787. $cache_enabled = $this->grav['config']->get('plugins.admin.cache_enabled');
  1788. if (!$cache_enabled) {
  1789. $this->grav['cache']->setEnabled(true);
  1790. }
  1791. $page_files = $this->grav['cache']->fetch(md5($files_cache_key));
  1792. if (!$cache_enabled) {
  1793. $this->grav['cache']->setEnabled(false);
  1794. }
  1795. if (!$page_files) {
  1796. $page_files = [];
  1797. $pages = static::enablePages();
  1798. if ($param_page) {
  1799. $page = $pages->find($param_page);
  1800. $page_files = $this->getFiles('images', $page, $page_files, $filtered);
  1801. $page_files = $this->getFiles('videos', $page, $page_files, $filtered);
  1802. $page_files = $this->getFiles('audios', $page, $page_files, $filtered);
  1803. $page_files = $this->getFiles('files', $page, $page_files, $filtered);
  1804. } else {
  1805. $allPages = $pages->all();
  1806. if ($allPages) foreach ($allPages as $page) {
  1807. $page_files = $this->getFiles('images', $page, $page_files, $filtered);
  1808. $page_files = $this->getFiles('videos', $page, $page_files, $filtered);
  1809. $page_files = $this->getFiles('audios', $page, $page_files, $filtered);
  1810. $page_files = $this->getFiles('files', $page, $page_files, $filtered);
  1811. }
  1812. }
  1813. if (count($page_files) >= self::MEDIA_PAGINATION_INTERVAL) {
  1814. $this->shouldLoadAdditionalFilesInBackground(true);
  1815. }
  1816. if (!$cache_enabled) {
  1817. $this->grav['cache']->setEnabled(true);
  1818. }
  1819. $this->grav['cache']->save(md5($files_cache_key), $page_files, 600); //cache for 10 minutes
  1820. if (!$cache_enabled) {
  1821. $this->grav['cache']->setEnabled(false);
  1822. }
  1823. }
  1824. if (count($page_files) >= self::MEDIA_PAGINATION_INTERVAL) {
  1825. $page_files = array_slice($page_files, $page_index * self::MEDIA_PAGINATION_INTERVAL, self::MEDIA_PAGINATION_INTERVAL);
  1826. }
  1827. return $page_files;
  1828. }
  1829. public function shouldLoadAdditionalFilesInBackground($status = null)
  1830. {
  1831. if ($status) {
  1832. $this->load_additional_files_in_background = true;
  1833. }
  1834. return $this->load_additional_files_in_background;
  1835. }
  1836. public function loadAdditionalFilesInBackground($status = null)
  1837. {
  1838. if (!$this->loading_additional_files_in_background) {
  1839. $this->loading_additional_files_in_background = true;
  1840. $this->files(false, false);
  1841. $this->shouldLoadAdditionalFilesInBackground(false);
  1842. $this->loading_additional_files_in_background = false;
  1843. }
  1844. }
  1845. private function getFiles($type, $page, $page_files, $filtered)
  1846. {
  1847. $page_files = $this->getMediaOfType($type, $page, $page_files);
  1848. if ($filtered) {
  1849. $page_files = $this->filterByType($page_files);
  1850. $page_files = $this->filterByDate($page_files);
  1851. }
  1852. return $page_files;
  1853. }
  1854. /**
  1855. * Get all the media of a type ('images' | 'audios' | 'videos' | 'files')
  1856. *
  1857. * @param string $type
  1858. * @param PageInterface|null $page
  1859. * @param array $files
  1860. *
  1861. * @return array
  1862. */
  1863. private function getMediaOfType($type, ?PageInterface $page, array $files)
  1864. {
  1865. if ($page) {
  1866. $media = $page->media();
  1867. $mediaOfType = $media->$type();
  1868. foreach($mediaOfType as $title => $file) {
  1869. $files[] = [
  1870. 'title' => $title,
  1871. 'type' => $type,
  1872. 'page_route' => $page->route(),
  1873. 'file' => $file->higherQualityAlternative()
  1874. ];
  1875. }
  1876. return $files;
  1877. }
  1878. return [];
  1879. }
  1880. /**
  1881. * Filter media by type
  1882. *
  1883. * @param array $filesFiltered
  1884. *
  1885. * @return array
  1886. */
  1887. private function filterByType($filesFiltered)
  1888. {
  1889. $filter_type = $this->grav['uri']->param('type');
  1890. if (!$filter_type) {
  1891. return $filesFiltered;
  1892. }
  1893. $filesFiltered = array_filter($filesFiltered, function ($file) use ($filter_type) {
  1894. return $file['type'] == $filter_type;
  1895. });
  1896. return $filesFiltered;
  1897. }
  1898. /**
  1899. * Filter media by date
  1900. *
  1901. * @param array $filesFiltered
  1902. *
  1903. * @return array
  1904. */
  1905. private function filterByDate($filesFiltered)
  1906. {
  1907. $filter_date = $this->grav['uri']->param('date');
  1908. if (!$filter_date) {
  1909. return $filesFiltered;
  1910. }
  1911. $year = substr($filter_date, 0, 4);
  1912. $month = substr($filter_date, 5, 2);
  1913. $filesFilteredByDate = [];
  1914. foreach($filesFiltered as $file) {
  1915. $filedate = $this->fileDate($file['file']);
  1916. $fileYear = $filedate->format('Y');
  1917. $fileMonth = $filedate->format('m');
  1918. if ($fileYear == $year && $fileMonth == $month) {
  1919. $filesFilteredByDate[] = $file;
  1920. }
  1921. }
  1922. return $filesFilteredByDate;
  1923. }
  1924. /**
  1925. * Return the DateTime object representation of a file modified date
  1926. *
  1927. * @param File $file
  1928. *
  1929. * @return DateTime
  1930. */
  1931. private function fileDate($file) {
  1932. $datetime = new \DateTime();
  1933. $datetime->setTimestamp($file->toArray()['modified']);
  1934. return $datetime;
  1935. }
  1936. /**
  1937. * Get the files dates list to be used in the Media Files filter
  1938. *
  1939. * @return array
  1940. */
  1941. public function filesDates()
  1942. {
  1943. $files = $this->files(false);
  1944. $dates = [];
  1945. foreach ($files as $file) {
  1946. $datetime = $this->fileDate($file['file']);
  1947. $year = $datetime->format('Y');
  1948. $month = $datetime->format('m');
  1949. if (!isset($dates[$year])) {
  1950. $dates[$year] = [];
  1951. }
  1952. if (!isset($dates[$year][$month])) {
  1953. $dates[$year][$month] = 1;
  1954. } else {
  1955. $dates[$year][$month]++;
  1956. }
  1957. }
  1958. return $dates;
  1959. }
  1960. /**
  1961. * Get the pages list to be used in the Media Files filter
  1962. *
  1963. * @return array
  1964. */
  1965. public function pages()
  1966. {
  1967. $pages = static::enablePages();
  1968. $collection = $pages->all();
  1969. $pagesWithFiles = [];
  1970. foreach ($collection as $page) {
  1971. if (count($page->media()->all())) {
  1972. $pagesWithFiles[] = $page;
  1973. }
  1974. }
  1975. return $pagesWithFiles;
  1976. }
  1977. /**
  1978. * @return Pages
  1979. */
  1980. public static function enablePages()
  1981. {
  1982. static $pages;
  1983. if ($pages) {
  1984. return $pages;
  1985. }
  1986. $grav = Grav::instance();
  1987. $admin = $grav['admin'];
  1988. /** @var Pages $pages */
  1989. $pages = Grav::instance()['pages'];
  1990. $pages->enablePages();
  1991. // If page is null, the default page does not exist, and we cannot route to it
  1992. $page = $pages->find('/', true);
  1993. if ($page) {
  1994. // Set original route for the home page.
  1995. $home = '/' . trim($grav['config']->get('system.home.alias'), '/');
  1996. $page->route($home);
  1997. }
  1998. $admin->routes = $pages->routes();
  1999. // Remove default route from routes.
  2000. if (isset($admin->routes['/'])) {
  2001. unset($admin->routes['/']);
  2002. }
  2003. return $pages;
  2004. }
  2005. /**
  2006. * Return HTTP_REFERRER if set
  2007. *
  2008. * @return null
  2009. */
  2010. public function getReferrer()
  2011. {
  2012. return $_SERVER['HTTP_REFERER'] ?? null;
  2013. }
  2014. /**
  2015. * Get Grav system log files
  2016. *
  2017. * @return array
  2018. */
  2019. public function getLogFiles()
  2020. {
  2021. $logs = new GravData(['grav.log' => 'Grav System Log', 'email.log' => 'Email Log']);
  2022. Grav::instance()->fireEvent('onAdminLogFiles', new Event(['logs' => &$logs]));
  2023. return $logs->toArray();
  2024. }
  2025. /**
  2026. * Get changelog for a given GPM package based on slug
  2027. *
  2028. * @param string|null $slug
  2029. * @return array
  2030. */
  2031. public function getChangelog($slug = null)
  2032. {
  2033. $gpm = $this->gpm();
  2034. $changelog = [];
  2035. if (!empty($slug)) {
  2036. $package = $gpm->findPackage($slug);
  2037. } else {
  2038. $package = $gpm->grav;
  2039. }
  2040. if ($package) {
  2041. $changelog = $package->getChangelog();
  2042. }
  2043. return $changelog;
  2044. }
  2045. /**
  2046. * Prepare and return POST data.
  2047. *
  2048. * @param array $post
  2049. * @return array
  2050. */
  2051. public function preparePost($post): array
  2052. {
  2053. if (!is_array($post)) {
  2054. return [];
  2055. }
  2056. unset($post['task']);
  2057. // Decode JSON encoded fields and merge them to data.
  2058. if (isset($post['_json'])) {
  2059. $post = array_replace_recursive($post, $this->jsonDecode($post['_json']));
  2060. unset($post['_json']);
  2061. }
  2062. return $this->cleanDataKeys($post);
  2063. }
  2064. /**
  2065. * Recursively JSON decode data.
  2066. *
  2067. * @param array $data
  2068. * @return array
  2069. * @throws JsonException
  2070. */
  2071. private function jsonDecode(array $data): array
  2072. {
  2073. foreach ($data as &$value) {
  2074. if (is_array($value)) {
  2075. $value = $this->jsonDecode($value);
  2076. } else {
  2077. $value = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
  2078. }
  2079. }
  2080. return $data;
  2081. }
  2082. /**
  2083. * @param array $source
  2084. * @return array
  2085. */
  2086. private function cleanDataKeys(array $source): array
  2087. {
  2088. $out = [];
  2089. foreach ($source as $key => $value) {
  2090. $key = str_replace(['%5B', '%5D'], ['[', ']'], $key);
  2091. if (is_array($value)) {
  2092. $out[$key] = $this->cleanDataKeys($value);
  2093. } else {
  2094. $out[$key] = $value;
  2095. }
  2096. }
  2097. return $out;
  2098. }
  2099. }