flourishlib / flourish-classes Goto Github PK
View Code? Open in Web Editor NEWThe class files for Flourish
Home Page: http://flourishlib.com
The class files for Flourish
Home Page: http://flourishlib.com
Ticket #523 by steelaz:
fHTTP - flourish class to help consume HTTP based APIs. I would suggest using cURL downgradable to fsockopen. fJSON and fXML could be used to parse API response.
Comment by @Tatsh:
I am providing functionality like that in an object class here:
https://github.com/tatsh/sutra/blob/master/classes/sHTTP.php
$req = new sHTTP('http://theXMLsite.com?alt=json', 'POST');
$req->setContent('post data');
$data = fJSON::decode($req->getData());
You can use URIs with fXML as it currently is. I followed fXML's way of doing an HTTP request with stream_context_create() and file_get_contents().
https://github.com/tatsh/sutra/blob/master/classes/sImage.php#L276
The flip() (necessary for some orientations) is in the same file.
Ticket #585 by @craigruks:
Attached is a patch to add functionality to fORMFile that allows a user to clear out a Flourish temp directory for a file column whenever they please.
Attached file:
--- fORMFile-new.php 2011-03-08 09:49:57.000000000 -0500
+++ fORMFile.php 2011-03-08 09:49:37.000000000 -0500
@@ -9,7 +9,8 @@
* @package Flourish
* @link http://flourishlib.com/fORMFile
*
- * @version 1.0.0b28
+ * @version 1.0.0b29
+ * @version 1.0.0b29 Added ::clearTempDir() so that a temp dir can be cleared at any time [cr, 2011-03-08]
* @changes 1.0.0b28 Backwards Compatibility Break - ::configureImageUploadColumn() no longer accepts the optional `$image_type` as the fourth parameter, instead ::addFImageMethodCall() must be called with `saveChanges` as the `$method` and the image type as the first parameter [wb, 2010-11-30]
* @changes 1.0.0b27 Fixed column inheritance to properly handle non-images and inheriting into image upload columns [wb, 2010-09-18]
* @changes 1.0.0b26 Enhanced ::configureColumnInheritance() to ensure both columns specified have been set up as file upload columns [wb, 2010-08-18]
@@ -45,6 +46,7 @@
const addFImageMethodCall = 'fORMFile::addFImageMethodCall';
const addFUploadMethodCall = 'fORMFile::addFUploadMethodCall';
const begin = 'fORMFile::begin';
+ const clearTempDir = 'fORMFile::clearTempDir';
const commit = 'fORMFile::commit';
const configureColumnInheritance = 'fORMFile::configureColumnInheritance';
const configureFileUploadColumn = 'fORMFile::configureFileUploadColumn';
@@ -224,6 +226,31 @@
/**
+ * Takes a directory and clears the temporary directory inside of it
+ *
+ * @param fActiveRecord $object The fActiveRecord instance
+ * @param array &$values The current values
+ * @param array &$old_values The old values
+ * @param array &$related_records Any records related to this record
+ * @param array &$cache The cache array for the record
+ * @param string $method_name The method that was called
+ * @param array $parameters The parameters passed to the method
+ * @return void
+ */
+ static public function clearTempDir($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
+ {
+ $class = get_class($object);
+
+ list ($action, $column) = fORM::parseMethod($method_name);
+ $column = preg_replace('#_temp_dir$#i', '', $column);
+
+ $upload_dir = self::$file_upload_columns[$class][$column];
+
+ self::prepareTempDir($upload_dir);
+ }
+
+
+ /**
* Commits a transaction, or decreases the level
*
* @internal
@@ -338,6 +365,12 @@
self::prepare
);
+ fORM::registerActiveRecordMethod(
+ $class,
+ 'clear' . $camelized_column . 'TempDir',
+ self::clearTempDir
+ );
+
fORM::registerReflectCallback($class, self::reflect);
fORM::registerInspectCallback($class, $column, self::inspect);
fORM::registerReplicateCallback($class, $column, self::replicate);
Ticket #602 by @khamer:
Example Usage: when working with particularly decrepit databases with unusually decrepit column names like 'CSTFRSTNM' or non-underscore names like 'CreditCardNumber', it'd be nice to provide better versions ('first_name' or 'credit_card_number') so that referencing the columns are more human. For example, getCstfrstnm vs getFirstName, or encodeCreditcardnumber vs encodeCardCardNumber. I'm not sure, but it'd be nice in where statements ( array('first_name=' => 'Kevin') instead of array('cstfrstnm=' => 'Kevin')).
Ticket #748 by @audvare
Line 1182 in fImage.php
if (strpos($command_line, ' -colorspace ') === FALSE) {
$command_line .= ' -colorspace RGB ';
}
Without this argument, the image converts fine.
As a temporary solution, I downgraded to ImageMagick? 6.7.5.3. There might be an issue with this version: http://www.imagemagick.org/discourse-server/viewtopic.php?f=3&t=20682
Ticket #619 by @khamer:
It'd be nice to be able to pass in arguments to buildRelated methods, such as
$users = $group->buildUsers(array('status=' => 'active'));
Along the same lines, it'd be nice to also support the other arguments (order, limit, page.)
Comment by @khamer:
I believe ticket #25 may be a dependency of this ticket.
English version:
https://github.com/tatsh/sutra/blob/master/classes/sNumber.php#L491
There should be a documentation page under General Documentation that explains the current Flourish code standards.
Comment by @wbond:
This is in progress at http://flourishlib.com/docs/CodeStandards
Ticket #538 by @wbond:
fActiveRecord should have the prepare and encode method split out into a new class called fORMHTML.
Ticket #615 by joseph.mendez:
is it possible to add/determine if message is unread or read already, or if its drat or trash in the listMessage() ?
Comment by @wbond:
This was also requested at http://flourishlib.com/discussion/2/390
Ticket #740 by @mblarsen
Please consider adding flourish to http://packagist.org/ a npm like package/dependency installer.
Comment by @wbond
Yes, I am on board with doing this.
The first step, which I have been thinking about for a little while, is getting Flourish into an environment like GitHub? so it is easier to collaborate. While I like a lot of what the current Flourish site offers, it's custom nature means that it is harder to let people contribute without spending a bunch of time trying to make sure they can't edit other files on the server.
I am hoping to move the main development of Flourish to GitHub? and set up a repo there with the documentation and hopefully even set up flourishlib.github.com to host the website. That way it will be easy for people to send pull requests with fixes and the collaboration model will be more open and sustainable.
In addition, this means changing to a semantic versioning system instead of using build numbers, especially since Git doesn't provide revision numbers. There will be a bunch of changes and tweaks that will need to be made like coming up with a better and more modern way to generate the docs (perhaps DocBlox??) and probably converting the wiki documentation over to markdown or something else that has modern tooling for easy generation.
I will be looking for help from people along the way since I am currently in a position where unfortunately I've committed to too many projects and can't keep up with the needs of all of them.
Comment by @mblarsen
The Packagist /getcomposer.org works perfectly with GitHub?. It both support auto versioning but can use tags as well and control it yourself. (testing it out with the small fFixture? extension I posted a few days back).
I'm impressed by the flourishlib site and it shows that you've put a lot of time into it - but there are other things, new projects, so I understand why you want to lift the burden on the 'practical stuff'. GitHub? is definitely a good way to go. I really like the URLs for the documentation, so I'm concerned if this could live on on github. Put perhaps a conversion if you docs into some simple markup mixed something like this TOC script http://projects.jga.me/toc/ could be a solution.
Thumbs of for making it more upen to pull requests.
Ticket #603 by @khamer:
Perhaps an ordered array of conditions?
This allows for order statements to no longer depend no the database for execution, which means build related methods could have a method signature that allows for conditions, orders, and limits. While there's some risk of these build methods leaking more schema into views/controllers, it'll hopefully mostly replace the more common and worse $record->buildRelatedRecords()->filter($conditions)
.
Ticket #745 by @DaleGribble
fUpload.php line 491: $max_size should be $size
Comment by @audvare
Added to my patch set.
https://github.com/tatsh/tatsh-overlay/tree/master/dev-php/flourish/files
Similar to this class which stores the data as a JSON string in the column, but it can be an array or object and there is no need to manually encode or decode to JSON.
https://github.com/tatsh/sutra/blob/master/classes/sORMJSON.php#L31
Should be similar things for serialize(), XML, YAML, and any other formats that might come about.
fORMJSON - array or keyed array or stdClass object
fORMXML - fXML object
fORMYAML - fYAML object (when that class is made)
fORMSerializable - unserialize() on out, serialize() called when being inserted
Ticket #744 by @phuocdaivl08
try {
$db = new fDatabase('mssql', 'TEST', 'sa', '123456','PHUOCDAI-PC\SQLEXPRESS');
// Please note that calling this method is not required, and simply
// causes an exception to be thrown if the connection can not be made
$db->connect();
} catch (fAuthorizationException $e) {
$e->printMessage();
}
Fatal error: Uncaught exception 'fConnectivityException' with message 'Unable to connect to database - hostname not found' in C:\AppServ?\inc\flourish\fDatabase.php:2057 Stack trace: #0 C:\AppServ?\inc\flourish\fDatabase.php(797): fDatabase→handleConnectionErrors(Array) #1 C:\AppServ?\www\posttin\c.php(31): fDatabase→connect() #2 {main} thrown in C:\AppServ?\inc\flourish\fDatabase.php on line 2057
Can you help me? How to fix this error? I'm from VietNam?. My English is not good!
Ticket #540 by @wbond:
It would be nice to have a Yaml class.
Comment by @mblarsen:
Indeed - I use this one:
http://components.symfony-project.org/yaml/
It works great, however I would like the parser and dumper to be the samme class.
The inline-notation-level is a good idea.
Comment by @ilo:
The Yaml component of symfoni is licensed as MIT also, Will, could this be of help to integrate into florusihlib?
Comment by @wbond:
Yes, I use sfYaml on a side project. I did a little bit of work refactoring it into a single class, but I gave up in the interest of trying to get closer to shipping the project. Ideally it would be a single class that supports encoding and decoding, probably with convenience functions for loading and saving.
I think the main thing that needs to be identified here is what version of Yaml, or what functionality, should be supported. Also, I would think that fYaml would use any available C extensions for speed, but provide a PHP fallback.
Comment by @netcarver:
FWIW, I've used spyc before. It's a pure PHP implementation of a single-file YAML 1.0 compatible class available under the MIT license.
Ticket #546 by @khamer:
It'd be useful (for me anyway) to have a piece of documentation that shows the signatures and such for each of the dynamic methods (prepare, encode, etc.) for each data type; ideally organized by method, then by data type.
Ticket #440 by vxnick:
Just a wild idea - have a class with common regular expressions, such as the following:
Some of these already exist in fValidation (and fAuthorization from memory), but it might be nice to split them out so we can just get a true/false return value.
Comment by @wbond:
Rather than pulling these all into a new class, I think I will take the model of fEmail, and make some regular expressions as constants on the applicable classes so that they can be reused.
Ticket #753 by @toonman
Mail client could not display embedded images in emails… Apparently the added spaces in the Message-ID caused this. Sample Message-ID: <e3ccb8f9ec.1335371032@Linux infong 2.4 #1 SMP Tue Jan 17 02:58:41 UTC 2012 i686 GNU/Linux>
I changed the fEmail line 626 from this $this→message_id = '<' . fCryptography::randomString(10, 'hexadecimal') . '.' . time() . '@' . self::getFQDN() . '>';
to this… $this→message_id = '<' . fCryptography::randomString(10, 'hexadecimal') . '.' . time() . '@' . fURL::makeFriendly( self::getFQDN() ) . '>';
By adding the fURL::makefriendly(). This removed the whitespaces and the image finally showed up in Thunderbird and Outlook. Maybe it should be added with the next update.
Thanks for you great work!
Ticket #622 by @wbond
Update the site design with the new logo
Ticket #202 by vena:
It'd be wonderful if we could have a set once and forget method to default owner/group and permissions for files created by fFile, fORMFile, etc. Outside of an suPHP environment, this can be quite necessary if you ever want to manipulate files created by the script by hand.
Comment by @wbond:
Due to the way that file permission are set up, the owner of a file can only be changed by the superuser. The group can only be changed to a group that the user, that the web server is running as, is part of. Because of those limitation it doesn't look like it will be possible to set that up.
In terms of the file permissions, I think I can come up with something. Currently fUpload chmods all uploaded files to 0644, but it would be good for fFile::create() and fDirectory::create() to all obey one set of permissions.
Ticket #746 by @DaleGribble
http://flourishlib.com/docs/fValidation#Instantiation
In the description field for addIntegerFields(), "intege" should be "integer"
Ticket #531 by @jeffturcotte:
It would be very convenient to be able to select a different timezone when preparing a timestamp object through fActiveRecord.
Ticket #750 by @netcarver
Currently the member variables and the check* methods are all declared private, making it impossible to extend the class and override any of those methods or access the member variables. Making these protected would allow extension if needed.
Comment by @audvare
I'd really like to know what you intend to do in your extension. I think the check* methods are private for good reason and I have yet to have a need to mess with fValidation's internals or extend on it.
fValidation has fValidation::addCallbackRule() which essentially allows custom validation on a per-object basis. And as such, you can simply create constants that define callbacks (so they look nicer in code).
class User extends fActiveRecord {
const nameIsAvailable = 'User::nameIsAvailable';
public static function nameIsAvailable($name) {
try {
new self(array('name' => $name));
return FALSE; // is taken
}
catch (fNotFoundException $e) {}
return TRUE;
}
}
// later code
try {
$validation = new fValidation;
$validation->addRequiredFields('name');
$validation->addCallbackRule('name', User::nameIsAvailable, 'Sorry, that user name is taken.');
$validation->validate();
fURL::redirect('/');
}
catch (fValidationException $e) {
fMessaging::create('validation', '/registration' $e->getMessage());
fURL::redirect('/registration');
}
I have a longer example here which uses fValidation: https://github.com/tatsh/sutra/wiki/Tutorial
Comment by @netcarver
Hello Andrew,
Thank you for reply and the great examples, especially in your link which I found particularly interesting.
My request is motivated by wanting to have callback rules be able to return an error message directly, rather than having to define it when the rule is added to the validation object. This would allow callbacks to provide messages that are appropriate to, say, some value of an input or other combinations of factors.
An additional motivation is the supply of custom strings to required elements such that the message displayed for, say, a missing age can be different to that displayed for a missing check box confirming acceptance of conditions. Whilst this can probably be done using a compose callback on fText, it might be easier to have a version of addRequiredFields() that accepts an array with key=>value pairs that match form element names to messages.
Comment by @audvare
The fValidaion::validate() method can also be controlled to not throw and instead return messages by passing TRUE as the first parameter. Passing TRUE as the second parameter removes filed names from the messages.
fValidationException goes through fText::compose(). You can use fText::registerCoposeCallback() in order to change messages. I know this may not be ideal for the situation you describe here.
You may instead need to skip fValidation for that purpose, still use fValidation but after you run fValidation::validate() perform more validation. If you do it in the try/catch block like in my example, then you can simply throw a new fValidationException when an error occurs (or of course in the method/funcion you call from there).
try {
$validation = new fValidation;
// Add conditions
$validation->validate();
// Method that throws with a specific message
SomeClass::validateAdditionalConditions();
}
catch (fValidationException $e) {
// Do something based on the error
}
You would not have to use fText::compose() as fValidation has fValidation::addStringReplacement() which makes str_replace() get called before each message is about to be composed.
I like the idea of specifying messages for required fields more easily. I suggest 2 solutions.
It would be interesting to have a method like fValidation::setRequiredFieldMessages(), basically an easier way than calling fValidation::addStringReplacement() many times and far easier to use since you don't have to know what to replace.
$validation = new fValidation;
$validation->setRequiredFields('accepted'); // checkbox
$validation->setRequiredFieldMessages(array(
'accepted' => 'You must accept the terms and conditions to register.',
));
$validation->validate();
Any messages not overridden will be left alone.
Or, since you can use an array with fValidation::addRequiredFields() then a few new signatures (honestly the former is probably a lot easier to maintain):
class fValidation() {
public function addRequiredFields(array $fields, array $messages) {}
public function addRequiredFields(array $fields_to_messages) {} // as you described
}
If you make a change directly to fValidation and it works, post a patch.
Do not know if every comment should be mentioned. Please take a look at it: http://flourishlib.com/ticket/750
Ticket #749 by @unstoppablecarl
when having tables: categories, records, categories_records. categories are in a many-to-many relationship with records icategories primary key is id records primary key is id
when calling $categories→prebuildRecords();
the id column is missing from the $row var in the fActiveRecord::loadFromResult() method
$pk_columns = $schema→getKeys($table, 'primary');
foreach ($pk_columns as $column) {
// error happens here
$value = $row[$column]; if ($value !== NULL && isset(self::$unescape_map[$class][$column])) {
$value = $db→unescape(self::$unescape_map[$class][$column], $value);
}
$this→values[$column] = fORMobjectify($class, $column, $value); unset($row[$column]);
}
it seems that this bit of code in the fRecordSet::prebuild() method is the cause I don't quite understand what it's purpose is. // If we are going through a join table we need to remove the related primary key that was used for matching
if (isset($relationshipjoin_table?)) {
unset($row[$relationshipcolumn?]);
}
Ticket #752 by @audvare
As described.
/**
* Forces use as a static class
*
* @return fORMValidation
*/
private function __construct() {
}
Ticket #537 by @wbond:
Make fORMRequest to hold the populate() method, and more methods for secure filtering of populate and population of individual columns.
Comment by @benhaines:
Attached above is a draft of the fORMRequest which i use currently, its not set up to work with relationships yet tho. Hope it helps with getting started with this integrated into the core flourish library, you can either whitelist or blacklist columns to be used with populate.
Attached file:
<?php
class fORMRequest{
const populate = 'fORMRequest::populate';
const hydrate = 'fORMRequest::hydrate';
const addProtectedColumns = 'fORMRequest::addProtectedColumns';
const addAccessibleColumns = 'fORMRequest::addAccessibleColumns';
static private $protected_columns = array();
static public function extend(){
fORM::registerActiveRecordMethod(
'*',
'populate',
self::populate
);
fORM::registerActiveRecordMethod(
'*',
'hydrate',
self::hydrate
);
}
static public function hydrate($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters){
$class = get_class($object);
$new_values = $parameters[0];
fORM::callHookCallbacks(
$object,
'pre::hydrate()',
$values,
$old_values,
$related_records,
$cache
);
if(!is_array($new_values)){
throw fProgrammerException(
'The hydrate method requires an associated array to populate the values'
);
}
$schema = fORMSchema::retrieve($class);
$table = fORM::tablize($class);
$new_value_keys = array_keys($new_values);
$column_info = $schema->getColumnInfo($table);
foreach ($column_info as $column => $info) {
if (in_array($column,$new_value_keys)) {
$method = 'set' . fGrammar::camelize($column, TRUE);
$cast_to = ($info['type'] == 'blob') ? 'binary' : NULL;
$object->$method($new_values[$column]);
}
}
fORM::callHookCallbacks(
$object,
'post::hydrate()',
$values,
$old_values,
$related_records,
$cache
);
return $object;
}
static public function populate($object, &$values, &$old_values, &$related_records, &$cache, $method_name, $parameters)
{
$class = get_class($object);
fORM::callHookCallbacks(
$object,
'pre::populate()',
$values,
$old_values,
$related_records,
$cache
);
$schema = fORMSchema::retrieve($class);
$table = fORM::tablize($class);
$column_info = $schema->getColumnInfo($table);
foreach ($column_info as $column => $info) {
//Ignore protected coulmns
if(isset(self::$protected_columns[$class]) && in_array($column,self::$protected_columns[$class])){
continue;
}
if (fRequest::check($column)) {
$method = 'set' . fGrammar::camelize($column, TRUE);
$cast_to = ($info['type'] == 'blob') ? 'binary' : NULL;
$object->$method(fRequest::get($column, $cast_to));
}
}
fORM::callHookCallbacks(
$object,
'post::populate()',
$values,
$old_values,
$related_records,
$cache
);
return $object;
}
static public function addProtectedColumns($class, $columns){
$class = fORM::getClass($class);
settype($columns, 'array');
if (!isset(self::$protected_columns[$class])) {
self::$protected_columns[$class] = array();
}
foreach ($columns as $column) {
if(!in_array($column,self::$protected_columns[$class])){
self::$protected_columns[$class][] = $column;
}
}
}
static public function addAccessibleColumns($class, $columns){
$class = fORM::getClass($class);
$schema = fORMSchema::retrieve($class);
$table = fORM::tablize($class);
$column_info = $schema->getColumnInfo($table);
$column_names = array_keys($column_info);
settype($columns, 'array');
if (!isset(self::$protected_columns[$class])) {
self::$protected_columns[$class] = array();
}
foreach ($column_names as $column) {
if(!in_array($column,$columns) && !in_array($column,self::$protected_columns[$class])){
self::$protected_columns[$class][] = $column;
}
}
}
}
Ticket #568 by @mattsah:
This is a ticket to discuss and work on the possibility of adding document database support to Flourish. I have dome some initial implementation on my personal fork, however, it would be nice to see some support get mainlined into the trunk. With that said, here's a few things out of initial discussion with Will as well as additional research.
Aimed support:
MongoDB is much better suited for the proposed structure than CouchDB, namely because it has a built in concept of Collections as well as being the only document database (not simply key value store) with official PHP support it seems. Additionally there are aspects to the API that map well to the existing fDatabase API, although albeit, without SQL.
It seems as though there are two approaches which could make this work cleanly, although one would likely add significantly more code and repeat similar logic found in fORM, fActiveRecord, etc.
It is possible to create a completely alternative Document driven Object Mapper independent of the common ORM/Relational base with classes such as those listed above.
It is possible to centralize and abstract similar code into additional base classes, and then simply have fODM and fORM, for example, using the common base class for common logic. Similarly, fDatabase would SQL related tasks moved to fRelationalDatabase or fRDatabase while the specific but similar fDatabase API would use fDocumentDatabase or fDDatabase.
Benefits of the first approach is that there will be no need to independently check whether we are dealing with ORM or ODM in the base classes. This means someone could completely and independently implement fODM from any fORM related code. Major downsides is that a number of classes would have to be re-implemented. While Document Databases tend not to have a defined schema or SQL support, using a number of similar methods to what is allowed in normal fORM operations, one could easily create a number of validation and enforcement rules for schema-less databases, while the existing fORM code works with the schema validation as well as additional validation or ORM rules applied on top. This would mean on adding relevant classes for EVERY similar aspect: fODMNumber, fODMFile, fODMValidation, etc. I count about 13 new classes at least given the current size of the ORM count.
The benefit of the second approach would be that we could concentrate similar functionality and api in more core classes — for example fMapper, fMapperNumber, fMapperFile, fMapperValidation — or something similar. From here only the specific differences could be added into fORM and fODM prefixed classes. One major downside of this, of course, is huge breaks in backwards compatibility.
A mixed approach could add base classes + all related prefixed classes which would simply wrap the base packages in the initial API. Keeping the core API semantic would be ideal, so for example fORMtablize could be abstracted into fMapperclassToPool, from which fORMtablize and fODMcollectionize would simply call that.
This would allow for consistent but semantic naming along with limited backwards compatibility breaks. Many fORM vs. fODM method names would be similar and follow similar API but would reflect the differences between core terminology, meaning if you know one you could work with the other very easily, but it would also remain intuitive to each class set.
On the issue of the databases themselves, part of the problem remains their disparate API as well as lacking support in PHP. For example, while MongoDB has a built in concept of collections, CouchDB does not, and it is up to the author to group documents via a property. This being said, one approach might be to make fDocument non-abstract, so that a Document is a Document is a Document, if when instantiated the class of $this is not fDocument, additional steps could be taken to work within the approriate collection for MongoDB or similar databases, while in the case of CouchDB a property could be added to the document automatically to specify its collection.
CouchDB, does, however, provide the concept of views, which could for example be established as such:
function(doc) {
if (doc.Collection == "collection_name") {
emit(null, doc)
}
}
However, from what I can tell there is no way to determine which views would be related to simple collections vs. which represent more complex map functions. In this sense, despite not having a schema, CouchDB is even less "schema-less" than MongoDB, as with MongoDB it is possible to list/show collections at least similar to getting a list of tables.
These are some initial ideas on the subject, and I will likely continuing developing some of them on my own fork to see what seems to work and what doesn't with more updates to come.
Ticket #623 by @chinvoon
hi,
I am just using fmailbox this class and found that it not bad, but poor is, it cannot do the softing by date, from or others. If allow i hope this function available in this class also.
And also, it cannot return total messages, but i already fix it and the coding are copy from your source also.
have a nice day.
Regards Tan Chin Voon
The class works great, provided you meet the requirements.
Here is my fork (minimal changes and more detailed documentation):
https://github.com/tatsh/fpORMTagging
Ticket #515 by @passcod:
See http://flourishlib.com/discussion/2/304
fCryptography should check for phpseclib's AES and Rijndael if mcrypt isn't found, and use these methods.
Comment by @passcod:
Here is an implementation over version 1.0.0b12 of fCryptography.
There is one significant issue to this implementation: data encrypted by phpseclib cannot be decrypted using mcrypt, and vice-versa. For this reason, instead of 'fCryptographysymmetric', when a string is encrypted using phpseclib the ciphertext prefix is 'fCryptographysymmetric-php'. Furthermore, the class will not attempt to decrypt a ciphertext not prefixed by the '-php' variant using phpseclib, nor will it try the inverse.
Another change made is to remove the dependency on mcrypt for key and iv generation, however this change should not affect de/encryption of existing data.
Comment by @wbond:
It appears that php sec lib does not provide cipher feedback mode (CFB) for its Rujndael implementation. Your code should be throwing strict error notices since you are using an undefined constant. This also explains why you were unable to produce results that are compatible with mcrypt.
To accomplish a proper pure-PHP fallback, Flourish is going to need to find or write a CFB mode AES implementation.
Comment by @wbond:
Upon further inspection, I found that phpseclib provides a CFB mode in SVN, just not in the release on their homepage. Also, you don't seem to be setting the same block and key sizes that mcrypt is using, which is probably why you are getting different results.
For me to accept your patch as the basis for the modifications to fCryptography, I just need to to fill out a contributor license agreement so that users of Flourish can be assured you are providing your code under the MIT license. If you wrote the code while working for another company, your company will need to fill out a corporate contributor license agreement and list you. If this is the case, you do not need to submit a CLA of your own.
Once you email me a PDF of the signed document, I can work on integrating your changes and testing it all.
Comment by @passcod:
Replying to wbond:
Also, you don't seem to be setting the same block and key sizes that mcrypt is using, which is probably why you are getting different results.
In fact, I do, I used the following piece of code to find the key/iv/block sizes used by mcrypt and hardcoded the values in:
$module = mcrypt_module_open('rijndael-192', '', 'cfb', '');
$key_size = mcrypt_enc_get_key_size($module);
$iv_size = mcrypt_enc_get_iv_size($module);
$block_size = mcrypt_enc_get_block_size($module);
echo "\n\n\nKEY: $key_size\nIV: $iv_size\nBLOCK: $block_size";
The result being:
KEY: 32
IV: 24
BLOCK: 24
I had read the Crypt code and left the block size to default to 24. However, for better design, I now explicitly state the block size on initialization of Crypt_Rijndael.
You will notice the IV has a length of 24, when I use ::randomString to create an alphanumeric string of 32 characters. As the code shows, I base64 decode the string to give a 24 bytes IV. (This could be improved by allowing the user to specify a custom alphabet to ::randomString and therefore using the full base64 alphabet.)
Comment by @wbond:
The default block size in Crypt_Rijndael is 16, but I see you updated your code to specify 24. However, your key size is still wrong. mcrypt uses a 32 byte, or 256 bit key, however you are explicitly setting the key size to 192. I've updated the fCryptography documentation to explicitly state the block and key sizes.
I also added a base64 option to fCryptography::randomString() for the purpose of generating raw bytes with base64_decode().
Comment by @passcod:
Third rev.
I now use the base64 option for ::randomString(), and I have fixed the key and block sizes.
The cipher is still not compatible phpseclib/mcrypt (tested as: encrypt using php, decrypt using mcrypt).
FYI, here's my test code:
<?php
function __autoload($class_name)
{
if ( preg_match("/^f.+$/i", $class_name) ) {
$class_root = $_SERVER['DOCUMENT_ROOT'] . '/lib/flourish/';
}
else {
$class_root = $_SERVER['DOCUMENT_ROOT'] . '/lib/class/';
}
$file = $class_root . $class_name . '.php';
if (file_exists($file)) {
include $file;
return;
}
throw new Exception('The class ' . $class_name . ' could not be loaded');
}
header('Content-type: text/plain');
$c = new Crypt_Rijndael(); unset($c);
// Load phpseclib using __autoload function
$secret = "secret";
$pw = "password";
$ciphertext = fCryptography::symmetricKeyEncrypt($secret, $pw);
echo "$ciphertext\n";
echo "fCryptography::symmetric-php^xxxxxxxxxxxxxxIVxxxxxxxxxxxxxxxx^xCIPHERx^xxxxxxxxxxxxxxxxxxHMACxxxxxxxxxxxxxxxxxx \n\n";
// This is a mask to check the form of the ciphertext
$ciphertext = str_replace('-php', '', $ciphertext);
$plain = fCryptography::symmetricKeyDecrypt($ciphertext, $pw);
echo "\nPHP-MCRYPT TEST:\n\n$ciphertext\n";
echo "fCryptography::symmetric^xxxxxxxxxxxxxxIVxxxxxxxxxxxxxxxx^xCIPHERx^xxxxxxxxxxxxxxxxxxHMACxxxxxxxxxxxxxxxxxx";
//echo "\n\n= $plain";
// (Uncomment to test)
?>
Comment by @wbond:
This implies to me that there is a bug in the phpseclib code for CFB mode or the Rijndael cipher, however I can't say for sure. I'll have to spend a little time looking into it to see if I can figure out what is going on.
By the way, thanks for all of your work!
Comment by zelnaga:
Looking over phpseclib's Crypt/AES.php, I see that the author does reimplement CFB mode, where mcrypt is concerned, per the following:
http://phpseclib.sourceforge.net/cfb-demo.phps
So what might appear to be a bug in phpseclib might actually be a bug in mcrypt. And that mcrypt could have bugs should not be a total surprise. mcrypt is, after all, abandoned. Unless I'm mistaken, the last commit was almost two years ago to the day:
http://mcrypt.cvs.sourceforge.net/viewvc/mcrypt/mcrypt/
Here's another bug on php.net:
Comment by zelnaga:
I also note that your calls to mcrypt_module_open use 'cfb' as the mode whereas what phpseclib is using is more analogous (although not quite identical for reasons already explained) to 'ncfb'.
Also, I thought this had already been discussed before, but why not ask the phpseclib author if they'd be willing to relicense - or at least dual license - their code under the MIT license and just include it? ie. let phpseclib use mcrypt when it's available instead of having the person using flourishlib have to make the decision for themselves?
Comment by zelnaga:
I sent the phpseclib maintainer an email suggesting they relicense their code to the MIT license and they agreed:
http://phpseclib.svn.sourceforge.net/viewvc/phpseclib?revision=130&view=revision
Comment by @wbond:
Thanks for the work you've done looking into this. I read through the links you provided. The first one didn't seem to give any definitive info as to if mcrypt was doing anything wrong - instead it seemed to be three people unsure.
I did read the PHP bug report, and I think I am going to take a little time to read through some of the mcrypt source and the phpseclib source to see where they differ. Thanks to your request to relicense phpseclib, it looks like if I can get them functioning the same I could integrate them together into Flourish, which would be awesome for portability. If this ends up being the case, I think the only non-standard PHP extension required of Flourish would be openssl for secure connections.
Attached file:
<?php
/**
* Provides cryptography functionality, including hashing, symmetric-key encryption and public-key encryption
*
* @copyright Copyright (c) 2007-2010 Will Bond
* @author Will Bond [wb] <[email protected]>
* @license http://flourishlib.com/license
*
* @patch passcod [passcod] <[email protected]>
*
* @package Flourish
* @link http://flourishlib.com/fCryptography
*
* @version 1.0.0b14
* @changes 1.0.0b14 Added support for phpseclib's pure-php symmetric de/encryption [passcod, 2010-11-07]
* @changes 1.0.0b13 Updated documentation about symmetric-key encryption to explicitly state block and key sizes, added base64 type to ::randomString() [wb, 2010-11-06]
* @changes 1.0.0b12 Fixed an inline comment that incorrectly references AES-256 [wb, 2010-11-04]
* @changes 1.0.0b11 Updated class to use fCore::startErrorCapture() instead of `error_reporting()` [wb, 2010-08-09]
* @changes 1.0.0b10 Added a missing parameter to an fProgrammerException in ::randomString() [wb, 2010-07-29]
* @changes 1.0.0b9 Added ::hashHMAC() [wb, 2010-04-20]
* @changes 1.0.0b8 Fixed ::seedRandom() to pass a directory instead of a file to [http://php.net/disk_free_space `disk_free_space()`] [wb, 2010-03-09]
* @changes 1.0.0b7 SECURITY FIX: fixed issue with ::random() and ::randomString() not producing random output on OSX, made ::seedRandom() more robust [wb, 2009-10-06]
* @changes 1.0.0b6 Changed ::symmetricKeyEncrypt() to throw an fValidationException when the $secret_key is less than 8 characters [wb, 2009-09-30]
* @changes 1.0.0b5 Fixed a bug where some windows machines would throw an exception when generating random strings or numbers [wb, 2009-06-09]
* @changes 1.0.0b4 Updated for new fCore API [wb, 2009-02-16]
* @changes 1.0.0b3 Changed @ error suppression operator to `error_reporting()` calls [wb, 2009-01-26]
* @changes 1.0.0b2 Backwards compatibility break - changed ::symmetricKeyEncrypt() to not encrypt the IV since we are using HMAC on it [wb, 2009-01-26]
* @changes 1.0.0b The initial implementation [wb, 2007-11-27]
*/
class fCryptography
{
// The following constants allow for nice looking callbacks to static methods
const checkPasswordHash = 'fCryptography::checkPasswordHash';
const hashHMAC = 'fCryptography::hashHMAC';
const hashPassword = 'fCryptography::hashPassword';
const publicKeyDecrypt = 'fCryptography::publicKeyDecrypt';
const publicKeyEncrypt = 'fCryptography::publicKeyEncrypt';
const publicKeySign = 'fCryptography::publicKeySign';
const publicKeyVerify = 'fCryptography::publicKeyVerify';
const random = 'fCryptography::random';
const randomString = 'fCryptography::randomString';
const symmetricKeyDecrypt = 'fCryptography::symmetricKeyDecrypt';
const symmetricKeyEncrypt = 'fCryptography::symmetricKeyEncrypt';
/**
* Checks a password against a hash created with ::hashPassword()
*
* @param string $password The password to check
* @param string $hash The hash to check against
* @return boolean If the password matches the hash
*/
static public function checkPasswordHash($password, $hash)
{
$salt = substr($hash, 29, 10);
if (self::hashWithSalt($password, $salt) == $hash) {
return TRUE;
}
return FALSE;
}
/**
* Create a private key resource based on a filename and password
*
* @throws fValidationException When the private key is invalid
*
* @param string $private_key_file The path to a PEM-encoded private key
* @param string $password The password for the private key
* @return resource The private key resource
*/
static private function createPrivateKeyResource($private_key_file, $password)
{
if (!file_exists($private_key_file)) {
throw new fProgrammerException(
'The path to the PEM-encoded private key specified, %s, is not valid',
$private_key_file
);
}
if (!is_readable($private_key_file)) {
throw new fEnvironmentException(
'The PEM-encoded private key specified, %s, is not readable',
$private_key_file
);
}
$private_key = file_get_contents($private_key_file);
$private_key_resource = openssl_pkey_get_private($private_key, $password);
if ($private_key_resource === FALSE) {
throw new fValidationException(
'The private key file specified, %s, does not appear to be a valid private key or the password provided is incorrect',
$private_key_file
);
}
return $private_key_resource;
}
/**
* Create a public key resource based on a filename
*
* @param string $public_key_file The path to an X.509 public key certificate
* @return resource The public key resource
*/
static private function createPublicKeyResource($public_key_file)
{
if (!file_exists($public_key_file)) {
throw new fProgrammerException(
'The path to the X.509 certificate specified, %s, is not valid',
$public_key_file
);
}
if (!is_readable($public_key_file)) {
throw new fEnvironmentException(
'The X.509 certificate specified, %s, can not be read',
$public_key_file
);
}
$public_key = file_get_contents($public_key_file);
$public_key_resource = openssl_pkey_get_public($public_key);
if ($public_key_resource === FALSE) {
throw new fProgrammerException(
'The public key certificate specified, %s, does not appear to be a valid certificate',
$public_key_file
);
}
return $public_key_resource;
}
/**
* Provides a pure PHP implementation of `hash_hmac()` for when the hash extension is not installed
*
* @internal
*
* @param string $algorithm The hashing algorithm to use: `'md5'` or `'sha1'`
* @param string $data The data to create an HMAC for
* @param string $key The key to generate the HMAC with
* @return string The HMAC
*/
static public function hashHMAC($algorithm, $data, $key)
{
if (function_exists('hash_hmac')) {
return hash_hmac($algorithm, $data, $key);
}
// Algorithm from http://www.ietf.org/rfc/rfc2104.txt
if (strlen($key) > 64) {
$key = pack('H*', $algorithm($key));
}
$key = str_pad($key, 64, "\x0");
$ipad = str_repeat("\x36", 64);
$opad = str_repeat("\x5C", 64);
return $algorithm(($key ^ $opad) . pack('H*', $algorithm(($key ^ $ipad) . $data)));
}
/**
* Hashes a password using a loop of sha1 hashes and a salt, making rainbow table attacks infeasible
*
* @param string $password The password to hash
* @return string An 80 character string of the Flourish fingerprint, salt and hashed password
*/
static public function hashPassword($password)
{
$salt = self::randomString(10);
return self::hashWithSalt($password, $salt);
}
/**
* Performs a large iteration of hashing a string with a salt
*
* @param string $source The string to hash
* @param string $salt The salt for the hash
* @return string An 80 character string of the Flourish fingerprint, salt and hashed password
*/
static private function hashWithSalt($source, $salt)
{
$sha1 = sha1($salt . $source);
for ($i = 0; $i < 1000; $i++) {
$sha1 = sha1($sha1 . (($i % 2 == 0) ? $source : $salt));
}
return 'fCryptography::password_hash#' . $salt . '#' . $sha1;
}
/**
* Decrypts ciphertext encrypted using public-key encryption via ::publicKeyEncrypt()
*
* A public key (X.509 certificate) is required for encryption and a
* private key (PEM) is required for decryption.
*
* @throws fValidationException When the ciphertext appears to be corrupted
*
* @param string $ciphertext The content to be decrypted
* @param string $private_key_file The path to a PEM-encoded private key
* @param string $password The password for the private key
* @return string The decrypted plaintext
*/
static public function publicKeyDecrypt($ciphertext, $private_key_file, $password)
{
self::verifyPublicKeyEnvironment();
$private_key_resource = self::createPrivateKeyResource($private_key_file, $password);
$elements = explode('#', $ciphertext);
// We need to make sure this ciphertext came from here, otherwise we are gonna have issues decrypting it
if (sizeof($elements) != 4 || $elements[0] != 'fCryptography::public') {
throw new fProgrammerException(
'The ciphertext provided does not appear to have been encrypted using %s',
__CLASS__ . '::publicKeyEncrypt()'
);
}
$encrypted_key = base64_decode($elements[1]);
$ciphertext = base64_decode($elements[2]);
$provided_hmac = $elements[3];
$plaintext = '';
$result = openssl_open($ciphertext, $plaintext, $encrypted_key, $private_key_resource);
openssl_free_key($private_key_resource);
if ($result === FALSE) {
throw new fEnvironmentException(
'There was an unknown error decrypting the ciphertext provided'
);
}
$hmac = self::hashHMAC('sha1', $encrypted_key . $ciphertext, $plaintext);
// By verifying the HMAC we ensure the integrity of the data
if ($hmac != $provided_hmac) {
throw new fValidationException(
'The ciphertext provided appears to have been tampered with or corrupted'
);
}
return $plaintext;
}
/**
* Encrypts the passed data using public key encryption via OpenSSL
*
* A public key (X.509 certificate) is required for encryption and a
* private key (PEM) is required for decryption.
*
* @param string $plaintext The content to be encrypted
* @param string $public_key_file The path to an X.509 public key certificate
* @return string A base-64 encoded result containing a Flourish fingerprint and suitable for decryption using ::publicKeyDecrypt()
*/
static public function publicKeyEncrypt($plaintext, $public_key_file)
{
self::verifyPublicKeyEnvironment();
$public_key_resource = self::createPublicKeyResource($public_key_file);
$ciphertext = '';
$encrypted_keys = array();
$result = openssl_seal($plaintext, $ciphertext, $encrypted_keys, array($public_key_resource));
openssl_free_key($public_key_resource);
if ($result === FALSE) {
throw new fEnvironmentException(
'There was an unknown error encrypting the plaintext provided'
);
}
$hmac = self::hashHMAC('sha1', $encrypted_keys[0] . $ciphertext, $plaintext);
return 'fCryptography::public#' . base64_encode($encrypted_keys[0]) . '#' . base64_encode($ciphertext) . '#' . $hmac;
}
/**
* Creates a signature for plaintext to allow verification of the creator
*
* A private key (PEM) is required for signing and a public key
* (X.509 certificate) is required for verification.
*
* @throws fValidationException When the private key is invalid
*
* @param string $plaintext The content to be signed
* @param string $private_key_file The path to a PEM-encoded private key
* @param string $password The password for the private key
* @return string The base64-encoded signature suitable for verification using ::publicKeyVerify()
*/
static public function publicKeySign($plaintext, $private_key_file, $password)
{
self::verifyPublicKeyEnvironment();
$private_key_resource = self::createPrivateKeyResource($private_key_file, $password);
$result = openssl_sign($plaintext, $signature, $private_key_resource);
openssl_free_key($private_key_resource);
if (!$result) {
throw new fEnvironmentException(
'There was an unknown error signing the plaintext'
);
}
return base64_encode($signature);
}
/**
* Checks a signature for plaintext to verify the creator - works with ::publicKeySign()
*
* A private key (PEM) is required for signing and a public key
* (X.509 certificate) is required for verification.
*
* @param string $plaintext The content to check
* @param string $signature The base64-encoded signature for the plaintext
* @param string $public_key_file The path to an X.509 public key certificate
* @return boolean If the public key file is the public key of the user who signed the plaintext
*/
static public function publicKeyVerify($plaintext, $signature, $public_key_file)
{
self::verifyPublicKeyEnvironment();
$public_key_resource = self::createPublicKeyResource($public_key_file);
$result = openssl_verify($plaintext, base64_decode($signature), $public_key_resource);
openssl_free_key($public_key_resource);
if ($result === -1) {
throw new fEnvironmentException(
'There was an unknown error verifying the plaintext and signature against the public key specified'
);
}
return ($result === 1) ? TRUE : FALSE;
}
/**
* Generates a random number using [http://php.net/mt_rand mt_rand()] after ensuring a good PRNG seed
*
* @param integer $min The minimum number to return
* @param integer $max The maximum number to return
* @return integer The psuedo-random number
*/
static public function random($min=NULL, $max=NULL)
{
self::seedRandom();
if ($min !== NULL || $max !== NULL) {
return mt_rand($min, $max);
}
return mt_rand();
}
/**
* Returns a random string of the type and length specified
*
* @param integer $length The length of string to return
* @param string $type The type of string to return: `'alphanumeric'`, `'alpha'`, `'numeric'`, or `'hexadecimal'`
* @return string A random string of the type and length specified
*/
static public function randomString($length, $type='alphanumeric')
{
if ($length < 1) {
throw new fProgrammerException(
'The length specified, %1$s, is less than the minimum of %2$s',
$length,
1
);
}
switch ($type) {
case 'base64':
$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/';
break;
case 'alphanumeric':
$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
break;
case 'alpha':
$alphabet = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
break;
case 'numeric':
$alphabet = '0123456789';
break;
case 'hexadecimal':
$alphabet = 'abcdef0123456789';
break;
default:
throw new fProgrammerException(
'The type specified, %1$s, is invalid. Must be one of: %2$s.',
$type,
join(', ', array('alphanumeric', 'alpha', 'numeric', 'hexadecimal'))
);
}
$alphabet_length = strlen($alphabet);
$output = '';
for ($i = 0; $i < $length; $i++) {
$output .= $alphabet[self::random(0, $alphabet_length-1)];
}
return $output;
}
/**
* Makes sure that the PRNG has been seeded with a fairly secure value
*
* @return void
*/
static private function seedRandom()
{
static $seeded = FALSE;
if ($seeded) {
return;
}
fCore::startErrorCapture(E_WARNING);
$bytes = NULL;
// On linux/unix/solaris we should be able to use /dev/urandom
if (!fCore::checkOS('windows') && $handle = fopen('/dev/urandom', 'rb')) {
$bytes = fread($handle, 4);
fclose($handle);
// On windows we should be able to use the Cryptographic Application Programming Interface COM object
} elseif (fCore::checkOS('windows') && class_exists('COM', FALSE)) {
try {
// This COM object no longer seems to work on PHP 5.2.9+, no response on the bug report yet
$capi = new COM('CAPICOM.Utilities.1');
$bytes = base64_decode($capi->getrandom(4, 0));
unset($capi);
} catch (Exception $e) { }
}
// If we could not use the OS random number generators we get some of the most unique info we can
if (!$bytes) {
$string = microtime(TRUE) . uniqid('', TRUE) . join('', stat(__FILE__)) . disk_free_space(dirname(__FILE__));
$bytes = substr(pack('H*', md5($string)), 0, 4);
}
fCore::stopErrorCapture();
$seed = (int) (base_convert(bin2hex($bytes), 16, 10) - 2147483647);
mt_srand($seed);
$seeded = TRUE;
}
/**
* Decrypts ciphertext encrypted using symmetric-key encryption via ::symmetricKeyEncrypt()
*
* Since this is symmetric-key cryptography, the same key is used for
* encryption and decryption.
*
* @throws fValidationException When the ciphertext appears to be corrupted
*
* @param string $ciphertext The content to be decrypted
* @param string $secret_key The secret key to use for decryption
* @return string The decrypted plaintext
*/
static public function symmetricKeyDecrypt($ciphertext, $secret_key)
{
$elements = explode('#', $ciphertext);
// We need to make sure this ciphertext came from here, otherwise we are gonna have issues decrypting it
if (sizeof($elements) == 4 || $elements[0] == 'fCryptography::symmetric' || $elements[0] == 'fCryptography::symmetric-php') {} else {
throw new fProgrammerException(
'The ciphertext provided does not appear to have been encrypted using %s',
__CLASS__ . '::symmetricKeyEncrypt()'
);
}
$iv = base64_decode($elements[1]);
$ciphertext = base64_decode($elements[2]);
$provided_hmac = $elements[3];
$hmac = self::hashHMAC('sha1', $iv . '#' . $ciphertext, $secret_key);
// By verifying the HMAC we ensure the integrity of the data
if ($hmac != $provided_hmac) {
throw new fValidationException(
'The ciphertext provided appears to have been tampered with or corrupted'
);
}
// This code uses the Rijndael cipher with a 192 bit block size and a 256 bit key in cipher feedback mode
$key = substr(sha1($secret_key), 0, 32);
if (class_exists('Crypt_Rijndael', false) && $elements[0] == 'fCryptography::symmetric-php') {
// Initialize Rijndael
$crypt = new Crypt_Rijndael(CRYPT_RIJNDAEL_MODE_CFB);
$crypt->setKeyLength(256);
$crypt->setBlockLength(24);
$crypt->disablePadding();
// Set key and iv
$crypt->setKey($key);
$crypt->setIV($iv);
// Main decryption
$plaintext = $crypt->decrypt($ciphertext);
// Clean up
unset($crypt);
}
elseif ($elements[0] == 'fCryptography::symmetric') {
self::verifySymmetricKeyEnvironment();
// Set up the main decryption, we are gonna use AES-192 (also know as rijndael-192) in cipher feedback mode
$module = mcrypt_module_open('rijndael-192', '', 'cfb', '');
mcrypt_generic_init($module, $key, $iv);
fCore::startErrorCapture(E_WARNING);
$plaintext = mdecrypt_generic($module, $ciphertext);
fCore::stopErrorCapture();
mcrypt_generic_deinit($module);
mcrypt_module_close($module);
}
else {
if ($elements[0] == 'fCryptography::symmetric') {
throw new fProgrammerException(
'The ciphertext provided cannot be decrypted because the required PHP extension %s is not installed',
'mcrypt'
);
}
else {
throw new fProgrammerException(
'The ciphertext provided cannot be decrypted because the required PHP library %s is not available',
'PHPSecLib (Crypt/Rijndael)'
);
}
}
return $plaintext;
}
/**
* Encrypts the passed data using symmetric-key encryption
*
* Since this is symmetric-key cryptography, the same key is used for
* encryption and decryption.
*
* @throws fValidationException When the $secret_key is less than 8 characters long
*
* @param string $plaintext The content to be encrypted
* @param string $secret_key The secret key to use for encryption - must be at least 8 characters
* @return string An encrypted and base-64 encoded result containing a Flourish fingerprint and suitable for decryption using ::symmetricKeyDecrypt()
*/
static public function symmetricKeyEncrypt($plaintext, $secret_key)
{
if (strlen($secret_key) < 8) {
throw new fValidationException(
'The secret key specified does not meet the minimum requirement of being at least %s characters long',
8
);
}
// This code uses the Rijndael cipher with a 192 bit block size and a
// 256 bit key in cipher feedback mode. Cipher feedback mode is chosen
// because no extra padding is added, ensuring we always get the exact
// same plaintext out of the decrypt method
$key = substr(sha1($secret_key), 0, 32);
$iv = base64_decode(self::randomString(32, 'base64'));
if (class_exists('Crypt_Rijndael', false)) {
$phpseclib = true;
// Initialize Rijndael
$crypt = new Crypt_Rijndael(CRYPT_RIJNDAEL_MODE_CFB);
$crypt->setKeyLength(256);
$crypt->setBlockLength(24);
$crypt->disablePadding();
// Set key and iv
$crypt->setKey($key);
$crypt->setIV($iv);
// Main encryption
$ciphertext = $crypt->encrypt($plaintext);
// Clean up
unset($crypt);
}
else {
$phpseclib = false;
self::verifySymmetricKeyEnvironment();
$module = mcrypt_module_open('rijndael-192', '', 'cfb', '');
// Finish the main encryption
mcrypt_generic_init($module, $key, $iv);
fCore::startErrorCapture(E_WARNING);
$ciphertext = mcrypt_generic($module, $plaintext);
fCore::stopErrorCapture();
// Clean up the main encryption
mcrypt_generic_deinit($module);
mcrypt_module_close($module);
}
// Here we are generating the HMAC for the encrypted data to ensure data integrity
$hmac = self::hashHMAC('sha1', $iv . '#' . $ciphertext, $secret_key);
// All of the data is then encoded using base64 to prevent issues with character sets
$encoded_iv = base64_encode($iv);
$encoded_ciphertext = base64_encode($ciphertext);
// Indicate in the resulting encrypted data what the encryption tool was
$return = 'fCryptography::symmetric';
$return .= $phpseclib ? '-php' : '';
$return .='#' . $encoded_iv . '#' . $encoded_ciphertext . '#' . $hmac;
return $return;
}
/**
* Makes sure the required PHP extensions and library versions are all correct
*
* @return void
*/
static private function verifyPublicKeyEnvironment()
{
if (!extension_loaded('openssl')) {
throw new fEnvironmentException(
'The PHP %s extension is required, however is does not appear to be loaded',
'openssl'
);
}
}
/**
* Makes sure the required PHP extensions and library versions are all correct
*
* @return void
*/
static private function verifySymmetricKeyEnvironment()
{
if (!extension_loaded('mcrypt')) {
throw new fEnvironmentException(
'The PHP %s extension is required, however is does not appear to be loaded',
'mcrypt'
);
}
if (!function_exists('mcrypt_module_open')) {
throw new fEnvironmentException(
'The cipher used, %1$s (also known as %2$s), requires libmcrypt version 2.4.x or newer. The version installed does not appear to meet this requirement.',
'AES-192',
'rijndael-192'
);
}
if (!in_array('rijndael-192', mcrypt_list_algorithms())) {
throw new fEnvironmentException(
'The cipher used, %1$s (also known as %2$s), does not appear to be supported by the installed version of libmcrypt',
'AES-192',
'rijndael-192'
);
}
}
/**
* Forces use as a static class
*
* @return fSecurity
*/
private function __construct() { }
}
/**
* Copyright (c) 2007-2010 Will Bond <[email protected]>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
Ticket #492 by @mattsah:
Are there any plans to add serialization of XML to fXML? It would be kinda cool if I could use fJSONencode and fXMLencode similarly. I understand there are conceptual differences between what these do, but it seems like fXMLencode is simple enough that it could just check if it was receiving a string and do it's normal string encoding… but if it were an object, perhaps it could iterate over the object creating elements for each of it's attributes or if it were an array of objects, multiple elements, returning a string of XML that could then be placed inside a root element.
It may also be cool to add support for pseudo toXML or toJSON methods for non-stdClass objects which could return custom stdClass objects with private properties etc.
Comment by @wbond:
There are a few issues I see with this:
I found a jQuery plugin that does something similar to what you are looking for: http://michalkorecki.com/content/introducing-json-xml-jquery-plugin. I see the example falling down if "students" was an array instead of a object with a single property called "student" that was an array. The later seems like it would be a more common data structure.
I wouldn't be opposed to adding such a thing if we can come up with a solid and intuitive set of rules for converting a JSON data structure to XML (just because we already know the PHP → JSON mapping rules).
Comment by @mattsah:
Some of what you raise is things that I was contemplating, and to me it all revolves around the question of why/when someone might be wanting to serialize something to XML. It seems to me both the API and recursion might benefit from an optional second parameter for use when encoding objects that would result in the string passed being used as the root element, so for example if I did the following:
$users = array(
array(
'name' => 'Matthew J. Sahagian',
'dob' => 'April 28th, 1984'
),
...
);
fXMLencode($users, 'users');
I might get something back such that:
<users>
<item index="0">
<name>Matthew J. Sahagian</name>
<dob>April 28th, 1984</dob>
</item>
<item index="1">
...
</item>
</user>
If the parameter is left null, arrays could use simply "data" while objects could perhaps default to the underscorized class name, or only in the event of stdclass… data.
Where keys/property names are used the element would obviously reflect these as in the above example.
I think data / item are good defaults for roots + numerically index array elements.
Obviously if someone creates an insanely nested array with no keys and then complains about all the recursive "data" elements, it is their own fault.
In the event of a numeric key pointing to an array you might get something like:
<data>
<item index="0">
<data>
...
</data>
</item>
</data>
As I mentioned when I opened, I feel the big question is going to be a matter of how it tends to be used by people… I think it's fairly rare that someone will be serializing non-associative arrays. And objects have the very useful distinction of having their property names to identify things. Perhaps this is at least a start to the concept? Obviously, the onus to create the desired format will be on the user giving keys/properties as appropriate to their arrays. As mentioned with a conception for _toJSON and _toXML (how do you escape tow underscores in this thing?) I feel this will likely get used the most in models, whereby the class name will be a good indicator as to what to call an element.
$users = Users::build(array('username~' => 'matt'));
fXML::encode($users->getRecords(), 'record_set');
<record_set>
<user>
<first_name>Matthew</first_name>
<last_name>Sahagian</last_name>
...
</user>
...
</record_set>
Comment by @wbond:
After talking with some different developers, here is the idea I have:
Does this sound like a solution that would work for you?
Comment by @mattsah:
This seems quite alright and as far as I can tell quite in line with what I originally had in mind. It seems in most cases that common XML subformats or microformats are fairly simple and I would imagine most people will be using them with appropriately structured objects or associative arrays, but it will be good to have the option to override both.
With regards to scalar values, indeed!
Comment by @wbond:
When I was at PHP|Tek 11, one of the guys from Frapi did a presentation and talked about how they support both XML and JSON. There was some discussion about how they translated arrays into XML. Since they have some traction in the PHP API arena, I think it would be good to emulate them as long as it isn't too crazy.
Ticket #601 by @jeffturcotte:
Would be nice to have a validation hook that gets called AFTER message reordering and replacement. Alternatively, post::validate could be moved, or message reordering and replacement could simply use post::validate() instead of being standalone. But both of those require more work and potential compatibility breaks.
(for reference fActiveRecord, lines 2901-2903)
https://github.com/tatsh/sutra/blob/master/patches/flourish-r1041-protected-determineprocessor.patch
From an extension class, need easy access to the operations queue.
Ticket #418 by @wbond:
fDate should be able to handle dates before 1901 and after 2038
Ticket #741 by @rcrisman
I create an sqLite database using flourish but when I go to load the database I get
the sqlite file is version 3.7.7.1
php version 5.4
[05-Mar-2012 16:40:00 UTC] PHP Fatal error: Uncaught exception 'fConnectivityException' with message 'The database specified does not appear to be a valid SQLite v2.1 or v3 database' in E:\public_html\inc\flourish\fDatabase.php:956
Stack trace:
#0 E:\public_html\inc\flourish\fDatabase.php(331): fDatabase->determineExtension()
#1 E:\public_html\inc\init.php(9): fDatabase->__construct('sqlite', 'E:\public_html/...')
#2 E:\public_html\index.php(2): include_once('E:\public_html\...')
#3 {main}
thrown in E:\public_html\inc\flourish\fDatabase.php on line 956
Something like:
<?php
fCRUD::makeForm(string $orm_class, string $post_url, string $method = 'post', array $field_type_overrides = array(), array $unwanted_fields = array());
fCRUD::registerFormFieldCallback(string $orm_class_or_star, string $field_type, string $callback);
fCRUD::makeTableForm(string $orm_class, string $post_url, string $method = 'post', array $these_fields_only = NULL);
fCRUD::registerTableFormCallback(string $orm_class_or_star, string $column_name, string|array $callback, array $callback_args = array());
<?php
$html = fCRUD::makeForm($orm_class, '/place-to-post');
function cb($html, $attr = array()) {
return '<div class="form-textfield-container"><input type="type" name="'.$attr['name'].'"></div>';
}
// Register a callback for all textfields rendered
fCRUD::registerFormFieldCallback('*', 'textfield', 'cb');
// Override some column types
$html = fCRUD::makeForm($orm_class, array('email_address' => 'email', 'user_password' => 'password'));
// Table with checkboxes
$html = fCRUD::makeTableForm($orm_class);
// Table with checkboxes, only the selected columns, callbacks, 2 signatures
fCrud::registerTableFormCallback($orm_class, 'date_created', array($fdate_obj, 'format'), array('U'));
$html = fCRUD::makeTableForm($orm_class, '/place-to-post', 'post', array('name', 'date_created'));
Ticket #573 by @mattsah:
Since the minification mode acts as a wrapper to CSS + JS anyhow, would it be possible to start an output buffer and include the CSS and JS so that PHP could be inlined in CSS and Javascript, the get the contents of the buffer and write that out to the cache files? This would be an awesome enhancement for AJAX based Javascript as if you are using a routing library or something you'd be able to use your normal link methods rather than having to hard code URLs, etc.
Comment by @wbond:
I understand the general concept of what you are looking for. Could you write some example code that would show how you think it would best work?
Comment by @mattsah:
Will, in my code I have done this fairly simply — although it is not an independently configurable feature it just becomes a "built in feature" of the minfication:
Starting on line 779 of fTemplating I have changed the code in the else statement as follows:
ob_start();
include $path;
$minified_path = ob_get_clean();
$minified_path = trim($this->minify($minified_path, $type));
file_put_contents($path_cache_file, $minified_path);
What follows is a piece of my Javascript code which gets minified (sans callback function):
jQuery.ajax({
type : 'GET',
url : <%= fJSON::encode(iw::makeLink(
iw::makeTarget('PastebinController', 'comments'),
array(
':hashid' => fRequest::get('hashid'),
':title' => fRequest::get('title')
)
)) %>,
success: function(data){
...
}
});
You can see from the above example I am able to inline the PHP to work with the router to get the appropriate link for the comments on the particular Paste retrieved by my PastebinController? — the iw::makeLink() method works with the router to construct the appropriate link from the provided information — if I change that route, my Javascript will change nicely with it.
Here's the problems I see:
$template = new fTemplating();
$template->enableMinification('development', $cache_directory);
$template->enableGlobalPHP();
An exception could then be thrown if enableGlobalPHP() was called without enabling Minification.
My thoughts were simply that because minification mode opens the file anyway and writes out to cache regardless, including would add little overhead, and a good amount of benefit for scenarios as pointed out above. It would also be useful for simple PHP enhancement of CSS.
<%= CSS::makeGradient(CSS::VERTICAL_GRADIENT, '#000', '#fff') %>
Output:
background-color: #000;
background: -moz-linear-gradient(top, #000 0%, #fff 100%); /* firefox */
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #000), color-stop(100%, #fff)); /* webkit */
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#000', endColorstr='#fff', GradientType=0); /* ie */
Ticket #369 by vxnick:
Consider making fURL::get() return the entire URL - including the domain and query string (if set). Alternatively, add a new method to return the entire URL, perhaps fURL::getURL().
Comment by vxnick:
Perhaps consider adding fURL::getProtocol() to return http://, https://, ftp://, etc, and to save on manually checking whether SSL is set in $_SERVER or the port is 443, etc.
Comment by vxnick:
fURL::getAnchor() might be another useful method - return an anchor if it exists (some/url#anchor).
Comment by @wbond:
Yeah, it seems like there should be:
fURL::getProtocol() - returns http://, etc
fURL::getHost() - returns example.com, with any non-standard port
fURL::getPath() - the current fURL::get(), /path/to/script
fURL::getQuery() - return ?key=value
Unfortunately the anchor is never sent to the server, so we can't return that.
The other issue I see is methods that return a combination of the above parts. For instance the protocol and host are often used together (currently fURL::getDomain()). Perhaps fURL::get() should return the whole URL by default, but optionally allow the programmer to pass in the parts they want?
fURL::get() - the whole URL http://example.com:81/path/to/script?key=value
fURL::get('protocol', 'host') - http://example.com:81
fURL::get('path', 'query') - /path/to/script?key=value
Or perhaps all of the different methods should be discarded for a single fURL::get() which you can pass elements into? This would also make it so it could allow schema (http) and port (80) without extra methods.
Comment by vxnick:
I like the idea of passing elements into fURL::get() - that seems like a really nice solution.
Comment by mantaworks:
The problem with passing elements in is that they might not go together. What's the expected output of fURLget('protocol', 'path') or fURLget('query', 'host')? There's ambiguity.
I'd say a method like fURL::getAllParts() or fURL::split() that returns an associative array with all the parts named would be the way to go. You'd still have the parts you're interested in displayed clearly, without being extremely verbose.
Comment by alexleeds:
According to the php parse_url function (http://php.net/manual/en/function.parse-url.php) it is possible to get the "fragment" (aka the part after the #)
parse_url('http://username:password@hostname/path?arg=value#anchor') returns:
Array
(
[scheme] => http
[host] => hostname
[user] => username
[pass] => password
[path] => /path
[query] => arg=value
[fragment] => anchor
)
Comment by @wbond:
parse_url() will return the fragment if you pass in a URL that contains one. The issue is that browsers don't send the fragment to the web server, so we can't get it.
Comment by @wbond:
@mantaworks
How about allowing only a single element to be specified, but with a < before or > after to indicate the element requested plus everything before it or everything after it?
// Would return http://example.com
fURL::get('<hostname');
// Would return /path/to/script?key=val
fURL::get('path>');
When working with URLs we are all looking for a way to get a single result usually containing the most specific, or least specific pieces.
Ticket #579 by @wbond:
fImage should be enhanced to support BMP images by adding a custom parser for BMPs and GD.
In addition, error messaging should be enhanced to tell the user what kind of file was provided if not an image file.
This is a result of ticket #578
Comment by doccie:
/* * ****************************************** */
/* Fonction: ImageCreateFromBMP */
/* Author: DHKold */
/* Contact: [email protected] */
/* Date: The 15th of June 2005 */
/* Version: 2.0B */
/* * ****************************************** */
public static function ImageCreateFromBMP($filename) {
// open file in binary mode
if (!$f1 = fopen($filename, "rb"))
return FALSE;
//1 : get headers
$FILE = unpack("vfile_type/Vfile_size/Vreserved/Vbitmap_offset", fread($f1, 14));
if ($FILE['file_type'] != 19778)
return FALSE;
//2 : get contents
$BMP = unpack('Vheader_size/Vwidth/Vheight/vplanes/vbits_per_pixel' .
'/Vcompression/Vsize_bitmap/Vhoriz_resolution' .
'/Vvert_resolution/Vcolors_used/Vcolors_important', fread($f1, 40));
$BMP['colors'] = pow(2, $BMP['bits_per_pixel']);
if ($BMP['size_bitmap'] == 0)
$BMP['size_bitmap'] = $FILE['file_size'] - $FILE['bitmap_offset'];
$BMP['bytes_per_pixel'] = $BMP['bits_per_pixel'] / 8;
$BMP['bytes_per_pixel2'] = ceil($BMP['bytes_per_pixel']);
$BMP['decal'] = ($BMP['width'] * $BMP['bytes_per_pixel'] / 4);
$BMP['decal'] -= floor($BMP['width'] * $BMP['bytes_per_pixel'] / 4);
$BMP['decal'] = 4 - (4 * $BMP['decal']);
if ($BMP['decal'] == 4)
$BMP['decal'] = 0;
//3 : get color palette
$PALETTE = array();
if ($BMP['colors'] < 16777216) {
$PALETTE = unpack('V' . $BMP['colors'], fread($f1, $BMP['colors'] * 4));
}
//4 : create image
$IMG = fread($f1, $BMP['size_bitmap']);
$VIDE = chr(0);
$res = imagecreatetruecolor($BMP['width'], $BMP['height']);
$P = 0;
$Y = $BMP['height'] - 1;
while ($Y >= 0) {
$X = 0;
while ($X < $BMP['width']) {
if ($BMP['bits_per_pixel'] == 24)
$COLOR = unpack("V", substr($IMG, $P, 3) . $VIDE);
elseif ($BMP['bits_per_pixel'] == 16) {
$COLOR = unpack("n", substr($IMG, $P, 2));
$COLOR[1] = $PALETTE[$COLOR[1] + 1];
} elseif ($BMP['bits_per_pixel'] == 8) {
$COLOR = unpack("n", $VIDE . substr($IMG, $P, 1));
$COLOR[1] = $PALETTE[$COLOR[1] + 1];
} elseif ($BMP['bits_per_pixel'] == 4) {
$COLOR = unpack("n", $VIDE . substr($IMG, floor($P), 1));
if (($P * 2) % 2 == 0)
$COLOR[1] = ($COLOR[1] >> 4); else
$COLOR[1] = ($COLOR[1] & 0x0F);
$COLOR[1] = $PALETTE[$COLOR[1] + 1];
}
elseif ($BMP['bits_per_pixel'] == 1) {
$COLOR = unpack("n", $VIDE . substr($IMG, floor($P), 1));
if (($P * 8) % 8 == 0)
$COLOR[1] = $COLOR[1] >> 7;
elseif (($P * 8) % 8 == 1)
$COLOR[1] = ($COLOR[1] & 0x40) >> 6;
elseif (($P * 8) % 8 == 2)
$COLOR[1] = ($COLOR[1] & 0x20) >> 5;
elseif (($P * 8) % 8 == 3)
$COLOR[1] = ($COLOR[1] & 0x10) >> 4;
elseif (($P * 8) % 8 == 4)
$COLOR[1] = ($COLOR[1] & 0x8) >> 3;
elseif (($P * 8) % 8 == 5)
$COLOR[1] = ($COLOR[1] & 0x4) >> 2;
elseif (($P * 8) % 8 == 6)
$COLOR[1] = ($COLOR[1] & 0x2) >> 1;
elseif (($P * 8) % 8 == 7)
$COLOR[1] = ($COLOR[1] & 0x1);
$COLOR[1] = $PALETTE[$COLOR[1] + 1];
}
else
return FALSE;
imagesetpixel($res, $X, $Y, $COLOR[1]);
$X++;
$P += $BMP['bytes_per_pixel'];
}
$Y--;
$P+=$BMP['decal'];
}
// close file
fclose($f1);
return $res;
}
This seems to be accepted on php.net as a good function for creating images resources from BMPs.
Comment by doccie:
Right now I'm using this code and that seems to do the trick. Slightly dodgy to automatically assume it's a BMP, but for my current purposes, it will suffice.
Thanks again for the help!
try {
$file = new fImage($tmp_file, true);
} catch(fException $e) {
//die($e->getMessage());
// probably means this is a BMP file
$res = papUtils::ImageCreateFromBMP($tmp_file);
imagejpeg($res, $tmp_file);
try {
$file = new fImage($tmp_file, true);
} catch(fException $e) {
die($e->getMessage());
}
}
Comment by @wbond:
I saw that code too. Unfortunately, as it stands right now I can't use it because it does not have a license attached, which means it is strictly copyright DHKold. Further, dhkold.com no longer seems active, so there is no way to contact the author to ask for permission.
I'll either need to find another implementation that is properly licensed, have someone write a cleanroom implementation (or an implementation based off of an MIT or BSD licensed work), or write it from scratch myself.
Ticket #747 by @xjia
I think we need something like the routing functionality in http://www.slimframework.com/
Currently I'm using flourish together with Slim but somewhere of them don't fit well with each other. If there were something like fRouter?, it will be perfect. :-)
ee http://www.slimframework.com/documentation/stable#routes for details of HTTP routing in Slim.
Comment by @mblarsen
This question has come up a few times before. Will usually recommend Moor as a routing companion for flourish https://github.com/jeffturcotte/moor
Maybe that is a better fit for your needs than Slim's router.
Comment by @xjia
Aha! Yeah you are right. Moor seems good. Thanks.
Ticket #539 by @wbond:
There should be an fUnitOfWork class to bundling up ORM operations into a single transaction
https://github.com/tatsh/sutra/blob/master/classes/sGrammar.php#L74
Can use fURL::makeFriendly()
for most strings but not camelCase.
Ticket #743 @pjbeardsley
Not clear what the parameters for the callback should be.
Ticket #742 by @pjbeardsley
Referenced in the class documentation, exists in the current version of the class, but no entry in the API docs.
Ticket #485 by @craigruks:
The following patch allows the populate action for related records to recursively call the populate action for further descendants.
Comment by @wbond:
There is a new fully recursive patch that will be sent as a pull request
fJSON::encodeWithPadding($data, $callback_name);
where $callback_name is a valid JavaScript identifier.
https://github.com/tatsh/sutra/blob/master/classes/sJSONP.php#L14
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.