I want to be able to accept HTML from untrusted users and sanitize it so that I can safely include it in pages on my website. By this I mean that markup should not be stripped or escaped, but should be passed through essentially unchanged unless it contains dangerous tags such as <script> or <iframe>, dangerous attributes such as onload, or dangerous CSS properties such as background URLs. (Apparently some older IEs will execute javascript URLs in CSS?)
Serving the content from a different domain, enclosed in an iframe, is not a good option because there is no way to tell in advance how tall the iframe has to be so it will always look ugly for some pages.
I looked into HTML Purifier, but it looks like it doesn't support HTML5 yet. I also looked into Google Caja, but I'm looking for a solution that doesn't use scripts.
Does anyone know of a library that will accomplish this? PHP is preferred, but beggars can't be choosers.
The black listing approach puts you under upgrade pressure. So each time browsers start to support new standards you MUST draw your sanitizing tool to the same level. Such changes happen more often than you think.
White listing (which is achieved by strip_tags with well defined exceptions) of cause shrinks options for your users, but puts you on the save site.
On my own sites I have the policy to apply the black listing on pages for very trusted users (such as admins) and the whitelisting on all other pages. That sets me into the position to not put much effort into the black listing. With more mature role & permission concepts you can even fine grain your black lists and white lists.
UPDATE:
I guess you look for this:
Allow user submitted HTML in PHP
with HTMLpurifier, how to add a couple attributes to the default whitelist, e.g. 'onclick'
I got the point that strip_tags whitelists on tag level but does accept everything on attribute level. Interestingly HTMLpurifier seems to do the whitelisting on attribute level. Thanks, was a nice learning here.
You might be able to do something along the lines of:
preg_replace('/<\s*iframe\s+[^>]*>.*<\s*\/\s*iframe\s+[^>]*>/i', '', $html);
preg_replace('/<\s*script\s+[^>]*>.*<\s*\/\s*script\s+[^>]*>/i', '', $html);
preg_replace('/\s+onload\s+=\s+"[^"]+"/i', '', $html);
... but then again: you have RegExes, now you have two problems - this might remove more than wanted and leave more than wanted as well.
But since HTML Purifier is probably the most modern and well suited (and open source) project you should still use that one and maybe make adjustments if you really need them.
You can check out one of the following as well:
kses - de facto standard, found a way into wordpress as well
htmLawed - an further developed kses
PHP Input Filter - can filter tags and attributes
Though you also have to make sure that your own page layout doesn't take a hit in including the results due to not closed tags.
Maybe it's better to go on a different approach?
How about telling them what they can use?
In that case you can use use strip_tags. It will be easier and a lot more controllable this way. Very easy to extend in the future aswell
On Ruby I'm using Nokogiri (php version) to parse HTML content. You can parse user's data and remove unnecessary tags or attributes, and then convert it to text.
phpQuery - another parser.
And in PHP there is a strip_tags function.
Or you can manualy remove all attributes:
$dom = new DOMDocument;
$dom -> loadHTML( $html );
$xpath = new DOMXPath( $dom );
$nodes = $xpath -> query( "//*[#style]" ); // all elements with style attribute
foreach ( $nodes as $node ) {
// remove or do what you want
$node -> removeAttribute( "style" );
}
echo $dom -> saveHTML();
See WdHTMLParser class. I use this class for my forum.
Sample with WdHTMLParser :
This class parse the HTML to an array :
<div>
<span>
<br />
<span>
un bout de texte
</span>
<input type="text" />
</span>
</div>
Array :
Array (
[0] => Array (
[name] => div
[args] => Array ()
[children] => Array (
[0] => Array (
[name] => span
[args] => Array ()
[children] => Array (
[0] => Array (
[name] => br
[args] => Array ()
)
[1] => Array (
[name] => span
[args] => Array ()
[children] => Array (
[0] => un bout de texte
)
)
[2] => Array (
[name] => input
[args] => Array (
[type] => text
)
)
)
)
)
)
)
WdHTMLParser array to HTML
I use this class on my website to convert array to HTML.
voyageWdHTML_allowattr : These attributes will be allowed.
voyageWdHTML_allowtag : These tags will be allowed.
voyageWdHTML_special : Make your own rules. Actually, I add "_blank" to each link. And replace <br> to new line (\n) in pre tag.
fix_javascript : You can to enable/disable this function, but it is useless.
Sample php :
<?php
include "WdHTMLParser.php";
include "parser.php";
list($erreur, $message) = (new Parser())->parseBadHTML("<div>
<span>
<a onclick=\"alert('Hacked ! :'(');\">Check javascript</a>
<script>alert(\"lol\");</script>
</span>
</div>");
if ($erreur) {
die("Error : ".$message);
}
echo $message;
Output :
<div>
<span>
<a target="_blank">Check javascript</a>
<pre>alert("lol");</pre>
</span>
</div>
My Parser class :
<?php
class Parser {
//private function fix_javascript(&$message) { }
private function voyageWdHTML_args($tab_args, $objname) {
$html = "";
foreach ($tab_args as $attr => $valeur) {
if ($valeur !== null && $this->voyageWdHTML_allowattr($attr)) {
$html .= " $attr=\"".htmlentities($valeur)."\"";
}
}
return $html;
}
private function voyageWdHTML_allowattr($attr) {
return in_array($attr, array("align", "face", "size", "href", "title", "target", "src", "color", "style",
"data-class", "data-format"));
}
private function voyageWdHTML_allowtag($name) {
return in_array($name, array("br", "b", "i", "u", "strike", "sub", "sup", "div", "ol", "ul", "li", "font", "span", "code",
"hr", "blockquote", "cite", "a", "img", "p", "pre", "h6", "h5", "h4", "h3", "h2", "h1"));
}
private function voyageWdHTML_special(&$obj) {
if ($obj["name"] == "a") { $obj["args"]["target"] = "_blank"; }
if ($obj["name"] == "pre") {
array_filter($obj["children"], function (&$var) {
if (is_string($var)) { return true; }
if ($var["name"] == "br") { $var = "\n"; return true; }
return false;
});
}
}
private function voyageWdHTML($tableau, $lvl = 0) {
$html = "";
foreach ($tableau as $obj) {
if (is_array($obj)) {
if (!$this->voyageWdHTML_allowtag($obj["name"])) {
$obj["name"] = "pre";
if (!isset($obj["children"])) {
$obj["children"] = array();
}
}
if (isset($obj["children"])) {
$this->voyageWdHTML_special($obj);
$html .= "<{$obj["name"]}{$this->voyageWdHTML_args($obj["args"], $obj["name"])}>{$this->voyageWdHTML($obj["children"], $lvl+1)}</{$obj["name"]}>";
} else {
$html .= "<{$obj["name"]}>";
}
} else {
$html .= $obj;
}
}
return $html;
}
public function parseBadHTML($message) {
$WdHTMLParser = new WdHTMLParser();
$message = str_replace(array("<br>", "<hr>"), array("<br/>", "<hr/>"), $message);
$tableau = $WdHTMLParser->parse($message);
if ($WdHTMLParser->malformed) {
$retour = $WdHTMLParser->error;
} else {
$retour = $this->voyageWdHTML($tableau);
//$this->fix_javascript($retour);// To make sur
}
return array($WdHTMLParser->malformed, $retour);
}
}
WdHTMLParser class
<?php
class WdHTMLParser {
private $encoding;
private $matches;
private $escaped;
private $opened = array();
public $malformed;
public function parse($html, $namespace = NULL, $encoding = 'utf-8') {
$this->malformed = false;
$this->encoding = $encoding;
$html = $this->escapeSpecials($html);
$this->matches = preg_split('#<(/?)' . $namespace . '([^>]*)>#', $html, -1, PREG_SPLIT_DELIM_CAPTURE);
$tree = $this->buildTree();
if ($this->escaped) {
$tree = $this->unescapeSpecials($tree);
}
return $tree;
}
private function escapeSpecials($html) {
$html = preg_replace_callback('#<\!--.+-->#sU', array($this, 'escapeSpecials_callback'), $html);
$html = preg_replace_callback('#<\?.+\?>#sU', array($this, 'escapeSpecials_callback'), $html);
return $html;
}
private function escapeSpecials_callback($m) {
$this->escaped = true;
$text = $m[0];
$text = str_replace(array('<', '>'), array("\x01", "\x02"), $text);
return $text;
}
private function unescapeSpecials($tree) {
return is_array($tree) ? array_map(array($this, 'unescapeSpecials'), $tree) : str_replace(array("\x01", "\x02"), array('<', '>'), $tree);
}
private function buildTree() {
$nodes = array();
$i = 0;
$text = NULL;
while (($value = array_shift($this->matches)) !== NULL) {
switch ($i++ % 3) {
case 0: {
if (trim($value)) {
$nodes[] = $value;
}
}
break;
case 1: {
$closing = ($value == '/');
}
break;
case 2: {
if (substr($value, -1, 1) == '/') {
$nodes[] = $this->parseMarkup(substr($value, 0, -1));
} else if ($closing) {
$open = array_pop($this->opened);
if ($value != $open) {
$this->error($value, $open);
}
return $nodes;
} else {
$node = $this->parseMarkup($value);
$this->opened[] = $node['name'];
$node['children'] = $this->buildTree($this->matches);
$nodes[] = $node;
}
}
}
}
return $nodes;
}
public function parseMarkup($markup) {
preg_match('#^[^\s]+#', $markup, $matches);
$name = $matches[0];
preg_match_all('#\s+([^=]+)\s*=\s*"([^"]+)"#', $markup, $matches, PREG_SET_ORDER);
$args = array();
foreach ($matches as $m) {
$args[$m[1]] = html_entity_decode($m[2], ENT_QUOTES, $this->encoding);
}
return array('name' => $name, 'args' => $args);
}
public function error($markup, $expected) {
$this->malformed = true;
printf('unexpected closing markup "%s", should be "%s"', $markup, $expected);
}
}
To make sur use, you can use this function (mybb.com) :
<?php
class Parser {
private function fix_javascript(&$message) {
$js_array = array(
"#(&\#(0*)106;?|&\#(0*)74;?|&\#x(0*)4a;?|&\#x(0*)6a;?|j)((&\#(0*)97;?|&\#(0*)65;?|a)(&\#(0*)118;?|&\#(0*)86;?|v)(&\#(0*)97;?|&\#(0*)65;?|a)(\s)?(&\#(0*)115;?|&\#(0*)83;?|s)(&\#(0*)99;?|&\#(0*)67;?|c)(&\#(0*)114;?|&\#(0*)82;?|r)(&\#(0*)105;?|&\#(0*)73;?|i)(&\#112;?|&\#(0*)80;?|p)(&\#(0*)116;?|&\#(0*)84;?|t)(&\#(0*)58;?|\:))#i",
"#(o)(nmouseover\s?=)#i",
"#(o)(nmouseout\s?=)#i",
"#(o)(nmousedown\s?=)#i",
"#(o)(nmousemove\s?=)#i",
"#(o)(nmouseup\s?=)#i",
"#(o)(nclick\s?=)#i",
"#(o)(ndblclick\s?=)#i",
"#(o)(nload\s?=)#i",
"#(o)(nsubmit\s?=)#i",
"#(o)(nblur\s?=)#i",
"#(o)(nchange\s?=)#i",
"#(o)(nfocus\s?=)#i",
"#(o)(nselect\s?=)#i",
"#(o)(nunload\s?=)#i",
"#(o)(nkeypress\s?=)#i"
);
$message = preg_replace($js_array, "$1<b></b>$2$4", $message);
}
}
I decided to just use html5lib-python. This is what I came up with:
#!/usr/bin/env python
import sys
from xml.dom.minidom import Node
import html5lib
from html5lib import (HTMLParser, sanitizer, serializer, treebuilders,
treewalkers)
parser = HTMLParser(tokenizer=sanitizer.HTMLSanitizer,
tree=treebuilders.getTreeBuilder("dom"))
serializer = serializer.htmlserializer.HTMLSerializer(omit_optional_tags=False)
document = parser.parse(sys.stdin.read(), encoding="utf-8")
# find the <html> node
for child in document.childNodes:
if child.nodeType == Node.ELEMENT_NODE and child.nodeName == 'html':
htmlNode = child
# find the <body> node
for child in htmlNode.childNodes:
if child.nodeType == Node.ELEMENT_NODE and child.nodeName == 'body':
bodyNode = child
# serialize all children of the <body> node
for child in bodyNode.childNodes:
stream = treewalkers.getTreeWalker("dom")(child)
sys.stdout.write(serializer.render(stream, encoding="utf-8"))
Example input:
<script>alert("hax")</script>
<p onload="alert('this is a dangerous attribute')"><b>hello,</b> world</p>
Example output:
<script>alert("hax")</script>
<p><b>hello,</b> world</p>
I personally use HTML Purifier for this exact purpose:
http://htmlpurifier.org/docs
It works well and allows you to customize down to every tag and attribute. So far I have had no security issues with this plugin.
Related
I try to return as json added attribute which I get with the following method in my User model but I keep getting
"message": "Malformed UTF-8 characters, possibly incorrectly encoded",
"exception": "InvalidArgumentException",
"file": "/var/www/timetool/vendor/laravel/framework/src/Illuminate/Http/JsonResponse.php",
the code
/**
* #return string
*/
public function getAvatarImageAttribute($value)
{
if($this->hasMedia('avatar')) {
$image = $this->getMedia('avatar');
$img = \Intervention\Image\ImageManagerStatic::make($image[0]->getPath())->encode('data-url');
}
elseif (isset($this->blob->dokument)) {
$img = 'data:image/jpeg;base64,'. base64_encode($this->blob->document);
} else {
$img = '';
}
return $img;
}
in controller I have
return \Response::json($users, 200, array('Content-Type' => 'application/json;charset=utf8'), JSON_UNESCAPED_UNICODE);
I'm thinking it's related to JSON needing only UTF8 chars and your blob may have invalid chars. Try utf8_encode($img). http://at2.php.net/manual/en/function.utf8-encode.php
In your controller just return. Laravel will build a proper json response for you.
Paste this function top of your document:
public static function convert_from_latin1_to_utf8_recursively($dat)
{
if (is_string($dat)) {
return utf8_encode($dat);
} elseif (is_array($dat)) {
$ret = [];
foreach ($dat as $i => $d) $ret[ $i ] = self::convert_from_latin1_to_utf8_recursively($d);
return $ret;
} elseif (is_object($dat)) {
foreach ($dat as $i => $d) $dat->$i = self::convert_from_latin1_to_utf8_recursively($d);
return $dat;
} else {
return $dat;
}
}
Call the above function to convert the content. It has a parameter just it need the value of blob image (binary):
$img = $this->convert_from_latin1_to_utf8_recursively($this->blob->document)
In my case the problem was the encoding of the controller. The solution was to convert it to UTF8 and the bug was fixed.
I picked up this ZF2AuthAcl module to make my life easier. For some reason it does not work out of the box. As soon as i activate it in Zend2 Application.config it takes over the whole site. Meaning it goes straight to login on any page i have. There is a "white list" and i tried to add pages to this in an array and it does not seem to work. I will show the Acl page that it has with the "white list" maybe i did not add them correctly or there is a better way. It is data driven also. Has anyone used this with success or know about it?
The author is the one who told me it probably has to do with the white list.
The area that i added to looked like this:
public function initAcl()
{
$this->roles = $this->_getAllRoles();
$this->resources = $this->_getAllResources();
$this->rolePermission = $this->_getRolePermissions();
// we are not putting these resource & permission in table bcz it is
// common to all user
$this->commonPermission = array(
'ZF2AuthAcl\Controller\Index' => array(
'logout',
'index'
),
);
$this->_addRoles()
->_addResources()
->_addRoleResources();
}
This is the whole thing with parts i added.
namespace ZF2AuthAcl\Utility;
use Zend\Permissions\Acl\Acl as ZendAcl;
use Zend\Permissions\Acl\Role\GenericRole as Role;
use Zend\Permissions\Acl\Resource\GenericResource as Resource;
use Zend\ServiceManager\ServiceLocatorAwareInterface;
use Zend\ServiceManager\ServiceLocatorInterface;
class Acl extends ZendAcl implements ServiceLocatorAwareInterface
{
const DEFAULT_ROLE = 'guest';
protected $_roleTableObject;
protected $serviceLocator;
protected $roles;
protected $permissions;
protected $resources;
protected $rolePermission;
protected $commonPermission;
public function setServiceLocator(ServiceLocatorInterface $serviceLocator)
{
$this->serviceLocator = $serviceLocator;
return $this;
}
public function getServiceLocator()
{
return $this->serviceLocator;
}
public function initAcl()
{
$this->roles = $this->_getAllRoles();
$this->resources = $this->_getAllResources();
$this->rolePermission = $this->_getRolePermissions();
// we are not putting these resource & permission in table bcz it is
// common to all user
$this->commonPermission = array(
'ZF2AuthAcl\Controller\Index' => array(
'logout',
'index'
),
'Frontend\Controller\Index' => array(
'index'
),
'Blog\Controller\Blog' => array(
'blog',
'list',
'view',
'UsMap',
'maps'
)
);
$this->_addRoles()
->_addResources()
->_addRoleResources();
}
public function isAccessAllowed($role, $resource, $permission)
{
if (! $this->hasResource($resource)) {
return false;
}
if ($this->isAllowed($role, $resource, $permission)) {
return true;
}
return false;
}
protected function _addRoles()
{
$this->addRole(new Role(self::DEFAULT_ROLE));
if (! empty($this->roles)) {
foreach ($this->roles as $role) {
$roleName = $role['role_name'];
if (! $this->hasRole($roleName)) {
$this->addRole(new Role($roleName), self::DEFAULT_ROLE);
}
}
}
return $this;
}
protected function _addResources()
{
if (! empty($this->resources)) {
foreach ($this->resources as $resource) {
if (! $this->hasResource($resource['resource_name'])) {
$this->addResource(new Resource($resource['resource_name']));
}
}
}
// add common resources
if (! empty($this->commonPermission)) {
foreach ($this->commonPermission as $resource => $permissions) {
if (! $this->hasResource($resource)) {
$this->addResource(new Resource($resource));
}
}
}
return $this;
}
protected function _addRoleResources()
{
// allow common resource/permission to guest user
if (! empty($this->commonPermission)) {
foreach ($this->commonPermission as $resource => $permissions) {
foreach ($permissions as $permission) {
$this->allow(self::DEFAULT_ROLE, $resource, $permission);
}
}
}
if (! empty($this->rolePermission)) {
foreach ($this->rolePermission as $rolePermissions) {
$this->allow($rolePermissions['role_name'], $rolePermissions['resource_name'], $rolePermissions['permission_name']);
}
}
return $this;
}
protected function _getAllRoles()
{
$roleTable = $this->getServiceLocator()->get("RoleTable");
return $roleTable->getUserRoles();
}
protected function _getAllResources()
{
$resourceTable = $this->getServiceLocator()->get("ResourceTable");
return $resourceTable->getAllResources();
}
protected function _getRolePermissions()
{
$rolePermissionTable = $this->getServiceLocator()->get("RolePermissionTable");
return $rolePermissionTable->getRolePermissions();
}
private function debugAcl($role, $resource, $permission)
{
echo 'Role:-' . $role . '==>' . $resource . '\\' . $permission . '<br/>';
}
}
06/10/2016 Additional information
I have also found that this ACL page is not in any of the pages in the module. The functions are not called out anywhere in any page nor is it "use" on any page. So how is it supposed to work?
Update 06/10/2017 - Area that has been fixed.
I have found where this is used in the module.php there is a whitelist that the pages have to be added too. Below is where you add them.
$whiteList = array(
'Frontend\Controller\Index-index',
*Add whatever modules/controller/action you do not want included*
'ZF2AuthAcl\Controller\Index-index',
'ZF2AuthAcl\Controller\Index-logout'
);
Above is the conclusion of my issue. I stumbled upon it. I did not look in the module.php file. That is where the answer was.
Here is a general implementation of Zend ACL. I followed this one. If you wish you can follow this one too.
Create a file named module.acl.php in the config/ folder of your module. This file contains configuration for roles and permissions. Modify this script as you need.
ModuleName/config/module.acl.php
return array(
'roles' => array(
'guest',
'member'
),
'permissions' => array(
'guest' => array(
// Names of routes for guest role
'users-signup',
'users-login'
),
'member' => array(
// Names of routes for member role
// Add more here if you need
'users-logout'
)
)
);
You need to import the following three classes and define and initialize some methods in the Module.php.
ModuleName/Module.php
use Zend\Permissions\Acl\Acl;
use Zend\Permissions\Acl\Role\GenericRole;
use Zend\Permissions\Acl\Resource\GenericResource;
// Optional; use this for authentication
use Zend\Authentication\AuthenticationService;
Now lets create methods that will deploy ACL and check roles and permissions.
Module::initAcl()
public function initAcl(MvcEvent $e)
{
// Set the ACL
if ($e->getViewModel()->acl == null) {
$acl = new Acl();
} else {
$acl = $e->getViewModel()->acl;
}
// Get the roles and permissions configuration
// You may fetch configuration from database instead.
$aclConfig = include __DIR__ . '/config/module.acl.php';
// Set roles
foreach ($aclConfig['roles'] as $role) {
if (!$acl->hasRole($role)) {
$role = new GenericRole($role);
$acl->addRole($role);
} else {
$role = $acl->getRole($role);
}
// Set resources
if (array_key_exists($role->getRoleId(), $aclConfig['permissions'])) {
foreach ($aclConfig['permissions'][$role->getRoleId()] as $resource) {
if (!$acl->hasResource($resource)) {
$acl->addResource(new GenericResource($resource));
}
// Add role to a specific resource
$acl->allow($role, $resource);
}
}
}
// Assign the fully prepared ACL object
$e->getViewModel()->acl = $acl;
}
Module::checkAcl()
public function checkAcl(MvcEvent $e) {
// Get the route
$route = $e->getRouteMatch()->getMatchedRouteName();
// Use this if you have authentication set
// Otherwise, take this off
$auth = new AuthenticationService();
// Set role as you need
$userRole = 'guest';
// Use this if you have authentication set
// Otherwise, take this off
if ($auth->hasIdentity()) {
$userRole = 'member';
$loggedInUser = $auth->getIdentity();
$e->getViewModel()->loggedInUser = $loggedInUser;
}
// Check if the resource has right permission
if (!$e->getViewModel()->acl->isAllowed($userRole, $route)) {
$response = $e->getResponse();
// Redirect to specific route
$response->getHeaders()->addHeaderLine('Location', $e->getRequest()->getBaseUrl() . '/404');
$response->setStatusCode(404);
return;
}
}
Now call those above methods on the onBootstrap() method in your Module.php. Initialize Module::initAcl() and check resource permission by adding Module::checkAcl() to the route event.
Module::onBootstrap()
public function onBootstrap(MvcEvent $e)
{
$this->initAcl($e);
$e->getApplication()->getEventManager()->attach('route', array($this, 'checkAcl'));
}
Let us know it helps you or not!
Is it possible in Yii2 to retrieve an array containing all controllers and actions for the whole application?
I finally ended up with:
protected function actionGetcontrollersandactions()
{
$controllerlist = [];
if ($handle = opendir('../controllers')) {
while (false !== ($file = readdir($handle))) {
if ($file != "." && $file != ".." && substr($file, strrpos($file, '.') - 10) == 'Controller.php') {
$controllerlist[] = $file;
}
}
closedir($handle);
}
asort($controllerlist);
$fulllist = [];
foreach ($controllerlist as $controller):
$handle = fopen('../controllers/' . $controller, "r");
if ($handle) {
while (($line = fgets($handle)) !== false) {
if (preg_match('/public function action(.*?)\(/', $line, $display)):
if (strlen($display[1]) > 2):
$fulllist[substr($controller, 0, -4)][] = strtolower($display[1]);
endif;
endif;
}
}
fclose($handle);
endforeach;
return $fulllist;
}
I started with the answer from Andreas Hinderberger, and just fine tuned it. I ended up with something like this:
It uses FileHelper to get all the files recursively, which is useful if you are extending controllers from base classes. It also formats the controller-id/action-id using Inflector::camel2id so they will match your routes.
public function getAllControllerActions()
{
$controllers = \yii\helpers\FileHelper::findFiles(Yii::getAlias('#app/controllers'), ['recursive' => true]);
$actions = [];
foreach ($controllers as $controller) {
$contents = file_get_contents($controller);
$controllerId = Inflector::camel2id(substr(basename($controller), 0, -14));
preg_match_all('/public function action(\w+?)\(/', $contents, $result);
foreach ($result[1] as $action) {
$actionId = Inflector::camel2id($action);
$route = $controllerId . '/' . $actionId;
$actions[$route] = $route;
}
}
asort($actions);
return $actions;
}
As far as I know Yii 2 doesn't have any built-in methods to achieve this. You can only get the current controller and its action.
What's the purpose of it? If you really need this you can write such functionality by yourself.
To get all controllers you should search for a files ending with Conroller. And they can be located in different places of application. For example in nested folders, modules, nested modules, etc. So there is more than just one place for search.
To get all actions you should search for all methods prefixed with action in each controller.
Also don't forget about attached actions in controller actions() method. In framework they are usually ending with Action, take a look for example at rest actions. But no one forces you to name it like that, so there is possibility that some external actions can just have different naming convention (for example if you working in team and don't follow this convention).
And you probably need to exclude such folders as vendor.
So it's not trivial task, but possible with some inaccuracies. I just don't get it what's the point of that.
Follow example walk trought all modules and collect all modules controller actions (no tested):
<?php
$controllerDirs = [];
$controllerDirs[] = \Yii::getAlias('#app/controllers');
if ($commonControllerDir = \Yii::getAlias('#common/controllers', false)) {
$controllerDirs['common'] = $commonControllerDir;
}
foreach (\Yii::$app->modules as $moduleId => $module) {
/*
* get module base path
*/
if (method_exists($module, 'getBasePath')) {
$basePath = $module->getBasePath();
} else {
$reflector = new \ReflectionClass($module['class']);
$basePath = StringHelper::dirname($reflector->getFileName());
}
$basePath .= '/controllers';
$controllerDirs[$moduleId] = $basePath;
}
$actions = [];
foreach ($controllerDirs as $moduleId => $cDir) {
$actions[$moduleId][$cDir] = actionGetcontrollersandactions($cDir);
}
print_r($actions);
function actionGetcontrollersandactions($controllerDir) {
$controllerlist = [];
if ($handle = opendir($controllerDir)) {
while (false !== ($file = readdir($handle))) {
if ($file != "." && $file != ".." && substr($file, strrpos($file, '.') - 10) == 'Controller.php') {
$controllerlist[] = $file;
}
}
closedir($handle);
}
asort($controllerlist);
$fulllist = [];
foreach ($controllerlist as $controller):
$handle = fopen($controllerDir . '/' . $controller, "r");
if ($handle) {
while (($line = fgets($handle)) !== false) {
if (preg_match('/public function action(.*?)\(/', $line, $display)):
if (strlen($display[1]) > 2):
$fulllist[substr($controller, 0, -4)][] = strtolower($display[1]);
endif;
endif;
}
}
fclose($handle);
endforeach;
return $fulllist;
}
I cannot for the life of me work out why
$xml['interaction']['twitteraccount'] = 'hello';
Causes my JSON output to render as HTML rather than JSON. I've tried all options and played around for a while. Surely I'm missing something? As soon as I take that line out again, it renders as JSON!
public function lifestream()
{
$this->RequestHandler->setContent('json', 'application/json' );
$this->set('interactions', $this->Interaction->find('all'));
$xmlArray = array();
foreach($this->Interaction->find('all') as $interaction) {
$sourceexploded = explode("/",$interaction['Interaction']['source']);
if($sourceexploded[0] == "twitter") {
$xml['interaction']['source'] = $sourceexploded[0];
$xml['interaction']['twitteraccount'] = 'hello';
} else {
$xml['interaction']['source'] = $interaction['Interaction']['source'];
}
$xml['interaction']['timestamp'] = $interaction['Interaction']['timestamp'];
$xml['interaction']['receivedfrom'] = $interaction['Interaction']['receivedfrom'];
$xmlArray[] = $xml;
}
echo json_encode($xmlArray);
You have to use the JsonView.
In your route.php write: Router::parseExtensions('json');
In your controller you have to set the RequestHandler Component.
class SomeNameController{
public $components = array('RequestHandler');
public function lifestream(){
$this->RequestHandler->setContent('json', 'application/json' );
$this->set('interactions', $this->Interaction->find('all'));
$xmlArray = array();
foreach($this->Interaction->find('all') as $interaction) {
/* do stuff */
$xmlArray[] = $xml;
}
$this->set('data', $xmlArray);
$this->set('_serialize', array(
'data',
));
}
}
Try to go on "samename/lifestream.json" now or make an HTTP request with "Content-Type: application/json".
Look at : http://book.cakephp.org/2.0/en/views/json-and-xml-views.html
add this 2 line of code:
$this->layout = 'ajax';
$this->autoRender = false;
The first line instructs the render to use an empty layout called ajax (you can find it on Views/Layouts/ajax.ctp
The second one instruct the render to not look for an view template (ctp file)
Then when you echo the json_encode it will rendered as xml
i would like to output dynamic generated html content instead of the translatable message but i can't make it work:
function custom_logo_module_block_view($delta = '') {
// don't worry about switch($delta) logic
// perform some operations and then display some generated html
// (maybe use the template(...) function)
// works fine but i'd like to print html
$block['content'] = t('No content available.');
return $block;
}
how can i print out generated html into a block?
i can't find any solutions or code examples. i think i might be pointing towards the wrong direction so best practice suggestions are welcome.
function custom_logo_module_block_view($delta = '') {
$block = array();
if ($delta == 'example') {
$block = array(
'subject' => t('Active users list'),
'content' => example_block_content()
);
}
return $block;
}
function example_block_content() {
// Query for active users from database
$users = db_select('users', 'u')
->fields('u', array('uid', 'name'))
->condition('u.status', 1)
->execute()
->fetchAll();
// Prepare items for item list
$items = array();
foreach ($users as $user) {
$items[] = l($user->name, "user/{$user->uid}");
}
$output = t('No active users available.');
if (!empty($items)) {
$output = theme('item_list', array('items' => $items));
}
return $output;
}
Update regarding your comments...
As far as I understand by some result you mean generated data from database. In this case you can try something like this:
function example_block_content() {
// Query for active users from database
$users = db_select('users', 'u')
->fields('u', array('uid', 'name'))
->condition('u.status', 1)
->execute()
->fetchAll();
$output = '';
foreach ($users as $user) {
$output.= '<div>'. $user->name .'</div>';
}
$output = "<div>Hello World". $output ."</div>";
return $output;
}
This will give you the following output:
<div>Hello World
<div>admin</div>
<div>ndrizza</div>
<div>Vlad Stratulat</div>
...
</div>
Or you can try:
function custom_logo_module_block_view($delta = '') {
$block = array();
if ($delta == 'example') {
$block = array(
'subject' => t('Active users list'),
// this will return "Hello World + some result" text inside <div>
'content' => "<div>Hello World + some result</div>"
);
}
return $block;
}
Both of this ways are working but they are not the right ways. The right way to generate content is in my first answer. Read more about theming in Drupal.