Thursday, December 31, 2009

Implement Zend ACL with database backend for our CMS

Possibly a better and purhaps more proper way to use Zend ACL in my CMS would be to
treat every user as a special 'role' and use groups as roles, and allow user to belong to multiple groups.

Then create a site-wide ACL first and store it in cache
Then when authorization is required, add the userObject to the ACL object, passing the array of his groups as second arg, like this:

$parents = array('guest', 'member', 'admin');
$acl->addRole($oUser, $parents); //
/*

$oUser is basically the $this->oViewer and must implement Zend_Acl_Role
*/

So now a user is added to the ACL, and even though he may now have any permissions
set directly on his object, he would get permissions from being member of
groups.

Another possibility is to NOT use user account as role, and only use user's primary
group_id as Role (the getRoleId() in user object will return the group_id)
And pass the array of other groups if user has any secondary groups or null

Which one is more flexible?
First of all I must verify that it's possible to set permissions to ACL before
the Role has been added because we will not be adding individual user to the ACL
at the time ACL is created, buy only at a time we need to check permissions when
we will be using just an instance of the ACL.

Update: It is definetely NOT POSSIBLE to add rule to ACL on a role that has not
been added yet! Registry::get() with throw an exception! This means we cannot just add rule for some specific userID before
we actually add that user (as object) to the ACL, which is not convenient at all!

Must examine the Zend ACL class for that - is it possible to set allow() or deny()
on Role before Role object has been added?

If that is too messy, then we can definitely use only groups for permission management
and if a user needs special permission then either use custom Accertion class OR
create a special custom group just for that user, set permissions on a group
and add user to that group. This looks much cleaner and probably more
easier to understand.

So groups should be Everyone, Guest, Unvalidated, Registered, Moderator, SuperModerator, Administrator

Where every group inherits from Everyone, then Guest may not even be needed,
what is the difference of guest and Everyone anyway?
Then if a special group is needed it may be created and set to inherit from
Members then special permissions added to it or explicit deny is added to it.

Then either an extra table will be required USER2GROUP to store
user to group mapping OR store all user groupIDs in USERS.groups field as comma-separated value
This may be somewhat not clean and may be difficult to set the correct order
and order of groups is important in ACL.

Another possibility is that by default every user belongs to just 1 group but
USER2GROUP will store ONLY secondary groups for some users that need them.

This will allow us to keep the USERS table as is, while still allowing users
to be added to extra groups if needed. Also we do have a user_type field in USERS
so we can use that for assertion test. We can also use userID for
accertion test if needed to check individual userID permission or
to check that user have not been deleted.

Having it setup this way - USER2GROUPS table for some users will require to get
the array from USER2GROUPS for userID at the time we need to check the permission
since the oViewer object only contains 1 group - default one per user. We can get it via cache
like u2g_$userID key, that's easy enough but still something is not 'beautiful' about it.

Another possibility - to allow pre-defined number of groups per user, say only 2 or 3
and then we can store then all in USERS table in 3 fields.

And another one - just stick to one group per user and when we need special permissions
when one group is just not enough we can use assertion and use userID

For example: to moderate specific forum a user must be in moderators group
AND be added to forum moderators for this one particular forum.

We can't possibly create a new group for every forum, that would be stupid
to have 40 extra usergroups and then if we want a user to be able to moderate
15 forums then we would have to add him to 15 extra groups. That's too much.

But with accertion that's easy, just pass oViewer to accertion, it will extract
userID and checks if it exists in array of users who can moderate that one forum.

Accertions can also easily be used to allow only 'Friends' of a user to add comments
or rate pics or something like that.

