Admin.php 70 KB

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