diff --git a/Classes/Controller/TemplatesAjaxController.php b/Classes/Controller/TemplatesAjaxController.php new file mode 100644 index 0000000..ebc4b36 --- /dev/null +++ b/Classes/Controller/TemplatesAjaxController.php @@ -0,0 +1,62 @@ +getQueryParams(); + $templateId = (string)($queryParams['templateId'] ?? ''); + $requestId = (string)($queryParams['requestId'] ?? ''); + $this->validateParameters($templateId, $requestId); + $moduleData = GeneralUtility::makeInstance(ModuleDataService::class) + ->getModuleDataByRequestId(Templates::class, $requestId); + $templateData = []; + $statusCode = 404; + + if ($moduleData instanceof ModuleData) { + $templateRecord = $moduleData['templates'][$templateId] ?? null; + if (is_array($templateRecord)) { + $absTemplatePath = GeneralUtility::getFileAbsFileName($templateRecord['path']); + if (GeneralUtility::isAllowedAbsPath($absTemplatePath) && file_exists($absTemplatePath)) { + $content = file_get_contents($absTemplatePath); + $statusCode = 200; + $templateData['templateId'] = $templateId; + $templateData['template'] = $content; + } + } + } + + return new JsonResponse($templateData, $statusCode); + } + + private function validateParameters(string $templateId, string $requestId): void + { + if (!$templateId || !$requestId) { + throw new \InvalidArgumentException( + 'Missing parameters, templateId and requestId need to be set.', + 1561386190 + ); + } + } +} diff --git a/Classes/Modules/Fluid/TemplatePaths.php b/Classes/Modules/Fluid/TemplatePaths.php new file mode 100644 index 0000000..527c996 --- /dev/null +++ b/Classes/Modules/Fluid/TemplatePaths.php @@ -0,0 +1,97 @@ +logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__); + } + + /** + * Attempts to resolve an absolute filename + * of a template (i.e. `templateRootPaths`) + * using a controller name, action and format. + * + * Works _backwards_ through template paths in + * order to achieve an "overlay"-type behavior + * where the last paths added are the first to + * be checked and the first path added acts as + * fallback if no other paths have the file. + * + * If the file does not exist in any path, + * including fallback path, `NULL` is returned. + * + * Path configurations filled from TypoScript + * is automatically recorded in the right + * order (see `fillFromTypoScriptArray`), but + * when manually setting the paths that should + * be checked, you as user must be aware of + * this reverse behavior (which you should + * already be, given that it is the same way + * TypoScript path configurations work). + * + * @param string $controller + * @param string $action + * @param string $format + * @return string|NULL + * @api + */ + public function resolveTemplateFileForControllerAndActionAndFormat($controller, $action, $format = null): ?string + { + $templateName = parent::resolveTemplateFileForControllerAndActionAndFormat( + $controller, + $action, + $format + ); + + if (is_string($templateName)) { + if (StringUtility::beginsWith($templateName, Environment::getExtensionsPath())) { + $path = str_replace(Environment::getExtensionsPath().DIRECTORY_SEPARATOR, 'EXT:', $templateName); + } elseif (StringUtility::beginsWith($templateName, Environment::getFrameworkBasePath())) { + $path = str_replace(Environment::getFrameworkBasePath().DIRECTORY_SEPARATOR, 'EXT:', $templateName); + } + + $format = $format ?? $this->getFormat(); + $identifier = uniqid("template-{$controller}-{$action}-{$format}-", false); + + $this->logger->log( + LogLevel::DEBUG, + $identifier, + [ + 'path' => $path ?? $templateName, + 'controller' => $controller, + 'action' => $action, + 'format' => $format, + ] + ); + } + + return $templateName; + } +} diff --git a/Classes/Modules/Fluid/Templates.php b/Classes/Modules/Fluid/Templates.php new file mode 100644 index 0000000..e995f1e --- /dev/null +++ b/Classes/Modules/Fluid/Templates.php @@ -0,0 +1,135 @@ +getLanguageService()->sL( + 'LLL:EXT:adminpanel_extended/Resources/Private/Language/locallang_fluid.xlf:submodule.templates.label' + ); + } + + /** + * Main method for content generation of an admin panel module. + * Return content as HTML. For modules implementing the DataProviderInterface + * the "ModuleData" object is automatically filled with the stored data - if + * no data is given a "fresh" ModuleData object is injected. + * + * @param \TYPO3\CMS\Adminpanel\ModuleApi\ModuleData $data + * @return string + */ + public function getContent(ModuleData $data): string + { + $view = GeneralUtility::makeInstance(StandaloneView::class); + + $view->setTemplatePathAndFilename('EXT:adminpanel_extended/Resources/Private/Templates/Fluid/Templates.html'); + $view->assignMultiple($data->getArrayCopy()); + $url = GeneralUtility::makeInstance(UriBuilder::class)->buildUriFromRoute( + 'ajax_adminPanelExtended_templateData', + ['requestId' => $data['requestId']] + ); + $view->assign('templateDataUri', $url); + + return $view->render(); + } + + /** + * @param \Psr\Http\Message\ServerRequestInterface $request + * @return \TYPO3\CMS\Adminpanel\ModuleApi\ModuleData + */ + public function getDataToStore(ServerRequestInterface $request): ModuleData + { + $log = InMemoryLogWriter::$log; + /** @var LogRecord[] $templateRecords */ + $templateRecords = array_filter( + $log, + static function (LogRecord $entry) { + return $entry->getComponent() === 'Psychomieze.AdminpanelExtended.Modules.Fluid.TemplatePaths'; + } + ); + + $templates = []; + + foreach ($templateRecords as $logRecord) { + $templates[$logRecord->getMessage()] = $logRecord->getData(); + } + + $templates = array_unique($templates, SORT_REGULAR); + + return new ModuleData([ + 'templates' => $templates, + 'requestId' => $request->getAttribute('adminPanelRequestId') + ]); + } + + /** + * Returns a string array with javascript files that will be rendered after the module + * + * Example: return ['EXT:adminpanel/Resources/Public/JavaScript/Modules/Edit.js']; + * + * @return array + */ + public function getJavaScriptFiles(): array + { + return [ + 'EXT:adminpanel_extended/Resources/Public/JavaScript/Templates.js', + 'EXT:adminpanel_extended/Resources/Public/JavaScript/vendor/prettify.js', + ]; + } + + /** + * Returns a string array with css files that will be rendered after the module + * + * Example: return ['EXT:adminpanel/Resources/Public/JavaScript/Modules/Edit.css']; + * + * @return array + */ + public function getCssFiles(): array + { + return [ + 'EXT:adminpanel_extended/Resources/Public/Css/Templates.css', + 'EXT:adminpanel_extended/Resources/Public/Css/vendor/desert.css' + ]; + } +} diff --git a/Configuration/Backend/AjaxRoutes.php b/Configuration/Backend/AjaxRoutes.php index 0a18fe7..95e2340 100644 --- a/Configuration/Backend/AjaxRoutes.php +++ b/Configuration/Backend/AjaxRoutes.php @@ -14,4 +14,8 @@ 'path' => '/adminpanelExtended/signals/data', 'target' => \Psychomieze\AdminpanelExtended\Controller\SignalsAjaxController::class . '::getData' ], + 'adminPanelExtended_templateData' => [ + 'path' => '/adminpanelExtended/templates/data', + 'target' => \Psychomieze\AdminpanelExtended\Controller\TemplatesAjaxController::class . '::getData' + ], ]; diff --git a/README.md b/README.md index 5e55816..6d35c19 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Currently in alpha phase! - Show currently online users and info if someone is editing current page - Show fluid global namespaces - Show fluid preprocessors +- Show fluid templates that have been rendered with code highlighting ### Development @@ -25,3 +26,7 @@ initiative, see + +## Misc + +This project is highly inspired by the symfony profiler: diff --git a/Resources/Private/Language/locallang_fluid.xlf b/Resources/Private/Language/locallang_fluid.xlf index eff3277..62985e0 100644 --- a/Resources/Private/Language/locallang_fluid.xlf +++ b/Resources/Private/Language/locallang_fluid.xlf @@ -28,6 +28,9 @@ Priority + + Templates + diff --git a/Resources/Private/Templates/Fluid/Templates.html b/Resources/Private/Templates/Fluid/Templates.html new file mode 100644 index 0000000..701a237 --- /dev/null +++ b/Resources/Private/Templates/Fluid/Templates.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + +
PathControllerActionFormat
+ {data.path} + {data.controller}{data.action}{data.format}
+
+ No templates +
+ diff --git a/Resources/Public/Css/Templates.css b/Resources/Public/Css/Templates.css new file mode 100644 index 0000000..825c47b --- /dev/null +++ b/Resources/Public/Css/Templates.css @@ -0,0 +1,28 @@ +/* + * This file is part of the TYPO3 Adminpanel Initiative. + * + * For the full copyright and license information, please read the + * LICENSE file that was distributed with this source code. + */ +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint { + background-color: #333; +} +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint *{ + white-space: pre; +} +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint li.L0, +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint li.L1, +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint li.L2, +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint li.L3, +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint li.L5, +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint li.L6, +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint li.L7, +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint li.L8 { + list-style-type: decimal; +} +#TSFE_ADMIN_PANEL_FORM.typo3-kidjls9dksoje.typo3-adminPanel pre.prettyprint ol.linenums { + padding-left: 40px; + margin-top: 0; + margin-bottom: 0; + color: #AEAEAE; +} diff --git a/Resources/Public/Css/vendor/desert.css b/Resources/Public/Css/vendor/desert.css new file mode 100644 index 0000000..ba7d735 --- /dev/null +++ b/Resources/Public/Css/vendor/desert.css @@ -0,0 +1,17 @@ +/* + + Copyright (C) 2006 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +pre .atn,pre .kwd,pre .tag{font-weight:700}pre.prettyprint{display:block;background-color:#333}pre .nocode{background-color:none;color:#000}pre .str{color:#ffa0a0}pre .kwd{color:khaki}pre .com{color:#87ceeb}pre .typ{color:#98fb98}pre .lit{color:#cd5c5c}pre .pln,pre .pun{color:#fff}pre .tag{color:khaki}pre .atn{color:#bdb76b}pre .atv{color:#ffa0a0}pre .dec{color:#98fb98}ol.linenums{margin-top:0;margin-bottom:0;color:#AEAEAE}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}@media print{pre.prettyprint{background-color:none}code .str,pre .str{color:#060}code .kwd,pre .kwd{color:#006;font-weight:700}code .com,pre .com{color:#600;font-style:italic}code .typ,pre .typ{color:#404;font-weight:700}code .lit,pre .lit{color:#044}code .pun,pre .pun{color:#440}code .pln,pre .pln{color:#000}code .tag,pre .tag{color:#006;font-weight:700}code .atn,pre .atn{color:#404}code .atv,pre .atv{color:#060}} diff --git a/Resources/Public/JavaScript/Templates.js b/Resources/Public/JavaScript/Templates.js new file mode 100644 index 0000000..41a271f --- /dev/null +++ b/Resources/Public/JavaScript/Templates.js @@ -0,0 +1,68 @@ +/* + * This file is part of the TYPO3 Adminpanel Initiative. + * + * For the full copyright and license information, please read the + * LICENSE file that was distributed with this source code. + */ +const Templates = {}; + +Templates.initialize = function () { + const templateLinks = document.querySelectorAll('[data-typo3-role=template-link]'); + for (let i = 0; i < templateLinks.length; i++) { + templateLinks[i].addEventListener('click', Templates.getTemplateData); + } +}; +Templates.getTemplateData = function (event) { + event.preventDefault(); + const uri = this.dataset.typo3AjaxUrl; + const request = new XMLHttpRequest(); + request.open('GET', uri); + request.send(); + + /** + * @param template + * @returns {HTMLTableRowElement} + */ + function createTableRow(template) { + const tableRow = document.createElement('tr'); + const templateData = document.createElement('td'); + const pre = document.createElement('pre'); + pre.setAttribute('class', 'prettyprint lang-html linenums'); + const code = document.createElement('code'); + code.innerText = template; + templateData.setAttribute('colspan', '5'); + + pre.appendChild(code); + templateData.appendChild(pre); + tableRow.appendChild(templateData); + return tableRow; + }; + + request.onreadystatechange = function () { + if (this.readyState === 4 && this.status === 200) { + const data = JSON.parse(this.responseText); + + const target = document.querySelector('[data-typo3-template-id=' + data.templateId + ']'); + const templateRow = createTableRow(data.template); + templateRow.id = data.templateId; + target.parentNode.parentNode.parentNode.insertBefore(templateRow, target.parentNode.parentNode.nextSibling); + + target.removeEventListener('click', Templates.getTemplateData); + PR.prettyPrint(); // Use google code prettify to pre format the template (https://github.com/google/code-prettify) + target.addEventListener('click', Templates.toggleRow); + } + }; +}; + +Templates.toggleRow = function () { + const templateId = this.dataset.typo3TemplateId; + let row = document.getElementById(templateId); + + if (row.style.display === 'none') { + row.style.display = 'table-row'; + } else { + row.style.display = 'none'; + } +}; + +window.addEventListener('load', Templates.initialize, false); diff --git a/Resources/Public/JavaScript/vendor/prettify.js b/Resources/Public/JavaScript/vendor/prettify.js new file mode 100644 index 0000000..477f03d --- /dev/null +++ b/Resources/Public/JavaScript/vendor/prettify.js @@ -0,0 +1,46 @@ +!function(){/* + + Copyright (C) 2006 Google Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +"undefined"!==typeof window&&(window.PR_SHOULD_USE_CONTINUATION=!0); +(function(){function T(a){function d(e){var a=e.charCodeAt(0);if(92!==a)return a;var c=e.charAt(1);return(a=w[c])?a:"0"<=c&&"7">=c?parseInt(e.substring(1),8):"u"===c||"x"===c?parseInt(e.substring(2),16):e.charCodeAt(1)}function f(e){if(32>e)return(16>e?"\\x0":"\\x")+e.toString(16);e=String.fromCharCode(e);return"\\"===e||"-"===e||"]"===e||"^"===e?"\\"+e:e}function c(e){var c=e.substring(1,e.length-1).match(RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g")); +e=[];var a="^"===c[0],b=["["];a&&b.push("^");for(var a=a?1:0,g=c.length;ak||122k||90k||122h[0]&&(h[1]+1>h[0]&&b.push("-"),b.push(f(h[1])));b.push("]");return b.join("")}function m(e){for(var a=e.source.match(RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g")),b=a.length,d=[],g=0,h=0;g/,null])):d.push(["com",/^#[^\r\n]*/,null,"#"]));a.cStyleComments&&(f.push(["com",/^\/\/[^\r\n]*/,null]),f.push(["com",/^\/\*[\s\S]*?(?:\*\/|$)/,null]));if(c=a.regexLiterals){var m=(c=1|\\/=?|::?|<>?>?=?|,|;|\\?|@|\\[|~|{|\\^\\^?=?|\\|\\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*("+ +("/(?=[^/*"+c+"])(?:[^/\\x5B\\x5C"+c+"]|\\x5C"+m+"|\\x5B(?:[^\\x5C\\x5D"+c+"]|\\x5C"+m+")*(?:\\x5D|$))+/")+")")])}(c=a.types)&&f.push(["typ",c]);c=(""+a.keywords).replace(/^ | $/g,"");c.length&&f.push(["kwd",new RegExp("^(?:"+c.replace(/[\s,]+/g,"|")+")\\b"),null]);d.push(["pln",/^\s+/,null," \r\n\t\u00a0"]);c="^.[^\\s\\w.$@'\"`/\\\\]*";a.regexLiterals&&(c+="(?!s*/)");f.push(["lit",/^@[a-z_$][a-z_$@0-9]*/i,null],["typ",/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],["pln",/^[a-z_$][a-z_$@0-9]*/i, +null],["lit",/^(?:0x[a-f0-9]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+\-]?\d+)?)[a-z]*/i,null,"0123456789"],["pln",/^\\[\s\S]?/,null],["pun",new RegExp(c),null]);return G(d,f)}function L(a,d,f){function c(a){var b=a.nodeType;if(1==b&&!t.test(a.className))if("br"===a.nodeName.toLowerCase())m(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)c(a);else if((3==b||4==b)&&f){var e=a.nodeValue,d=e.match(q);d&&(b=e.substring(0,d.index),a.nodeValue=b,(e=e.substring(d.index+ +d[0].length))&&a.parentNode.insertBefore(l.createTextNode(e),a.nextSibling),m(a),b||a.parentNode.removeChild(a))}}function m(a){function c(a,b){var e=b?a.cloneNode(!1):a,k=a.parentNode;if(k){var k=c(k,1),d=a.nextSibling;k.appendChild(e);for(var f=d;f;f=d)d=f.nextSibling,k.appendChild(f)}return e}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;a=c(a.nextSibling,0);for(var e;(e=a.parentNode)&&1===e.nodeType;)a=e;b.push(a)}for(var t=/(?:^|\s)nocode(?:\s|$)/,q=/\r\n?|\n/,l=a.ownerDocument,n=l.createElement("li");a.firstChild;)n.appendChild(a.firstChild); +for(var b=[n],p=0;p=+m[1],d=/\n/g,t=a.a,q=t.length,f=0,l=a.c,n=l.length,c=0,b=a.g,p=b.length,w=0;b[p]=q;var r,e;for(e=r=0;e=h&&(c+=2);f>=k&&(w+=2)}}finally{g&&(g.style.display=a)}}catch(y){D.console&&console.log(y&&y.stack||y)}}var D="undefined"!==typeof window? +window:{},B=["break,continue,do,else,for,if,return,while"],F=[[B,"auto,case,char,const,default,double,enum,extern,float,goto,inline,int,long,register,restrict,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],H=[F,"alignas,alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,delegate,dynamic_cast,explicit,export,friend,generic,late_check,mutable,namespace,noexcept,noreturn,nullptr,property,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"], +O=[F,"abstract,assert,boolean,byte,extends,finally,final,implements,import,instanceof,interface,null,native,package,strictfp,super,synchronized,throws,transient"],P=[F,"abstract,add,alias,as,ascending,async,await,base,bool,by,byte,checked,decimal,delegate,descending,dynamic,event,finally,fixed,foreach,from,get,global,group,implicit,in,interface,internal,into,is,join,let,lock,null,object,out,override,orderby,params,partial,readonly,ref,remove,sbyte,sealed,select,set,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,value,var,virtual,where,yield"], +F=[F,"abstract,async,await,constructor,debugger,enum,eval,export,from,function,get,import,implements,instanceof,interface,let,null,of,set,undefined,var,with,yield,Infinity,NaN"],Q=[B,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],R=[B,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"], +B=[B,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],S=/^(DIR|FILE|array|vector|(de|priority_)?queue|(forward_)?list|stack|(const_)?(reverse_)?iterator|(unordered_)?(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,W=/\S/,X=x({keywords:[H,P,O,F,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",Q,R,B],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}), +I={};t(X,["default-code"]);t(G([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),"default-markup htm html mxml xhtml xml xsl".split(" "));t(G([["pln",/^[\s]+/, +null," \t\r\n"],["atv",/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],["pun",/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]); +t(G([],[["atv",/^[\s\S]+/]]),["uq.val"]);t(x({keywords:H,hashComments:!0,cStyleComments:!0,types:S}),"c cc cpp cxx cyc m".split(" "));t(x({keywords:"null,true,false"}),["json"]);t(x({keywords:P,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:S}),["cs"]);t(x({keywords:O,cStyleComments:!0}),["java"]);t(x({keywords:B,hashComments:!0,multiLineStrings:!0}),["bash","bsh","csh","sh"]);t(x({keywords:Q,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py","python"]);t(x({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END", +hashComments:!0,multiLineStrings:!0,regexLiterals:2}),["perl","pl","pm"]);t(x({keywords:R,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb","ruby"]);t(x({keywords:F,cStyleComments:!0,regexLiterals:!0}),["javascript","js","ts","typescript"]);t(x({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0, +regexLiterals:!0}),["coffee"]);t(G([],[["str",/^[\s\S]+/]]),["regex"]);var Y=D.PR={createSimpleLexer:G,registerLangHandler:t,sourceDecorator:x,PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:D.prettyPrintOne=function(a,d,f){f=f||!1;d=d||null;var c=document.createElement("div");c.innerHTML="
"+a+"
"; +c=c.firstChild;f&&L(c,f,!0);M({j:d,m:f,h:c,l:1,a:null,i:null,c:null,g:null});return c.innerHTML},prettyPrint:D.prettyPrint=function(a,d){function f(){for(var c=D.PR_SHOULD_USE_CONTINUATION?b.now()+250:Infinity;psubject = new TemplatesAjaxController(); + $this->request = new ServerRequest(); + $this->request = $this->request->withQueryParams(['templateId' => 'some id', 'requestId' => 'some id']); + } + + /** + * @test + * @param array $queryParams + * @dataProvider queryParamsDataProvider + */ + public function getDataThrowsExceptionValidatesRequiredParameters(array $queryParams): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Missing parameters, templateId and requestId need to be set.'); + $this->expectExceptionCode(1561386190); + + $this->request = $this->request->withQueryParams($queryParams); + $actual = $this->subject->getData($this->request); + + self::assertSame($queryParams, $this->request->getQueryParams()); + } + + /** + * @test + * @dataProvider moduleDataProvider + * @param $moduleData + */ + public function getDataReturnsEmptyJsonResponseIfModuleDataIsNotFoundOrTemplateRecordIsNotAnArray($moduleData): void + { + $this->mockModuleDataService($moduleData); + + $response = $this->subject->getData($this->request); + + static::assertSame(404, $response->getStatusCode()); + static::assertSame('', $response->getBody()->getContents()); + } + + /** + * @test + * @dataProvider invalidPathDataProvider + * @param string $path + */ + public function getDataReturnsEmptyJsonResponseOnInvalidPath(string $path): void + { + $moduleData = new ModuleData(); + $moduleData['templates']['some id'] = ['path' => $path]; + $this->mockModuleDataService($moduleData); + + $response = $this->subject->getData($this->request); + + static::assertSame(404, $response->getStatusCode()); + static::assertSame('', $response->getBody()->getContents()); + } + + /** + * @test + */ + public function getDataReturnsJsonResponseWithHtmlContentAsTemplate(): void + { + $path = 'EXT:adminpanel_extended/Tests/Unit/Fixtures/DummyTemplate.html'; + $moduleData = new ModuleData(); + $moduleData['templates']['some id'] = ['path' => $path]; + $this->mockModuleDataService($moduleData); + + $response = $this->subject->getData($this->request); + + static::assertSame(200, $response->getStatusCode()); + $jsonDecodedContent = json_decode($response->getBody()->getContents(), true); + static::assertArrayHasKey('templateId', $jsonDecodedContent); + static::assertArrayHasKey('template', $jsonDecodedContent); + static::assertStringContainsString('some html code', $jsonDecodedContent['template']); + } + + /** + * @return array + */ + public function queryParamsDataProvider(): array + { + return [ + 'empty query params' => [ + 'queryParams' => [], + ], + 'only template id' => [ + 'queryParams' => ['templateId' => 'some id'], + ], + 'only request id' => [ + 'queryParams' => ['requestId' => 'some id'], + ], + 'empty template id' => [ + 'queryParams' => ['templateId' => '', 'requestId' => 'some id'], + ], + 'empty request id' => [ + 'queryParams' => ['templateId' => 'some id', 'requestId' => ''], + ], + 'empty request id and template id' => [ + 'queryParams' => ['templateId' => '', 'requestId' => ''], + ], + ]; + } + + public function moduleDataProvider(): array + { + return [ + ['moduleData' => null], + ['moudleData' => new ModuleData()], + ]; + } + + /** + * @param $moduleData + */ + protected function mockModuleDataService($moduleData): void + { + $moduleDataService = $this->prophesize(ModuleDataService::class); + $moduleDataService->getModuleDataByRequestId(Templates::class, Argument::any())->willReturn($moduleData); + GeneralUtility::addInstance(ModuleDataService::class, $moduleDataService->reveal()); + } + + public function invalidPathDataProvider(): array + { + return [ + 'non existing relative template path' => ['foo'], + 'unallowed abs filename' => ['/baa'], + ]; + } +} diff --git a/Tests/Unit/Domain/Repository/UserSessionRepositoryTest.php b/Tests/Unit/Domain/Repository/UserSessionRepositoryTest.php index 1670263..2784c4e 100644 --- a/Tests/Unit/Domain/Repository/UserSessionRepositoryTest.php +++ b/Tests/Unit/Domain/Repository/UserSessionRepositoryTest.php @@ -3,6 +3,13 @@ namespace Psychomieze\AdminpanelExtended\Tests\Unit\Domain\Repository; +/* + * This file is part of the TYPO3 Adminpanel Initiative. + * + * For the full copyright and license information, please read the + * LICENSE file that was distributed with this source code. + */ + use Psychomieze\AdminpanelExtended\Domain\Repository\UserSessionRepository; use TYPO3\CMS\Core\Session\Backend\SessionBackendInterface; use TYPO3\CMS\Core\Session\SessionManager; diff --git a/Tests/Unit/Fixtures/DummyTemplate.html b/Tests/Unit/Fixtures/DummyTemplate.html new file mode 100644 index 0000000..e97820c --- /dev/null +++ b/Tests/Unit/Fixtures/DummyTemplate.html @@ -0,0 +1 @@ +some html code diff --git a/Tests/Unit/Modules/Fluid/TemplatePathsTest.php b/Tests/Unit/Modules/Fluid/TemplatePathsTest.php new file mode 100644 index 0000000..bad0f6b --- /dev/null +++ b/Tests/Unit/Modules/Fluid/TemplatePathsTest.php @@ -0,0 +1,67 @@ +subject = new TemplatePaths(); + $this->logger = $this->prophesize(Logger::class); + } + + /** + * @test + */ + public function getTemplateIdentifierShouldNotLogAnythingIfTemplateNameIsNotAString(): void + { + $this->logger->log(LogLevel::DEBUG, Argument::any(), Argument::any())->shouldNotBeCalled(); + $this->mockLogger(); + $this->subject->getTemplateIdentifier(); + } + + /** + * @test + */ + public function getTemplateIdentifierLogsPathSetBySetTemplatePathAndFileName(): void + { + $this->mockLogger(); + $this->subject->setTemplatePathAndFilename('EXT:adminpanel_extended/Resources/Private/Templates/Fluid/Templates.html'); + $foo = $this->subject->getTemplateIdentifier(); + + } + + protected function mockLogger(): void + { + $logManager = $this->prophesize(LogManager::class); + $logManager->getLogger(Argument::any())->willReturn($this->logger->reveal()); + + GeneralUtility::setSingletonInstance(LogManager::class, $logManager->reveal()); + } +} diff --git a/Tests/Unit/Modules/Fluid/TemplatesTest.php b/Tests/Unit/Modules/Fluid/TemplatesTest.php new file mode 100644 index 0000000..50686ef --- /dev/null +++ b/Tests/Unit/Modules/Fluid/TemplatesTest.php @@ -0,0 +1,111 @@ +subject = new Templates(); + } + + /** + * @test + */ + public function getIdentifier(): void + { + static::assertSame('psychomieze_fluid_templates', $this->subject->getIdentifier()); + } + + /** + * @test + */ + public function getLabel(): void + { + $languageService = $this->prophesize(LanguageService::class); + $languageService->sL('LLL:EXT:adminpanel_extended/Resources/Private/Language/locallang_fluid.xlf:submodule.templates.label') + ->willReturn('some label'); + $GLOBALS['LANG'] = $languageService->reveal(); + + self::assertSame('some label', $this->subject->getLabel()); + + unset($GLOBALS['LANG']); + } + + /** + * @test + */ + public function getCssFiles(): void + { + static::assertSame( + [ + 'EXT:adminpanel_extended/Resources/Public/Css/Templates.css', + 'EXT:adminpanel_extended/Resources/Public/Css/vendor/desert.css', + ], + $this->subject->getCssFiles() + ); + } + + /** + * @test + */ + public function getContent(): void + { + $moduleData = new ModuleData(); + $moduleData['requestId'] = 'some request id'; + + $view = $this->prophesize(StandaloneView::class); + $view->setTemplatePathAndFilename('EXT:adminpanel_extended/Resources/Private/Templates/Fluid/Templates.html')->shouldBeCalled(); + $view->assignMultiple($moduleData->getArrayCopy())->shouldBeCalled(); + $view->assign('templateDataUri', 'some url')->shouldBeCalled(); + $view->render()->willReturn('some html'); + GeneralUtility::addInstance(StandaloneView::class, $view->reveal()); + + $uriBuilder = $this->prophesize(UriBuilder::class); + GeneralUtility::setSingletonInstance(UriBuilder::class, $uriBuilder->reveal()); + + $uriBuilder->buildUriFromRoute( + 'ajax_adminPanelExtended_templateData', + ['requestId' => $moduleData['requestId']] + )->willReturn('some url'); + + $this->subject->getContent($moduleData); + } + + /** + * @test + */ + public function getJavaScriptFiles(): void + { + static::assertSame( + [ + 'EXT:adminpanel_extended/Resources/Public/JavaScript/Templates.js', + 'EXT:adminpanel_extended/Resources/Public/JavaScript/vendor/prettify.js', + ], + $this->subject->getJavaScriptFiles() + ); + } +} diff --git a/ext_localconf.php b/ext_localconf.php index 09abd6c..ae15268 100644 --- a/ext_localconf.php +++ b/ext_localconf.php @@ -39,8 +39,16 @@ function () { 'submodules' => [ 'general' => [ 'module' => \Psychomieze\AdminpanelExtended\Modules\Fluid\General::class + ], + 'templates' => [ + 'module' => \Psychomieze\AdminpanelExtended\Modules\Fluid\Templates::class, + 'after' => ['general'] ] ] ]; + + $GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Fluid\View\TemplatePaths::class] = [ + 'className' => \Psychomieze\AdminpanelExtended\Modules\Fluid\TemplatePaths::class + ]; } );