So, in conclusion: we start with one user = one group. The User implements the Acl_Role and getRoleId returns
the groupID (maybe with prefix like 'g2' for group2

but that may not be necessary, just cast groupID to string and we should be fine.

If at any time we would need the feature where user can belong to multiple groups
we can then add an extra table like USER2EXTRA_GROUP and then deal with it OR
go with comma separated groups list in USERS or even serialized array of groups in groups
which would searching by group_id impossible by the way.....

Or maybe we can have some clever mysql select trick that would return
comma-separate list of groups.

Anyway, for now just one man one vote (one group that is)
And use Assertions when just groupID is not enough.

Also need to decide in how to store permissions - probably in
unum type of field with possible values ALLOW,DENY,NONE where none is the default
meaning no permission.

Setting of CacheControl headers on apache

Cache Control with mod_expires and mod_headers

For Apache, mod_expires and mod_headers handle cache control through HTTP headers sent from the server. Since they are not installed by default, have your server administrator install them for you. For Apache/1.3x, enable the expires and headers modules by adding the following lines to your httpd.conf configuration file.

LoadModule expires_module libexec/mod_expires.so
LoadModule headers_module libexec/mod_headers.so

AddModule mod_expires.c
AddModule mod_headers.c
...
AddModule mod_gzip.c

Note that the load order is important in Apache/1.3x, mod_gzip must load last, after all other modules.

For Apache/2.0, enable the modules in your httpd.conf file like this.

LoadModule expires_module modules/mod_expires.so
LoadModule headers_module modules/mod_headers.so
LoadModule deflate_module modules/mod_deflate.so

mod_deflate is the native compression module in Apache/2.0 (although mod_gzip does a better job of handling wayward browsers). In this case, the load order does not matter, as Apache/2.0 handles this for you.
Target Files by Extension for Caching

One quick way to enable cache control headers for existing sites is to target files by extension. Although this method has some disadvantages (notably the requirement of file extensions), it has the virtue of simplicity. To turn on mod_expires set ExpiresActive to on.

ExpiresActive On

Next target your website's root HTML directory to enable caching for your site in one fell swoop.

<Directory "/home/website/public_html">
Options FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
Allow from all
ExpiresDefault A300
<FilesMatch "\.html$">
Expires A86400
</FilesMatch>
<FilesMatch "\.(gif|jpg|png|js|css)$">
Expires A2592000
</FilesMatch>
</Directory>

ExpiresDefault A300 sets the default expiry time to 300 seconds after access (A). Using M300 would set the expiry time to 300 seconds after file modification. The FilesMatch segment sets the cache-control header for all .html files to 86400 seconds (1 day). The second FilesMatch section sets the cache-control header for all images, external JavaScripts and CSS files to 2592000 seconds (30 days).

Note that you can target your files with more granularity using multiple directory sections, like this:

<Directory "/home/website/public_html/images/logos/">

For truly dynamic content you can force resources to not be cached by setting an age of zero seconds and to not store the resource anywhere.

<Directory "/home/website/cgi-bin/">
Header Set Cache-Control "max-age=0, no-store"
</Directory>

Target Files by MIME Type

The disadvantage of the above method is the reliance on the existence of file extensions. In some cases webmasters elect to use extensionless URLs for portability and performance (see Rewrite URLs with Content Negotiation). A better method is to use the ExpiresByType command of the mod_expires module. As the name implies, ExpiresByType targets resources for caching by MIME type, like this.

ExpiresActive On
ExpiresDefault "access plus 300 seconds"

<Directory "/home/website/public_html">
Options FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
Allow from all
ExpiresByType text/html "access plus 1 day"
ExpiresByType text/css "access plus 1 day"
ExpiresByType text/javascript "access plus 1 day"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/jpg "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType application/x-shockwave-flash "access plus 1 day"
</Directory>



<FilesMatch "\.(ico|pdf|flv|jpg|jpeg|png|gif|js|css|swf)$">
Header set Expires "Mon, 22 Dec 2019 10:00:00 GMT"
</FilesMatch>

Friday, December 25, 2009

Charset of input string for DOMDocument loadHTML()

It's very important to tell DOMDocument which charset the input string is encoded.

In many cases I know for sure that the string is in utf-8, but unless I tell this somehow to DOMDocument during loadHTML()
it will do bad things to my string.

This is how to correctly load the html fragment - by appending the full doctype and most importantly the meta with charset info:

$sHtml = '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"
"http://www.w3.org/TR/REC-html40/loose.dtd">
<head>
<meta equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body><div>'.$sHtml.'</div></body></html>';

$ER = error_reporting(0);
if(false === @$oDom->loadHTML($sHtml)){
throw new LampcmsDevException('Error. Unable to load html string: '.$sHtml);
}
error_reporting($ER);

Here I am also wrapping the whole string in <div>
so that it will be easy to get back just the contents of
the first div when I need to do saveXML(), I can do this:

$string = substr($this->saveXML($this->getElementsByTagName('div')->item(0)), 5, -6);
This is because I know that the content is wrapped in the div tag, so I am getting the first div, then stripping off the words <div> and </div> from the string.

If loading the xml instead of html, then instead of this meta tag with charset, just make sure that charset is declared in the xml declaration, like this:
<?xml version="1.0" encoding="utf-8" ?>

Meaning the first line must indicate the charset

Sunday, December 20, 2009

Shocking Fandango.com scam and spam

Be warned: when you order movie ticket via fandango.com
they show the enticing offer like $10 off your next ticket or maybe something even better, but
if you read the fine print - it says you will be charged $12/month if you accept this offer.

Also, fandango.com will spam you constantly like there is no tomorrow. When I ordered my ticket I specifically
unchecked all the boxes that said 'receive emails from us'
I know for a fact that I was very careful not to join any of their mailing lists, but 2 days later I received some crap spam
from fandango.

So I thought, find, I will just unsubscribe. So I clicked on the 'unsubscribe', followed that link, unsubscribed.

24 hours later, you guessed it - another bullcrap spam from fandango, may they burn in hell

My conclusion: fandango definitely uses spam to generate revenue and they also use the marketing tacktics that
can be compared to Nigerian online scams - where they tell you you got some money coming to you, but if you
fall for it, you are sure will lose lots of money.

My advice: avoid fandango, buy tickets yourself. Fandango is a 21 century plaique, much more dangerous to human kind than a swine flue.

Fandango contributes to global warning? Fandango kills dogs for food? Fandango destroys rain forest?
Fandango commits mass genocide in Africa?
Fandango runs secret nazi-style concentration camps and tortures people?
Fandango.com supports terrorists?
Fandango.com employs illegal aliens?
fandango is secretely owned by Osama bin Laden?
There are just a few questions on the minds of people and we all deserve answers.