diff --git a/CONTRIBUTORS b/CONTRIBUTORS new file mode 100644 index 0000000..159baba --- /dev/null +++ b/CONTRIBUTORS @@ -0,0 +1 @@ +Nicolas Lassale \ No newline at end of file diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..fcdce90 --- /dev/null +++ b/COPYING @@ -0,0 +1,461 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 2.1, February 1999 + + Copyright (C) 1991, 1999 Free Software Foundation, Inc. + 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + +[This is the first released version of the Lesser GPL. It also counts + as the successor of the GNU Library Public License, version 2, hence + the version number 2.1.] + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +Licenses are intended to guarantee your freedom to share and change +free software--to make sure the software is free for all its users. + + This license, the Lesser General Public License, applies to some +specially designated software packages--typically libraries--of the +Free Software Foundation and other authors who decide to use it. You +can use it too, but we suggest you first think carefully about whether +this license or the ordinary General Public License is the better +strategy to use in any particular case, based on the explanations below. + + When we speak of free software, we are referring to freedom of use, +not price. Our General Public Licenses are designed to make sure that +you have the freedom to distribute copies of free software (and charge +for this service if you wish); that you receive source code or can get +it if you want it; that you can change the software and use pieces of +it in new free programs; and that you are informed that you can do +these things. + + To protect your rights, we need to make restrictions that forbid +distributors to deny you these rights or to ask you to surrender these +rights. These restrictions translate to certain responsibilities for +you if you distribute copies of the library or if you modify it. + + For example, if you distribute copies of the library, whether gratis +or for a fee, you must give the recipients all the rights that we gave +you. You must make sure that they, too, receive or can get the source +code. If you link other code with the library, you must provide +complete object files to the recipients, so that they can relink them +with the library after making changes to the library and recompiling +it. And you must show them these terms so they know their rights. + + We protect your rights with a two-step method: (1) we copyright the +library, and (2) we offer you this license, which gives you legal +permission to copy, distribute and/or modify the library. + + To protect each distributor, we want to make it very clear that +there is no warranty for the free library. Also, if the library is +modified by someone else and passed on, the recipients should know +that what they have is not the original version, so that the original +author's reputation will not be affected by problems that might be +introduced by others. + + Finally, software patents pose a constant threat to the existence of +any free program. We wish to make sure that a company cannot +effectively restrict the users of a free program by obtaining a +restrictive license from a patent holder. Therefore, we insist that +any patent license obtained for a version of the library must be +consistent with the full freedom of use specified in this license. + + Most GNU software, including some libraries, is covered by the +ordinary GNU General Public License. This license, the GNU Lesser +General Public License, applies to certain designated libraries, and +is quite different from the ordinary General Public License. We use +this license for certain libraries in order to permit linking those +libraries into non-free programs. + + When a program is linked with a library, whether statically or using +a shared library, the combination of the two is legally speaking a +combined work, a derivative of the original library. The ordinary +General Public License therefore permits such linking only if the +entire combination fits its criteria of freedom. The Lesser General +Public License permits more lax criteria for linking other code with +the library. + + We call this license the "Lesser" General Public License because it +does Less to protect the user's freedom than the ordinary General +Public License. It also provides other free software developers Less +of an advantage over competing non-free programs. These disadvantages +are the reason we use the ordinary General Public License for many +libraries. However, the Lesser license provides advantages in certain +special circumstances. + + For example, on rare occasions, there may be a special need to +encourage the widest possible use of a certain library, so that it becomes +a de-facto standard. To achieve this, non-free programs must be +allowed to use the library. A more frequent case is that a free +library does the same job as widely used non-free libraries. In this +case, there is little to gain by limiting the free library to free +software only, so we use the Lesser General Public License. + + In other cases, permission to use a particular library in non-free +programs enables a greater number of people to use a large body of +free software. For example, permission to use the GNU C Library in +non-free programs enables many more people to use the whole GNU +operating system, as well as its variant, the GNU/Linux operating +system. + + Although the Lesser General Public License is Less protective of the +users' freedom, it does ensure that the user of a program that is +linked with the Library has the freedom and the wherewithal to run +that program using a modified version of the Library. + + The precise terms and conditions for copying, distribution and +modification follow. Pay close attention to the difference between a +"work based on the library" and a "work that uses the library". The +former contains code derived from the library, whereas the latter must +be combined with the library in order to run. + + GNU LESSER GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License Agreement applies to any software library or other +program which contains a notice placed by the copyright holder or +other authorized party saying it may be distributed under the terms of +this Lesser General Public License (also called "this License"). +Each licensee is addressed as "you". + + A "library" means a collection of software functions and/or data +prepared so as to be conveniently linked with application programs +(which use some of those functions and data) to form executables. + + The "Library", below, refers to any such software library or work +which has been distributed under these terms. A "work based on the +Library" means either the Library or any derivative work under +copyright law: that is to say, a work containing the Library or a +portion of it, either verbatim or with modifications and/or translated +straightforwardly into another language. (Hereinafter, translation is +included without limitation in the term "modification".) + + "Source code" for a work means the preferred form of the work for +making modifications to it. For a library, complete source code means +all the source code for all modules it contains, plus any associated +interface definition files, plus the scripts used to control compilation +and installation of the library. + + Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running a program using the Library is not restricted, and output from +such a program is covered only if its contents constitute a work based +on the Library (independent of the use of the Library in a tool for +writing it). Whether that is true depends on what the Library does +and what the program that uses the Library does. + + 1. You may copy and distribute verbatim copies of the Library's +complete source code as you receive it, in any medium, provided that +you conspicuously and appropriately publish on each copy an +appropriate copyright notice and disclaimer of warranty; keep intact +all the notices that refer to this License and to the absence of any +warranty; and distribute a copy of this License along with the +Library. + + You may charge a fee for the physical act of transferring a copy, +and you may at your option offer warranty protection in exchange for a +fee. + + 2. You may modify your copy or copies of the Library or any portion +of it, thus forming a work based on the Library, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) The modified work must itself be a software library. + + b) You must cause the files modified to carry prominent notices + stating that you changed the files and the date of any change. + + c) You must cause the whole of the work to be licensed at no + charge to all third parties under the terms of this License. + + d) If a facility in the modified Library refers to a function or a + table of data to be supplied by an application program that uses + the facility, other than as an argument passed when the facility + is invoked, then you must make a good faith effort to ensure that, + in the event an application does not supply such function or + table, the facility still operates, and performs whatever part of + its purpose remains meaningful. + + (For example, a function in a library to compute square roots has + a purpose that is entirely well-defined independent of the + application. Therefore, Subsection 2d requires that any + application-supplied function or table used by this function must + be optional: if the application does not supply it, the square + root function must still compute square roots.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Library, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Library, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote +it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Library. + +In addition, mere aggregation of another work not based on the Library +with the Library (or with a work based on the Library) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may opt to apply the terms of the ordinary GNU General Public +License instead of this License to a given copy of the Library. To do +this, you must alter all the notices that refer to this License, so +that they refer to the ordinary GNU General Public License, version 2, +instead of to this License. (If a newer version than version 2 of the +ordinary GNU General Public License has appeared, then you can specify +that version instead if you wish.) Do not make any other change in +these notices. + + Once this change is made in a given copy, it is irreversible for +that copy, so the ordinary GNU General Public License applies to all +subsequent copies and derivative works made from that copy. + + This option is useful when you wish to copy part of the code of +the Library into a program that is not a library. + + 4. You may copy and distribute the Library (or a portion or +derivative of it, under Section 2) in object code or executable form +under the terms of Sections 1 and 2 above provided that you accompany +it with the complete corresponding machine-readable source code, which +must be distributed under the terms of Sections 1 and 2 above on a +medium customarily used for software interchange. + + If distribution of object code is made by offering access to copy +from a designated place, then offering equivalent access to copy the +source code from the same place satisfies the requirement to +distribute the source code, even though third parties are not +compelled to copy the source along with the object code. + + 5. A program that contains no derivative of any portion of the +Library, but is designed to work with the Library by being compiled or +linked with it, is called a "work that uses the Library". Such a +work, in isolation, is not a derivative work of the Library, and +therefore falls outside the scope of this License. + + However, linking a "work that uses the Library" with the Library +creates an executable that is a derivative of the Library (because it +contains portions of the Library), rather than a "work that uses the +library". The executable is therefore covered by this License. +Section 6 states terms for distribution of such executables. + + When a "work that uses the Library" uses material from a header file +that is part of the Library, the object code for the work may be a +derivative work of the Library even though the source code is not. +Whether this is true is especially significant if the work can be +linked without the Library, or if the work is itself a library. The +threshold for this to be true is not precisely defined by law. + + If such an object file uses only numerical parameters, data +structure layouts and accessors, and small macros and small inline +functions (ten lines or less in length), then the use of the object +file is unrestricted, regardless of whether it is legally a derivative +work. (Executables containing this object code plus portions of the +Library will still fall under Section 6.) + + Otherwise, if the work is a derivative of the Library, you may +distribute the object code for the work under the terms of Section 6. +Any executables containing that work also fall under Section 6, +whether or not they are linked directly with the Library itself. + + 6. As an exception to the Sections above, you may also combine or +link a "work that uses the Library" with the Library to produce a +work containing portions of the Library, and distribute that work +under terms of your choice, provided that the terms permit +modification of the work for the customer's own use and reverse +engineering for debugging such modifications. + + You must give prominent notice with each copy of the work that the +Library is used in it and that the Library and its use are covered by +this License. You must supply a copy of this License. If the work +during execution displays copyright notices, you must include the +copyright notice for the Library among them, as well as a reference +directing the user to the copy of this License. Also, you must do one +of these things: + + a) Accompany the work with the complete corresponding + machine-readable source code for the Library including whatever + changes were used in the work (which must be distributed under + Sections 1 and 2 above); and, if the work is an executable linked + with the Library, with the complete machine-readable "work that + uses the Library", as object code and/or source code, so that the + user can modify the Library and then relink to produce a modified + executable containing the modified Library. (It is understood + that the user who changes the contents of definitions files in the + Library will not necessarily be able to recompile the application + to use the modified definitions.) + + b) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (1) uses at run time a + copy of the library already present on the user's computer system, + rather than copying library functions into the executable, and (2) + will operate properly with a modified version of the library, if + the user installs one, as long as the modified version is + interface-compatible with the version that the work was made with. + + c) Accompany the work with a written offer, valid for at + least three years, to give the same user the materials + specified in Subsection 6a, above, for a charge no more + than the cost of performing this distribution. + + d) If distribution of the work is made by offering access to copy + from a designated place, offer equivalent access to copy the above + specified materials from the same place. + + e) Verify that the user has already received a copy of these + materials or that you have already sent this user a copy. + + For an executable, the required form of the "work that uses the +Library" must include any data and utility programs needed for +reproducing the executable from it. However, as a special exception, +the materials to be distributed need not include anything that is +normally distributed (in either source or binary form) with the major +components (compiler, kernel, and so on) of the operating system on +which the executable runs, unless that component itself accompanies +the executable. + + It may happen that this requirement contradicts the license +restrictions of other proprietary libraries that do not normally +accompany the operating system. Such a contradiction means you cannot +use both them and the Library together in an executable that you +distribute. + + 7. You may place library facilities that are a work based on the +Library side-by-side in a single library together with other library +facilities not covered by this License, and distribute such a combined +library, provided that the separate distribution of the work based on +the Library and of the other library facilities is otherwise +permitted, and provided that you do these two things: + + a) Accompany the combined library with a copy of the same work + based on the Library, uncombined with any other library + facilities. This must be distributed under the terms of the + Sections above. + + b) Give prominent notice with the combined library of the fact + that part of it is a work based on the Library, and explaining + where to find the accompanying uncombined form of the same work. + + 8. You may not copy, modify, sublicense, link with, or distribute +the Library except as expressly provided under this License. Any +attempt otherwise to copy, modify, sublicense, link with, or +distribute the Library is void, and will automatically terminate your +rights under this License. However, parties who have received copies, +or rights, from you under this License will not have their licenses +terminated so long as such parties remain in full compliance. + + 9. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Library or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Library (or any work based on the +Library), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Library or works based on it. + + 10. Each time you redistribute the Library (or any work based on the +Library), the recipient automatically receives a license from the +original licensor to copy, distribute, link with or modify the Library +subject to these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties with +this License. + + 11. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Library at all. For example, if a patent +license would not permit royalty-free redistribution of the Library by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Library. + +If any portion of this section is held invalid or unenforceable under any +particular circumstance, the balance of the section is intended to apply, +and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 12. If the distribution and/or use of the Library is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Library under this License may add +an explicit geographical distribution limitation excluding those countries, +so that distribution is permitted only in or among countries not thus +excluded. In such case, this License incorporates the limitation as if +written in the body of this License. + + 13. The Free Software Foundation may publish revised and/or new +versions of the Lesser General Public License from time to time. +Such new versions will be similar in spirit to the present version, +but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Library +specifies a version number of this License which applies to it and +"any later version", you have the option of following the terms and +conditions either of that version or of any later version published by +the Free Software Foundation. If the Library does not specify a +license version number, you may choose any version ever published by +the Free Software Foundation. + + 14. If you wish to incorporate parts of the Library into other free +programs whose distribution conditions are incompatible with these, +write to the author to ask for permission. For software which is +copyrighted by the Free Software Foundation, write to the Free +Software Foundation; we sometimes make exceptions for this. Our +decision will be guided by the two goals of preserving the free status +of all derivatives of our free software and of promoting the sharing +and reuse of software generally. + + NO WARRANTY + + 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO +WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. +EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR +OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY +KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE +LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME +THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN +WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY +AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU +FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR +CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE +LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING +RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A +FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF +SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGES. + + END OF TERMS AND CONDITIONS + + + diff --git a/apps/todos/README b/apps/todos/README new file mode 100644 index 0000000..d30b446 --- /dev/null +++ b/apps/todos/README @@ -0,0 +1,36 @@ + + Plume Framework Test Application +---------------------------------- + +This test application is a simple TODO list manager. + +This test application is here to expose in a simple way the +functionalities of the Plume Framework aka "Pluf" and to propose a +"best practice" example on the way to structure your application using +Pluf. + + Installation +-------------- + +1) In src/Todo/conf/ copy todo.php-dist as todo.php. + +2) Edit src/Todo/conf/toto.php and set your database information + together with the 'tmp_folder'. + +3) Edit www/index.php and ensure that the variable $path_to_Pluf + is pointing to the folder where your Pluf.php file is. + +4) Point your web browser to /testapp/index.php?_px_action=/install/ it will + create the necessary tables to test the Todo application. + +5) Go to /testapp/index.php and enjoy this little todo application. + + Uninstallation +---------------- + +1) Point your web browser to /testapp/index.php?_px_action=/uninstall/ + +2) Delete the files in the 'tmp_folder'. + +3) Delete the /testapp/conf/testapp.php file. + diff --git a/apps/todos/src/Todo/Form/List.php b/apps/todos/src/Todo/Form/List.php new file mode 100644 index 0000000..5da9724 --- /dev/null +++ b/apps/todos/src/Todo/Form/List.php @@ -0,0 +1,39 @@ +fields['name'] = new Pluf_Form_Field_Varchar( + array('required' => true, + 'label' => __('List name'), + 'help_text' => __('For example: "Happy stuff".'), + )); + } +} + + diff --git a/apps/todos/src/Todo/Item.php b/apps/todos/src/Todo/Item.php new file mode 100644 index 0000000..9d073a0 --- /dev/null +++ b/apps/todos/src/Todo/Item.php @@ -0,0 +1,111 @@ +_a['table'] = 'todo_items'; + + /** + * The name of the model in the class definition. + */ + $this->_a['model'] = 'Todo_Item'; + + /** + * The definition of the model. Each key of the associative array + * corresponds to a "column" and the definition of the column is + * given in the corresponding array. + */ + $this->_a['cols'] = array( + // It is mandatory to have an "id" column. + 'id' => + array( + 'type' => 'Pluf_DB_Field_Sequence', + //It is automatically added. + 'blank' => true, + ), + 'item' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 250, + // The verbose name is all lower case + 'verbose' => __('todo item'), + ), + 'completed' => + array( + 'type' => 'Pluf_DB_Field_Boolean', + 'default' => false, + 'verbose' => __('completed'), + ), + 'list' => + array( + // Here we relate the model to a Todolist + // model. This is like saying that a Todoitem + // belongs to a given Todolist + 'type' => 'Pluf_DB_Field_Foreignkey', + 'blank' => false, + 'model' => 'Todo_List', + 'verbose' => __('in list'), + 'help_text' => __('To easily manage your todo items, you are invited to organize your todo items in lists.'), + ), + ); + /** + * You can define the indexes. + * Indexes are you to sort and find elements. Here we define + * an index on the completed column to easily select the list + * of completed or not completed elements. + */ + $this->_a['idx'] = array( + 'completed_idx' => + array('col' => 'completed', + 'type' => 'normal'), + ); + $this->_a['views'] = array( + 'todo' => + array( + 'where' => 'completed=false', + ), + ); + } + + public function __toString() + { + return $this->item.(($this->completed) ? ' - Done' : ''); + } +} + diff --git a/apps/todos/src/Todo/List.php b/apps/todos/src/Todo/List.php new file mode 100644 index 0000000..7b569ac --- /dev/null +++ b/apps/todos/src/Todo/List.php @@ -0,0 +1,91 @@ +_a['table'] = 'todo_lists'; + + /** + * The name of the model in the class definition. + */ + $this->_a['model'] = 'Todo_List'; + + /** + * The definition of the model. Each key of the associative array + * corresponds to a "column" and the definition of the column is + * given in the corresponding array. + */ + $this->_a['cols'] = array( + // It is mandatory to have an "id" column. + 'id' => + array( + 'type' => 'Pluf_DB_Field_Sequence', + //It is automatically added. + 'blank' => true, + ), + 'name' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'size' => 100, + // The verbose name is all lower case + 'verbose' => __('name'), + ), + ); + /** + * You can define the indexes. + * Indexes are you to sort and find elements. Here we define + * an index on the completed column to easily select the list + * of completed or not completed elements. + */ + $this->_a['idx'] = array(); + $this->_a['views'] = array(); + } + + + /** + * To nicely render the list in the option boxes. + */ + function __toString() + { + return $this->name; + } +} diff --git a/apps/todos/src/Todo/Migrations/Install.php b/apps/todos/src/Todo/Migrations/Install.php new file mode 100644 index 0000000..d9c6330 --- /dev/null +++ b/apps/todos/src/Todo/Migrations/Install.php @@ -0,0 +1,61 @@ +model = $list; + $schema->createTables(); + $schema->model = $item; + $schema->createTables(); +} + +function Todo_Migrations_Install_teardown($params='') +{ + // The uninstallation is the reverse of the installation. + // We create the data models the same way, but instead of calling + // createTables() we call dropTables() + // You can see that without all the comments, you do not have a + // lot of lines of code. + $list = new Todo_List(); + $item = new Todo_Item(); + $db = Pluf::db(); + $schema = Pluf::factory('Pluf_DB_Schema', $db); + $schema->model = $list; + $schema->dropTables(); + $schema->model = $item; + $schema->dropTables(); +} diff --git a/apps/todos/src/Todo/Tests/TestTodo.php b/apps/todos/src/Todo/Tests/TestTodo.php new file mode 100644 index 0000000..93ba6b5 --- /dev/null +++ b/apps/todos/src/Todo/Tests/TestTodo.php @@ -0,0 +1,114 @@ +client = new Pluf_Test_Client(Pluf::f('todo_urls')); + } + + /** + * Delete the client and lists. + * + * Delete all the list which may be left. When the lists are + * deleted, the items in those list are automatically deleted too. + */ + public function tearDown() + { + $this->client = null; + foreach ($this->lists as $list) { + $list->delete(); + } + } + + public function testCreateList() + { + $list = new Todo_List(); + $list->name = 'Test list'; + $this->assertEqual(true, $list->create()); + $this->lists[] = $list; // to have it deleted in tearDown + $id = $list->id; + $nlist = new Todo_List($id); + $this->assertEqual($nlist->id, $id); + } + + public function testCreateItem() + { + $list = new Todo_List(); + $list->name = 'Test list'; + $this->assertEqual(true, $list->create()); + $this->lists[] = $list; // to have it deleted in tearDown + $item = new Todo_Item(); + $item->list = $list; + $item->item = 'Create unit tests'; + $this->assertEqual(true, $item->create()); + $nlist = $item->get_list(); + $this->assertEqual($nlist->id, $list->id); + $items = $list->get_todo_item_list(); + $this->assertEqual(1, $items->count()); + $item2 = new Todo_Item(); + $item2->list = $list; + $item2->item = 'Create more unit tests'; + $item2->create(); + // first list has 2 items. + $this->assertEqual(2, $list->get_todo_item_list()->count()); + $list2 = new Todo_List(); + $list2->name = 'Test list 2'; + $this->assertEqual(true, $list2->create()); + $this->lists[] = $list2; // to have it deleted in tearDown + $this->assertEqual(0, $list2->get_todo_item_list()->count()); + // Move the item in the second list. + $item2->list = $list2; + $item2->update(); + // One item in each list. + $this->assertEqual(1, $list2->get_todo_item_list()->count()); + $this->assertEqual(1, $list->get_todo_item_list()->count()); + } + +} \ No newline at end of file diff --git a/apps/todos/src/Todo/Views.php b/apps/todos/src/Todo/Views.php new file mode 100644 index 0000000..296af72 --- /dev/null +++ b/apps/todos/src/Todo/Views.php @@ -0,0 +1,379 @@ +getList(); + // Create a context for the template + $context = new Pluf_Template_Context(array('page_title' => 'Home', + 'lists' => $lists) + ); + // Load a template + $tmpl = new Pluf_Template('todo/index.html'); + // Render the template and send the response to the user + return new Pluf_HTTP_Response($tmpl->render($context)); + } + + + /** + * Display the viewItem page of the application. + * + * @param Pluf_HTTP_Request Request object + * @param array Matches against the regex of the dispatcher + * @return Pluf_HTTP_Response or can throw Exception + */ + public function viewItem($request, $match) + { + // Basically the same as the viewList view but with a Todo_Item + $item_id = $match[1]; + // We now are loading the corresponding item + $item = new Todo_Item($item_id); + // And check that the item has been found + if ($item->id != $item_id) { + return new Pluf_HTTP_Response_NotFound('The item has not been found.'); + } + // Now we get the list in wich the item is + $list = $item->get_list(); + // We have the item and the list, just display them. Instead of + // creating a context, then a template and then rendering it + // within a response object, we are going to use a shortcut + // function. Using shortcuts is better as you end up having a + // cleaner code. + return Pluf_Shortcuts_RenderToResponse('todo/item/view.html', + array('page_title' => 'View Item', + 'list' => $list, + 'item' => $item)); + } + + /** + * Display the addItem page of the application. + * + * @param Pluf_HTTP_Request Request object + * @param array Matches against the regex of the dispatcher + * @return Pluf_HTTP_Response or can throw Exception + */ + public function addItem($request, $match) + { + // The workflow of the addition of an item is simple + // If the request of GET method a form is displayed + // If it is a POST method, the form is submitted and the + // content is proceeded to create the new item. + // We create a Todo_Item item as we are creating one here + $item = new Todo_Item(); + $list = Pluf_Shortcuts_GetObjectOr404('Todo_List', $match[1]); + if ($request->method == 'POST') { + // We get the data submitted by the user and initialize + // the form with. + $form = Pluf_Shortcuts_GetFormForModel($item, $request->POST); + if ($form->isValid()) { + // If no errors, we can save the Todo_Item from the data + $item = $form->save(); + // We redirect the user to the page of the Todo_List in which + // we have created the item. + // We redirect the user to the page of the Todo_List + // in which we have updated the item. We are using a + // shortcut to get the URL directly from the view name + // of interest. This allows us to not hard code the + // path to the view in the view itself. + $url = Pluf_HTTP_URL_urlForView('Todo_Views::viewList', + array($item->list)); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + // As we already now the list in which we are going to add + // the item, we pass it as initial value. The user can + // change it in the select box. + $initial = array('list'=>$list->id); + $form = Pluf_Shortcuts_GetFormForModel($item, $initial); + } + // Here we are with a GET request or a POST request with errors + // So we create the rendering view of the form + // We create a new rendering view + return Pluf_Shortcuts_RenderToResponse('todo/item/add.html', + array('page_title' => 'Create a Todo Item', + 'list' => $list, + 'form' => $form)); + } + + /** + * Display the updateItem page of the application. + * + * @param Pluf_HTTP_Request Request object + * @param array Matches against the regex of the dispatcher + * @return Pluf_HTTP_Response or can throw Exception + */ + public function updateItem($request, $match) + { + // Updating an item is somehow like creating an object but you + // need first to load it to populate the form The workflow of + // the update of an item is simple If the request of GET + // method a form is displayed If it is a POST method, the form + // is submitted and the content is proceeded to update item. + // We create a Todo_Item item as we are updating one here + + // Here we are going to use another shortcut to get the item + // or return a 404 error page if failing. + $item = Pluf_Shortcuts_GetObjectOr404('Todo_Item', $match[1]); + $new_data = $item->getData(); + if ($request->method == 'POST') { + // We get the data submitted by the user + $form = Pluf_Shortcuts_GetFormForModel($item, $request->POST); + if ($form->isValid()) { + // The form is valid, we save it. + $item = $form->save(); + // We redirect the user to the page of the Todo_List + // in which we have updated the item. We are using a + // shortcut to get the URL directly from the view name + // of interest. This allows us to not hard code the + // path to the view in the view itself. + $url = Pluf_HTTP_URL_urlForView('Todo_Views::viewList', + array($item->list)); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $form = Pluf_Shortcuts_GetFormForModel($item, $item->getData()); + } + // We proceed the same way by creating a context for a template + // and providing the results to the user. + return Pluf_Shortcuts_RenderToResponse('todo/item/update.html', + array('page_title' => 'Update a Todo Item', + 'item' => $item, + 'form' => $form)); + } + + /** + * Display the deleteItem page of the application. + * + * @param Pluf_HTTP_Request Request object + * @param array Matches against the regex of the dispatcher + * @return Pluf_HTTP_Response or can throw Exception + */ + public function deleteItem($request, $match) + { + // A delete of an item is like an update. First you check that + // the item is available, then you delete it if the request is + // a POST request, else you provide a form to ask confirmation + // before deletion. + $item = Pluf_Shortcuts_GetObjectOr404('Todo_Item', $match[1]); + if ($request->method == 'POST') { + // Store the list id. + $list_id = $item->list; + // We can here directly delete the Todo_Item. Note that if + // your object is linking to other objects you need to be + // sure that you are taking into consideration the + // foreignkey and manytomany relationships. + $item->delete(); + $url = Pluf_HTTP_URL_urlForView('Todo_Views::viewList', + array($list_id)); + return new Pluf_HTTP_Response_Redirect($url); + } + // Here we are with a GET request we show a form with a + // confirmation button to delete the item. + return Pluf_Shortcuts_RenderToResponse('todo/item/delete.html', + array('page_title' => 'Delete a Todo Item', + 'item' => $item)); + } + + /** + * Display the listLists page of the application. + * + * @param Pluf_HTTP_Request Request object + * @param array Matches against the regex of the dispatcher + * @return Pluf_HTTP_Response or can throw Exception + */ + public function listLists($request, $match) + { + // The list of lists is like the home, so we just return + // the same content. + return $this->main($request, $match); + } + + /** + * Display the viewList page of the application. + * + * @param Pluf_HTTP_Request Request object + * @param array Matches against the regex of the dispatcher + * @return Pluf_HTTP_Response or can throw Exception + */ + public function viewList($request, $match) + { + // We are showing the content of the list. + // That is, all the items in the list. + $list = Pluf_Shortcuts_GetObjectOr404('Todo_List', $match[1]); + // Now we get the items in the list + $items = $list->get_todo_item_list(); + // We have the items and the list, just display them + // Create a context for the template + return Pluf_Shortcuts_RenderToResponse('todo/list/view.html', + array('page_title' => 'View List', + 'list' => $list, + 'items' => $items)); + } + + /** + * Display the updateList page of the application. + * + * @param Pluf_HTTP_Request Request object + * @param array Matches against the regex of the dispatcher + * @return Pluf_HTTP_Response or can throw Exception + */ + public function updateList($request, $match) + { + // Take a look at updateItem() to have explanations. Here you + // can see that the code is quite compact without comments. + $list = Pluf_Shortcuts_GetObjectOr404('Todo_List', $match[1]); + if ($request->method == 'POST') { + $form = new Todo_Form_List($request->POST); + if ($form->isValid()) { + $list->setFromFormData($form->cleaned_data); + $list->update(); + $url = Pluf_HTTP_URL_urlForView('Todo_Views::viewList', + array($list->id)); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $form = new Todo_Form_List($list->getData()); + } + return Pluf_Shortcuts_RenderToResponse('todo/list/update.html', + array('page_title' => 'Edit a Todo List', + 'list' => $list, + 'form' => $form)); + } + + /** + * Display the deleteList page of the application. + * + * @param Pluf_HTTP_Request Request object + * @param array Matches against the regex of the dispatcher + * @return Pluf_HTTP_Response or can throw Exception + */ + public function deleteList($request, $match) + { + // Here we show how a list can be delete with the associated + // Todo_Item. + $list = Pluf_Shortcuts_GetObjectOr404('Todo_List', $match[1]); + if ($request->method == 'POST') { + // We need first to delete all the Todo_Item in the list. + $items = $list->get_todo_item_list(); + foreach ($items as $item) { + $item->delete(); + } + // Then we can delete the list + $list->delete(); + // You can also drop directly the list and the todo items + // will be automatically dropped at the same time. + $url = Pluf_HTTP_URL_urlForView('Todo_Views::main', + array()); + return new Pluf_HTTP_Response_Redirect($url); + } + // Here we are with a GET request we show a form with a + // confirmation button to delete the list. + // To show the items to be deleted, we get them + $items = $list->get_todo_item_list(); + return Pluf_Shortcuts_RenderToResponse('todo/list/delete.html', + array('page_title' => 'Delete a Todo List', + 'list' => $list, + 'items' => $items)); + } + + /** + * Display the addList page of the application. + * + * @param Pluf_HTTP_Request Request object + * @param array Matches against the regex of the dispatcher + * @return Pluf_HTTP_Response or can throw Exception + */ + public function addList($request, $match) + { + // The workflow of the addition of an item is simple + // If the request of GET method a form is displayed + // If it is a POST method, the form is submitted and the + // content is proceeded to create the new list. + // We create a Todo_List item as we are creating one here + $list = new Todo_List(); + if ($request->method == 'POST') { + // We create the form bounded to the data submitted. + $form = new Todo_Form_List($request->POST); + if ($form->isValid()) { + // If no errors, we can save the Todo_List from the data + + $list->setFromFormData($form->cleaned_data); + $list->create(); + // We redirect the user to the page of the Todo_List + $url = Pluf_HTTP_URL_urlForView('Todo_Views::viewList', + array($list->id)); + return new Pluf_HTTP_Response_Redirect($url); + } + } else { + $form = new Todo_Form_List(); + } + return Pluf_Shortcuts_RenderToResponse('todo/list/add.html', + array('page_title' => 'Add a Todo List', + 'form' => $form)); + } + +} diff --git a/apps/todos/src/Todo/conf/todo.php b/apps/todos/src/Todo/conf/todo.php new file mode 100644 index 0000000..0e5f430 --- /dev/null +++ b/apps/todos/src/Todo/conf/todo.php @@ -0,0 +1,79 @@ + 'Pluf_Template_Tag_Url', + ); + +// Default database configuration. The database defined here will be +// directly accessible from Pluf::db() of course it is still possible +// to open any other number of database connections through Pluf_DB +$cfg['db_login'] = ''; +$cfg['db_password'] = ''; +$cfg['db_server'] = ''; +$cfg['db_database'] = $cfg['tmp_folder'].'/todo.db'; + +// Must be shared by all the installed_apps and the core framework. +// That way you can have several installations of the core framework. +$cfg['db_table_prefix'] = 'pluf_testapp_'; + +// Starting version 4.1 of MySQL the utf-8 support is "correct". +// The reason of the db_version for MySQL is only for that. +$cfg['db_version'] = ''; +$cfg['db_engine'] = 'SQLite'; +return $cfg; diff --git a/apps/todos/src/Todo/conf/todo.php-dist b/apps/todos/src/Todo/conf/todo.php-dist new file mode 100644 index 0000000..2cd7b84 --- /dev/null +++ b/apps/todos/src/Todo/conf/todo.php-dist @@ -0,0 +1,84 @@ + 'Pluf_Template_Tag_Url', + ); + +// Default database configuration. The database defined here will be +// directly accessible from Pluf::db() of course it is still possible +// to open any other number of database connections through Pluf_DB +$cfg['db_login'] = ''; +$cfg['db_password'] = ''; +$cfg['db_server'] = ''; +$cfg['db_database'] = $cfg['tmp_folder'].'/todo.db'; + +// Must be shared by all the installed_apps and the core framework. +// That way you can have several installations of the core framework. +$cfg['db_table_prefix'] = 'pluf_testapp_'; + +// Starting version 4.1 of MySQL the utf-8 support is "correct". +// The reason of the db_version for MySQL is only for that. +$cfg['db_version'] = ''; +$cfg['db_engine'] = 'SQLite'; +return $cfg; diff --git a/apps/todos/src/Todo/conf/todo.test.php b/apps/todos/src/Todo/conf/todo.test.php new file mode 100644 index 0000000..267525c --- /dev/null +++ b/apps/todos/src/Todo/conf/todo.test.php @@ -0,0 +1,93 @@ + 'Pluf_Template_Tag_Url', + ); + +// Default database configuration. The database defined here will be +// directly accessible from Pluf::db() of course it is still possible +// to open any other number of database connections through Pluf_DB +$cfg['db_login'] = ''; +$cfg['db_password'] = ''; +$cfg['db_server'] = ''; +// For testing purpose, the SQLite memory database is the best thing +// to use. +$cfg['db_database'] = ':memory:'; + +// Must be shared by all the installed_apps and the core framework. +// That way you can have several installations of the core framework. +$cfg['db_table_prefix'] = 'test_'; +$cfg['db_engine'] = 'SQLite'; +return $cfg; diff --git a/apps/todos/src/Todo/conf/urls.php b/apps/todos/src/Todo/conf/urls.php new file mode 100644 index 0000000..c1904ee --- /dev/null +++ b/apps/todos/src/Todo/conf/urls.php @@ -0,0 +1,98 @@ + '#^/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'main'); + +$ctl[] = array('regex' => '#^/install/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'install'); + +$ctl[] = array('regex' => '#^/uninstall/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'uninstall'); + +$ctl[] = array('regex' => '#^/item/(\d+)/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'viewItem'); + +$ctl[] = array('regex' => '#^/list/(\d+)/item/add/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'addItem'); + +$ctl[] = array('regex' => '#^/item/(\d+)/update/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'updateItem'); + +$ctl[] = array('regex' => '#^/item/(\d+)/delete/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'deleteItem'); + +$ctl[] = array('regex' => '#^/list/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'listLists'); + +$ctl[] = array('regex' => '#^/list/(\d+)/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'viewList'); + +$ctl[] = array('regex' => '#^/list/(\d+)/update/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'updateList'); + +$ctl[] = array('regex' => '#^/list/(\d+)/delete/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'deleteList'); + +$ctl[] = array('regex' => '#^/list/add/$#', + 'priority' => 4, + 'base' => Pluf::f('todo_base'), + 'model' => 'Todo_Views', + 'method' => 'addList'); + +return $ctl; diff --git a/apps/todos/src/Todo/relations.php b/apps/todos/src/Todo/relations.php new file mode 100644 index 0000000..f07a454 --- /dev/null +++ b/apps/todos/src/Todo/relations.php @@ -0,0 +1,33 @@ + array('Todo_List')); +return $m; +?> \ No newline at end of file diff --git a/apps/todos/src/Todo/templates/todo/base.html b/apps/todos/src/Todo/templates/todo/base.html new file mode 100644 index 0000000..b5383d4 --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/base.html @@ -0,0 +1,18 @@ + + + + + + Todo Test Application : {$page_title} + + + +

{$page_title}

+ + +{block body}{/block} + +

Home

+ + + diff --git a/apps/todos/src/Todo/templates/todo/index.html b/apps/todos/src/Todo/templates/todo/index.html new file mode 100644 index 0000000..6e59dae --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/index.html @@ -0,0 +1,16 @@ +{extends 'todo/base.html'} + +{block body} + +{if $lists} +
    +{foreach $lists as $list} +
  1. {$list.name}
  2. +{/foreach} +
+{/if} + +

Create a new list.

+ + +{/block} diff --git a/apps/todos/src/Todo/templates/todo/item/add.html b/apps/todos/src/Todo/templates/todo/item/add.html new file mode 100644 index 0000000..c4c7c26 --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/item/add.html @@ -0,0 +1,10 @@ +{extends 'todo/base.html'} + +{block body} +
+{$form.render_p} +

+ Cancel +

+
+{/block} diff --git a/apps/todos/src/Todo/templates/todo/item/delete.html b/apps/todos/src/Todo/templates/todo/item/delete.html new file mode 100644 index 0000000..87fac64 --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/item/delete.html @@ -0,0 +1,8 @@ +{extends 'todo/base.html'} + +{block body} +
+

Are you sure you want to delete the todo item {$item.item}?

+ +
+{/block} diff --git a/apps/todos/src/Todo/templates/todo/item/update.html b/apps/todos/src/Todo/templates/todo/item/update.html new file mode 100644 index 0000000..c38744f --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/item/update.html @@ -0,0 +1,12 @@ +{extends 'todo/base.html'} + +{block body} +
+{$form.render_p} +

+ +Cancel
+Delete this item +

+
+{/block} diff --git a/apps/todos/src/Todo/templates/todo/item/view.html b/apps/todos/src/Todo/templates/todo/item/view.html new file mode 100644 index 0000000..680604d --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/item/view.html @@ -0,0 +1,13 @@ +{extends 'todo/base.html'} + +{block body} +

{$item.item}

+ +

Status: {if $item.completed}Done{else}Todo{/if}.

+

List: {$list.name}.

+ +

Update this item +| Create a new item +

+ +{/block} diff --git a/apps/todos/src/Todo/templates/todo/list/add.html b/apps/todos/src/Todo/templates/todo/list/add.html new file mode 100644 index 0000000..e979977 --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/list/add.html @@ -0,0 +1,10 @@ +{extends 'todo/base.html'} + +{block body} +
+{$form.render_p|unsafe} +

+ Cancel +

+
+{/block} diff --git a/apps/todos/src/Todo/templates/todo/list/delete.html b/apps/todos/src/Todo/templates/todo/list/delete.html new file mode 100644 index 0000000..b88a3b7 --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/list/delete.html @@ -0,0 +1,14 @@ +{extends 'todo/base.html'} + +{block body} +
+

Are you sure you want to delete the todo list {$list.name}, with +the following associated todo items?

+
    +{foreach $items as $item} +
  1. {$item.item}
  2. +{/foreach} +
+ +
+{/block} diff --git a/apps/todos/src/Todo/templates/todo/list/update.html b/apps/todos/src/Todo/templates/todo/list/update.html new file mode 100644 index 0000000..d99634b --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/list/update.html @@ -0,0 +1,13 @@ +{extends 'todo/base.html'} + +{block body} +
+{$form.render_p} +

+ +Cancel +
+Delete this list +

+
+{/block} diff --git a/apps/todos/src/Todo/templates/todo/list/view.html b/apps/todos/src/Todo/templates/todo/list/view.html new file mode 100644 index 0000000..48105d6 --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/list/view.html @@ -0,0 +1,17 @@ +{extends 'todo/base.html'} + +{block body} +

{$list.name}

+ +{if $items} +
    +{foreach $items as $item} +
  1. {$item.item}
  2. +{/foreach} +
+{/if} + +

Create a new item | Update the list

+ + +{/block} diff --git a/apps/todos/src/Todo/templates/todo/simple.html b/apps/todos/src/Todo/templates/todo/simple.html new file mode 100644 index 0000000..1fdfc76 --- /dev/null +++ b/apps/todos/src/Todo/templates/todo/simple.html @@ -0,0 +1,5 @@ +{extends 'base.html'} + +{block body} +

{$message}

+{/block} diff --git a/apps/todos/www/index.php b/apps/todos/www/index.php new file mode 100644 index 0000000..d363d15 --- /dev/null +++ b/apps/todos/www/index.php @@ -0,0 +1,42 @@ + 0, "AP" => 1, "EU" => 2, "AD" => 3, "AE" => 4, "AF" => 5, +"AG" => 6, "AI" => 7, "AL" => 8, "AM" => 9, "AN" => 10, "AO" => 11, +"AQ" => 12, "AR" => 13, "AS" => 14, "AT" => 15, "AU" => 16, "AW" => 17, +"AZ" => 18, "BA" => 19, "BB" => 20, "BD" => 21, "BE" => 22, "BF" => 23, +"BG" => 24, "BH" => 25, "BI" => 26, "BJ" => 27, "BM" => 28, "BN" => 29, +"BO" => 30, "BR" => 31, "BS" => 32, "BT" => 33, "BV" => 34, "BW" => 35, +"BY" => 36, "BZ" => 37, "CA" => 38, "CC" => 39, "CD" => 40, "CF" => 41, +"CG" => 42, "CH" => 43, "CI" => 44, "CK" => 45, "CL" => 46, "CM" => 47, +"CN" => 48, "CO" => 49, "CR" => 50, "CU" => 51, "CV" => 52, "CX" => 53, +"CY" => 54, "CZ" => 55, "DE" => 56, "DJ" => 57, "DK" => 58, "DM" => 59, +"DO" => 60, "DZ" => 61, "EC" => 62, "EE" => 63, "EG" => 64, "EH" => 65, +"ER" => 66, "ES" => 67, "ET" => 68, "FI" => 69, "FJ" => 70, "FK" => 71, +"FM" => 72, "FO" => 73, "FR" => 74, "FX" => 75, "GA" => 76, "GB" => 77, +"GD" => 78, "GE" => 79, "GF" => 80, "GH" => 81, "GI" => 82, "GL" => 83, +"GM" => 84, "GN" => 85, "GP" => 86, "GQ" => 87, "GR" => 88, "GS" => 89, +"GT" => 90, "GU" => 91, "GW" => 92, "GY" => 93, "HK" => 94, "HM" => 95, +"HN" => 96, "HR" => 97, "HT" => 98, "HU" => 99, "ID" => 100, "IE" => 101, +"IL" => 102, "IN" => 103, "IO" => 104, "IQ" => 105, "IR" => 106, "IS" => 107, +"IT" => 108, "JM" => 109, "JO" => 110, "JP" => 111, "KE" => 112, "KG" => 113, +"KH" => 114, "KI" => 115, "KM" => 116, "KN" => 117, "KP" => 118, "KR" => 119, +"KW" => 120, "KY" => 121, "KZ" => 122, "LA" => 123, "LB" => 124, "LC" => 125, +"LI" => 126, "LK" => 127, "LR" => 128, "LS" => 129, "LT" => 130, "LU" => 131, +"LV" => 132, "LY" => 133, "MA" => 134, "MC" => 135, "MD" => 136, "MG" => 137, +"MH" => 138, "MK" => 139, "ML" => 140, "MM" => 141, "MN" => 142, "MO" => 143, +"MP" => 144, "MQ" => 145, "MR" => 146, "MS" => 147, "MT" => 148, "MU" => 149, +"MV" => 150, "MW" => 151, "MX" => 152, "MY" => 153, "MZ" => 154, "NA" => 155, +"NC" => 156, "NE" => 157, "NF" => 158, "NG" => 159, "NI" => 160, "NL" => 161, +"NO" => 162, "NP" => 163, "NR" => 164, "NU" => 165, "NZ" => 166, "OM" => 167, +"PA" => 168, "PE" => 169, "PF" => 170, "PG" => 171, "PH" => 172, "PK" => 173, +"PL" => 174, "PM" => 175, "PN" => 176, "PR" => 177, "PS" => 178, "PT" => 179, +"PW" => 180, "PY" => 181, "QA" => 182, "RE" => 183, "RO" => 184, "RU" => 185, +"RW" => 186, "SA" => 187, "SB" => 188, "SC" => 189, "SD" => 190, "SE" => 191, +"SG" => 192, "SH" => 193, "SI" => 194, "SJ" => 195, "SK" => 196, "SL" => 197, +"SM" => 198, "SN" => 199, "SO" => 200, "SR" => 201, "ST" => 202, "SV" => 203, +"SY" => 204, "SZ" => 205, "TC" => 206, "TD" => 207, "TF" => 208, "TG" => 209, +"TH" => 210, "TJ" => 211, "TK" => 212, "TM" => 213, "TN" => 214, "TO" => 215, +"TL" => 216, "TR" => 217, "TT" => 218, "TV" => 219, "TW" => 220, "TZ" => 221, +"UA" => 222, "UG" => 223, "UM" => 224, "US" => 225, "UY" => 226, "UZ" => 227, +"VA" => 228, "VC" => 229, "VE" => 230, "VG" => 231, "VI" => 232, "VN" => 233, +"VU" => 234, "WF" => 235, "WS" => 236, "YE" => 237, "YT" => 238, "RS" => 239, +"ZA" => 240, "ZM" => 241, "ME" => 242, "ZW" => 243, "A1" => 244, "A2" => 245, +"O1" => 246, "AX" => 247, "GG" => 248, "IM" => 249, "JE" => 250 +); + var $GEOIP_COUNTRY_CODES = array( +"", "AP", "EU", "AD", "AE", "AF", "AG", "AI", "AL", "AM", "AN", "AO", "AQ", +"AR", "AS", "AT", "AU", "AW", "AZ", "BA", "BB", "BD", "BE", "BF", "BG", "BH", +"BI", "BJ", "BM", "BN", "BO", "BR", "BS", "BT", "BV", "BW", "BY", "BZ", "CA", +"CC", "CD", "CF", "CG", "CH", "CI", "CK", "CL", "CM", "CN", "CO", "CR", "CU", +"CV", "CX", "CY", "CZ", "DE", "DJ", "DK", "DM", "DO", "DZ", "EC", "EE", "EG", +"EH", "ER", "ES", "ET", "FI", "FJ", "FK", "FM", "FO", "FR", "FX", "GA", "GB", +"GD", "GE", "GF", "GH", "GI", "GL", "GM", "GN", "GP", "GQ", "GR", "GS", "GT", +"GU", "GW", "GY", "HK", "HM", "HN", "HR", "HT", "HU", "ID", "IE", "IL", "IN", +"IO", "IQ", "IR", "IS", "IT", "JM", "JO", "JP", "KE", "KG", "KH", "KI", "KM", +"KN", "KP", "KR", "KW", "KY", "KZ", "LA", "LB", "LC", "LI", "LK", "LR", "LS", +"LT", "LU", "LV", "LY", "MA", "MC", "MD", "MG", "MH", "MK", "ML", "MM", "MN", +"MO", "MP", "MQ", "MR", "MS", "MT", "MU", "MV", "MW", "MX", "MY", "MZ", "NA", +"NC", "NE", "NF", "NG", "NI", "NL", "NO", "NP", "NR", "NU", "NZ", "OM", "PA", +"PE", "PF", "PG", "PH", "PK", "PL", "PM", "PN", "PR", "PS", "PT", "PW", "PY", +"QA", "RE", "RO", "RU", "RW", "SA", "SB", "SC", "SD", "SE", "SG", "SH", "SI", +"SJ", "SK", "SL", "SM", "SN", "SO", "SR", "ST", "SV", "SY", "SZ", "TC", "TD", +"TF", "TG", "TH", "TJ", "TK", "TM", "TN", "TO", "TL", "TR", "TT", "TV", "TW", +"TZ", "UA", "UG", "UM", "US", "UY", "UZ", "VA", "VC", "VE", "VG", "VI", "VN", +"VU", "WF", "WS", "YE", "YT", "RS", "ZA", "ZM", "ME", "ZW", "A1", "A2", "O1", +"AX", "GG", "IM", "JE" +); + var $GEOIP_COUNTRY_CODES3 = array( +"","AP","EU","AND","ARE","AFG","ATG","AIA","ALB","ARM","ANT","AGO","AQ","ARG", +"ASM","AUT","AUS","ABW","AZE","BIH","BRB","BGD","BEL","BFA","BGR","BHR","BDI", +"BEN","BMU","BRN","BOL","BRA","BHS","BTN","BV","BWA","BLR","BLZ","CAN","CC", +"COD","CAF","COG","CHE","CIV","COK","CHL","CMR","CHN","COL","CRI","CUB","CPV", +"CX","CYP","CZE","DEU","DJI","DNK","DMA","DOM","DZA","ECU","EST","EGY","ESH", +"ERI","ESP","ETH","FIN","FJI","FLK","FSM","FRO","FRA","FX","GAB","GBR","GRD", +"GEO","GUF","GHA","GIB","GRL","GMB","GIN","GLP","GNQ","GRC","GS","GTM","GUM", +"GNB","GUY","HKG","HM","HND","HRV","HTI","HUN","IDN","IRL","ISR","IND","IO", +"IRQ","IRN","ISL","ITA","JAM","JOR","JPN","KEN","KGZ","KHM","KIR","COM","KNA", +"PRK","KOR","KWT","CYM","KAZ","LAO","LBN","LCA","LIE","LKA","LBR","LSO","LTU", +"LUX","LVA","LBY","MAR","MCO","MDA","MDG","MHL","MKD","MLI","MMR","MNG","MAC", +"MNP","MTQ","MRT","MSR","MLT","MUS","MDV","MWI","MEX","MYS","MOZ","NAM","NCL", +"NER","NFK","NGA","NIC","NLD","NOR","NPL","NRU","NIU","NZL","OMN","PAN","PER", +"PYF","PNG","PHL","PAK","POL","SPM","PCN","PRI","PSE","PRT","PLW","PRY","QAT", +"REU","ROU","RUS","RWA","SAU","SLB","SYC","SDN","SWE","SGP","SHN","SVN","SJM", +"SVK","SLE","SMR","SEN","SOM","SUR","STP","SLV","SYR","SWZ","TCA","TCD","TF", +"TGO","THA","TJK","TKL","TLS","TKM","TUN","TON","TUR","TTO","TUV","TWN","TZA", +"UKR","UGA","UM","USA","URY","UZB","VAT","VCT","VEN","VGB","VIR","VNM","VUT", +"WLF","WSM","YEM","YT","SRB","ZAF","ZMB","MNE","ZWE","A1","A2","O1", +"ALA","GGY","IMN","JEY" + ); + var $GEOIP_COUNTRY_NAMES = array( +"", "Asia/Pacific Region", "Europe", "Andorra", "United Arab Emirates", +"Afghanistan", "Antigua and Barbuda", "Anguilla", "Albania", "Armenia", +"Netherlands Antilles", "Angola", "Antarctica", "Argentina", "American Samoa", +"Austria", "Australia", "Aruba", "Azerbaijan", "Bosnia and Herzegovina", +"Barbados", "Bangladesh", "Belgium", "Burkina Faso", "Bulgaria", "Bahrain", +"Burundi", "Benin", "Bermuda", "Brunei Darussalam", "Bolivia", "Brazil", +"Bahamas", "Bhutan", "Bouvet Island", "Botswana", "Belarus", "Belize", +"Canada", "Cocos (Keeling) Islands", "Congo, The Democratic Republic of the", +"Central African Republic", "Congo", "Switzerland", "Cote D'Ivoire", "Cook +Islands", "Chile", "Cameroon", "China", "Colombia", "Costa Rica", "Cuba", "Cape +Verde", "Christmas Island", "Cyprus", "Czech Republic", "Germany", "Djibouti", +"Denmark", "Dominica", "Dominican Republic", "Algeria", "Ecuador", "Estonia", +"Egypt", "Western Sahara", "Eritrea", "Spain", "Ethiopia", "Finland", "Fiji", +"Falkland Islands (Malvinas)", "Micronesia, Federated States of", "Faroe +Islands", "France", "France, Metropolitan", "Gabon", "United Kingdom", +"Grenada", "Georgia", "French Guiana", "Ghana", "Gibraltar", "Greenland", +"Gambia", "Guinea", "Guadeloupe", "Equatorial Guinea", "Greece", "South Georgia +and the South Sandwich Islands", "Guatemala", "Guam", "Guinea-Bissau", +"Guyana", "Hong Kong", "Heard Island and McDonald Islands", "Honduras", +"Croatia", "Haiti", "Hungary", "Indonesia", "Ireland", "Israel", "India", +"British Indian Ocean Territory", "Iraq", "Iran, Islamic Republic of", +"Iceland", "Italy", "Jamaica", "Jordan", "Japan", "Kenya", "Kyrgyzstan", +"Cambodia", "Kiribati", "Comoros", "Saint Kitts and Nevis", "Korea, Democratic +People's Republic of", "Korea, Republic of", "Kuwait", "Cayman Islands", +"Kazakstan", "Lao People's Democratic Republic", "Lebanon", "Saint Lucia", +"Liechtenstein", "Sri Lanka", "Liberia", "Lesotho", "Lithuania", "Luxembourg", +"Latvia", "Libyan Arab Jamahiriya", "Morocco", "Monaco", "Moldova, Republic +of", "Madagascar", "Marshall Islands", "Macedonia", +"Mali", "Myanmar", "Mongolia", "Macau", "Northern Mariana Islands", +"Martinique", "Mauritania", "Montserrat", "Malta", "Mauritius", "Maldives", +"Malawi", "Mexico", "Malaysia", "Mozambique", "Namibia", "New Caledonia", +"Niger", "Norfolk Island", "Nigeria", "Nicaragua", "Netherlands", "Norway", +"Nepal", "Nauru", "Niue", "New Zealand", "Oman", "Panama", "Peru", "French +Polynesia", "Papua New Guinea", "Philippines", "Pakistan", "Poland", "Saint +Pierre and Miquelon", "Pitcairn Islands", "Puerto Rico", "Palestinian Territory", +"Portugal", "Palau", "Paraguay", "Qatar", "Reunion", "Romania", +"Russian Federation", "Rwanda", "Saudi Arabia", "Solomon Islands", +"Seychelles", "Sudan", "Sweden", "Singapore", "Saint Helena", "Slovenia", +"Svalbard and Jan Mayen", "Slovakia", "Sierra Leone", "San Marino", "Senegal", +"Somalia", "Suriname", "Sao Tome and Principe", "El Salvador", "Syrian Arab +Republic", "Swaziland", "Turks and Caicos Islands", "Chad", "French Southern +Territories", "Togo", "Thailand", "Tajikistan", "Tokelau", "Turkmenistan", +"Tunisia", "Tonga", "Timor-Leste", "Turkey", "Trinidad and Tobago", "Tuvalu", +"Taiwan", "Tanzania, United Republic of", "Ukraine", +"Uganda", "United States Minor Outlying Islands", "United States", "Uruguay", +"Uzbekistan", "Holy See (Vatican City State)", "Saint Vincent and the +Grenadines", "Venezuela", "Virgin Islands, British", "Virgin Islands, U.S.", +"Vietnam", "Vanuatu", "Wallis and Futuna", "Samoa", "Yemen", "Mayotte", +"Serbia", "South Africa", "Zambia", "Montenegro", "Zimbabwe", +"Anonymous Proxy","Satellite Provider","Other", +"Aland Islands","Guernsey","Isle of Man","Jersey" +); +} +function GeoIP_load_shared_mem ($file) { + + $fp = fopen($file, "rb"); + if (!$fp) { + print "error opening $file: $php_errormsg\n"; + exit; + } + $s_array = fstat($fp); + $size = $s_array['size']; + if ($shmid = @shmop_open (GEOIP_SHM_KEY, "w", 0, 0)) { + shmop_delete ($shmid); + shmop_close ($shmid); + } + $shmid = shmop_open (GEOIP_SHM_KEY, "c", 0644, $size); + shmop_write ($shmid, fread($fp, $size), 0); + shmop_close ($shmid); +} + +function _setup_segments($gi){ + $gi->databaseType = GEOIP_COUNTRY_EDITION; + $gi->record_length = STANDARD_RECORD_LENGTH; + if ($gi->flags & GEOIP_SHARED_MEMORY) { + $offset = @shmop_size ($gi->shmid) - 3; + for ($i = 0; $i < STRUCTURE_INFO_MAX_SIZE; $i++) { + $delim = @shmop_read ($gi->shmid, $offset, 3); + $offset += 3; + if ($delim == (chr(255).chr(255).chr(255))) { + $gi->databaseType = ord(@shmop_read ($gi->shmid, $offset, 1)); + $offset++; + + if ($gi->databaseType == GEOIP_REGION_EDITION_REV0){ + $gi->databaseSegments = GEOIP_STATE_BEGIN_REV0; + } else if ($gi->databaseType == GEOIP_REGION_EDITION_REV1){ + $gi->databaseSegments = GEOIP_STATE_BEGIN_REV1; + } else if (($gi->databaseType == GEOIP_CITY_EDITION_REV0)|| + ($gi->databaseType == GEOIP_CITY_EDITION_REV1) + || ($gi->databaseType == GEOIP_ORG_EDITION) + || ($gi->databaseType == GEOIP_ISP_EDITION) + || ($gi->databaseType == GEOIP_ASNUM_EDITION)){ + $gi->databaseSegments = 0; + $buf = @shmop_read ($gi->shmid, $offset, SEGMENT_RECORD_LENGTH); + for ($j = 0;$j < SEGMENT_RECORD_LENGTH;$j++){ + $gi->databaseSegments += (ord($buf[$j]) << ($j * 8)); + } + if (($gi->databaseType == GEOIP_ORG_EDITION)|| + ($gi->databaseType == GEOIP_ISP_EDITION)) { + $gi->record_length = ORG_RECORD_LENGTH; + } + } + break; + } else { + $offset -= 4; + } + } + if (($gi->databaseType == GEOIP_COUNTRY_EDITION)|| + ($gi->databaseType == GEOIP_PROXY_EDITION)|| + ($gi->databaseType == GEOIP_NETSPEED_EDITION)){ + $gi->databaseSegments = GEOIP_COUNTRY_BEGIN; + } + } else { + $filepos = ftell($gi->filehandle); + fseek($gi->filehandle, -3, SEEK_END); + for ($i = 0; $i < STRUCTURE_INFO_MAX_SIZE; $i++) { + $delim = fread($gi->filehandle,3); + if ($delim == (chr(255).chr(255).chr(255))){ + $gi->databaseType = ord(fread($gi->filehandle,1)); + if ($gi->databaseType == GEOIP_REGION_EDITION_REV0){ + $gi->databaseSegments = GEOIP_STATE_BEGIN_REV0; + } + else if ($gi->databaseType == GEOIP_REGION_EDITION_REV1){ + $gi->databaseSegments = GEOIP_STATE_BEGIN_REV1; + } else if (($gi->databaseType == GEOIP_CITY_EDITION_REV0) || + ($gi->databaseType == GEOIP_CITY_EDITION_REV1) || + ($gi->databaseType == GEOIP_ORG_EDITION) || + ($gi->databaseType == GEOIP_ISP_EDITION) || + ($gi->databaseType == GEOIP_ASNUM_EDITION)){ + $gi->databaseSegments = 0; + $buf = fread($gi->filehandle,SEGMENT_RECORD_LENGTH); + for ($j = 0;$j < SEGMENT_RECORD_LENGTH;$j++){ + $gi->databaseSegments += (ord($buf[$j]) << ($j * 8)); + } + if ($gi->databaseType == GEOIP_ORG_EDITION || + $gi->databaseType == GEOIP_ISP_EDITION) { + $gi->record_length = ORG_RECORD_LENGTH; + } + } + break; + } else { + fseek($gi->filehandle, -4, SEEK_CUR); + } + } + if (($gi->databaseType == GEOIP_COUNTRY_EDITION)|| + ($gi->databaseType == GEOIP_PROXY_EDITION)|| + ($gi->databaseType == GEOIP_NETSPEED_EDITION)){ + $gi->databaseSegments = GEOIP_COUNTRY_BEGIN; + } + fseek($gi->filehandle,$filepos,SEEK_SET); + } + return $gi; +} + +function GeoIP_open($filename, $flags) { + $gi = new GeoIP; + $gi->flags = $flags; + if ($gi->flags & GEOIP_SHARED_MEMORY) { + $gi->shmid = @shmop_open (GEOIP_SHM_KEY, "a", 0, 0); + } else { + $gi->filehandle = fopen($filename,"rb"); + if ($gi->flags & GEOIP_MEMORY_CACHE) { + $s_array = fstat($gi->filehandle); + $gi->memory_buffer = fread($gi->filehandle, $s_array['size']); + } + } + + $gi = _setup_segments($gi); + return $gi; +} + +function GeoIP_close($gi) { + if ($gi->flags & GEOIP_SHARED_MEMORY) { + return true; + } + + return fclose($gi->filehandle); +} + +function GeoIP_country_id_by_name($gi, $name) { + $addr = gethostbyname($name); + if (!$addr || $addr == $name) { + return false; + } + return GeoIP_country_id_by_addr($gi, $addr); +} + +function GeoIP_country_code_by_name($gi, $name) { + $country_id = GeoIP_country_id_by_name($gi,$name); + if ($country_id !== false) { + return $gi->GEOIP_COUNTRY_CODES[$country_id]; + } + return false; +} + +function GeoIP_country_name_by_name($gi, $name) { + $country_id = GeoIP_country_id_by_name($gi,$name); + if ($country_id !== false) { + return $gi->GEOIP_COUNTRY_NAMES[$country_id]; + } + return false; +} + +function GeoIP_country_id_by_addr($gi, $addr) { + $ipnum = ip2long($addr); + return _GeoIP_seek_country($gi, $ipnum) - GEOIP_COUNTRY_BEGIN; +} + +function GeoIP_country_code_by_addr($gi, $addr) { + if ($gi->databaseType == GEOIP_CITY_EDITION_REV1) { + $record = GeoIP_record_by_addr($gi,$addr); + return $record->country_code; + } else { + $country_id = GeoIP_country_id_by_addr($gi,$addr); + if ($country_id !== false) { + return $gi->GEOIP_COUNTRY_CODES[$country_id]; + } + } + return false; +} + +function GeoIP_country_name_by_addr($gi, $addr) { + if ($gi->databaseType == GEOIP_CITY_EDITION_REV1) { + $record = GeoIP_record_by_addr($gi,$addr); + return $record->country_name; + } else { + $country_id = GeoIP_country_id_by_addr($gi,$addr); + if ($country_id !== false) { + return $gi->GEOIP_COUNTRY_NAMES[$country_id]; + } + } + return false; +} + +function _GeoIP_seek_country($gi, $ipnum) { + $offset = 0; + for ($depth = 31; $depth >= 0; --$depth) { + if ($gi->flags & GEOIP_MEMORY_CACHE) { + $buf = substr($gi->memory_buffer, + 2 * $gi->record_length * $offset, + 2 * $gi->record_length); + } elseif ($gi->flags & GEOIP_SHARED_MEMORY) { + $buf = @shmop_read ($gi->shmid, + 2 * $gi->record_length * $offset, + 2 * $gi->record_length ); + } else { + fseek($gi->filehandle, 2 * $gi->record_length * $offset, SEEK_SET) == 0 + or die("fseek failed"); + $buf = fread($gi->filehandle, 2 * $gi->record_length); + } + $x = array(0,0); + for ($i = 0; $i < 2; ++$i) { + for ($j = 0; $j < $gi->record_length; ++$j) { + $x[$i] += ord($buf[$gi->record_length * $i + $j]) << ($j * 8); + } + } + if ($ipnum & (1 << $depth)) { + if ($x[1] >= $gi->databaseSegments) { + return $x[1]; + } + $offset = $x[1]; + } else { + if ($x[0] >= $gi->databaseSegments) { + return $x[0]; + } + $offset = $x[0]; + } + } + trigger_error("error traversing database - perhaps it is corrupt?", E_USER_ERROR); + return false; +} + +function _get_org($gi,$ipnum){ + $seek_org = _GeoIP_seek_country($gi,$ipnum); + if ($seek_org == $gi->databaseSegments) { + return NULL; + } + $record_pointer = $seek_org + (2 * $gi->record_length - 1) * $gi->databaseSegments; + if ($gi->flags & GEOIP_SHARED_MEMORY) { + $org_buf = @shmop_read ($gi->shmid, $record_pointer, MAX_ORG_RECORD_LENGTH); + } else { + fseek($gi->filehandle, $record_pointer, SEEK_SET); + $org_buf = fread($gi->filehandle,MAX_ORG_RECORD_LENGTH); + } + $org_buf = substr($org_buf, 0, strpos($org_buf, 0)); + return $org_buf; +} + +function GeoIP_org_by_addr ($gi,$addr) { + if ($addr == NULL) { + return 0; + } + $ipnum = ip2long($addr); + return _get_org($gi, $ipnum); +} + +function _get_region($gi,$ipnum){ + if ($gi->databaseType == GEOIP_REGION_EDITION_REV0){ + $seek_region = _GeoIP_seek_country($gi,$ipnum) - GEOIP_STATE_BEGIN_REV0; + if ($seek_region >= 1000){ + $country_code = "US"; + $region = chr(($seek_region - 1000)/26 + 65) . chr(($seek_region - 1000)%26 + 65); + } else { + $country_code = $gi->GEOIP_COUNTRY_CODES[$seek_region]; + $region = ""; + } + return array ($country_code,$region); + } else if ($gi->databaseType == GEOIP_REGION_EDITION_REV1) { + $seek_region = _GeoIP_seek_country($gi,$ipnum) - GEOIP_STATE_BEGIN_REV1; + //print $seek_region; + if ($seek_region < US_OFFSET){ + $country_code = ""; + $region = ""; + } else if ($seek_region < CANADA_OFFSET) { + $country_code = "US"; + $region = chr(($seek_region - US_OFFSET)/26 + 65) . chr(($seek_region - US_OFFSET)%26 + 65); + } else if ($seek_region < WORLD_OFFSET) { + $country_code = "CA"; + $region = chr(($seek_region - CANADA_OFFSET)/26 + 65) . chr(($seek_region - CANADA_OFFSET)%26 + 65); + } else { + $country_code = $gi->GEOIP_COUNTRY_CODES[($seek_region - WORLD_OFFSET) / FIPS_RANGE]; + $region = ""; + } + return array ($country_code,$region); + } +} + +function GeoIP_region_by_addr ($gi,$addr) { + if ($addr == NULL) { + return 0; + } + $ipnum = ip2long($addr); + return _get_region($gi, $ipnum); +} + +function GeoIP_getdnsattributes ($l,$ip){ + $r = new Net_DNS_Resolver(); + $r->nameservers = array("ws1.maxmind.com"); + $p = $r->search($l."." . $ip .".s.maxmind.com","TXT","IN"); + $str = is_object($p->answer[0])?$p->answer[0]->string():''; + ereg("\"(.*)\"",$str,$regs); + $str = $regs[1]; + return $str; +} + diff --git a/src/Pluf.php b/src/Pluf.php new file mode 100644 index 0000000..b625262 --- /dev/null +++ b/src/Pluf.php @@ -0,0 +1,359 @@ +$val) { + if (0 === strpos($key, $pfx)) { + if (!$strip) { + $ret[$key] = $val; + } else { + $ret[substr($key, $pfx_len)] = $val; + } + } + } + return $ret; + } + + + /** + * Returns a given object. + * + * Loads automatically the corresponding class file if needed. + * If impossible to get the class $model, exception is thrown. + * + * @param string Model to load. + * @param mixed Extra parameters for the constructor of the model. + */ + public static function factory($model, $params=null) + { + if ($params !== null) { + return new $model($params); + } + return new $model(); + } + + /** + * Load a class depending on its name. + * + * Throw an exception if not possible to load the class. + * + * @param string Class to load. + */ + public static function loadClass($class) + { + if (class_exists($class, false)) { + return; + } + $file = str_replace('_', DIRECTORY_SEPARATOR, $class) . '.php'; + if (false !== ($file=Pluf::fileExists($file))) { + include $file; + } + if (!class_exists($class, false)) { + throw new Exception('Impossible to load the class: '.$class); + } + } + + /** + * Load a function depending on its name. + * + * The implementation file of the function + * MyApp_Youpla_Boum_Stuff() is MyApp/Youpla/Boum.php That way it + * is possible to group all the related function in one file. + * + * Throw an exception if not possible to load the function. + * + * @param string Function to load. + */ + public static function loadFunction($function) + { + if (function_exists($function)) { + return; + } + $elts = explode('_', $function); + array_pop($elts); + $file = implode(DIRECTORY_SEPARATOR, $elts) . '.php'; + if (false !== ($file=Pluf::fileExists($file))) { + include $file; + } + if (!function_exists($function)) { + throw new Exception('Impossible to load the function: '.$function); + } + } + + + /** + * Hack for [[php file_exists()]] that checks the include_path. + * + * Use this to see if a file exists anywhere in the include_path. + * + * + * $file = 'path/to/file.php'; + * if (Pluf::fileExists('path/to/file.php')) { + * include $file; + * } + * + * + * @credits Paul M. Jones + * + * @param string $file Check for this file in the include_path. + * @return mixed Full path to the file if the file exists and + * is readable in the include_path, false if not. + */ + public static function fileExists($file) + { + $file = trim($file); + if (!$file) { + return false; + } + // using an absolute path for the file? + // dual check for Unix '/' and Windows '\', + // or Windows drive letter and a ':'. + $abs = ($file[0] == '/' || $file[0] == '\\' || $file[1] == ':'); + if ($abs && file_exists($file)) { + return $file; + } + // using a relative path on the file + $path = explode(PATH_SEPARATOR, ini_get('include_path')); + foreach ($path as $dir) { + // strip Unix '/' and Windows '\' + $target = rtrim($dir, '\\/').DIRECTORY_SEPARATOR.$file; + if (file_exists($target)) { + return $target; + } + } + // never found it + return false; + } + + /** + * Helper to load the default database connection. + * + * This method is just dispatching to the function define in the + * configuration by the 'db_get_connection' key or use the default + * 'Pluf_DB_getConnection'. If you want to use your own function, + * take a look at the Pluf_DB_getConnection function to use the + * same approach for your method. + * + * The extra parameters can be used to selectively connect to a + * given database. When the ORM is getting a connection, it is + * passing the current model as parameter. That way you could get + * different databases for different models. + * + * @param mixed Extra parameters. + * @return resource DB connection. + */ + public static function &db($extra=null) + { + $func = Pluf::f('db_get_connection', 'Pluf_DB_getConnection'); + Pluf::loadFunction($func); + $a = $func($extra); + return $a; + } +} + + +/** + * Translate a string. + * + * @param string String to be translated. + * @return string Translated string. + */ +function __($str) +{ + $locale = (isset($GLOBALS['_PX_current_locale'])) ? $GLOBALS['_PX_current_locale'] : 'en'; + if (!empty($GLOBALS['_PX_locale'][$locale][$str][0])) { + return $GLOBALS['_PX_locale'][$locale][$str][0]; + } + return $str; +} + +/** + * Translate the plural form of a string. + * + * @param string Singular form of the string. + * @param string Plural form of the string. + * @param int Number of elements. + * @return string Translated string. + */ +function _n($sing, $plur, $n) +{ + $locale = (isset($GLOBALS['_PX_current_locale'])) ? $GLOBALS['_PX_current_locale'] : 'en'; + if (isset($GLOBALS['_PX_current_locale_plural_form'])) { + $pform = $GLOBALS['_PX_current_locale_plural_form']; + } else { + $pform = Pluf_Translation::getPluralForm($locale); + } + $index = Pluf_Translation::$pform($n); + if (!empty($GLOBALS['_PX_locale'][$locale][$sing.'#'.$plur][$index])) { + return $GLOBALS['_PX_locale'][$locale][$sing.'#'.$plur][$index]; + } + // We have no translations or default English + if ($n == 1) { + return $sing; + } + return $plur; +} + +/** + * Autoload function. + * + * @param string Class name. + */ +function __autoload($class_name) +{ + try { + Pluf::loadClass($class_name); + } catch (Exception $e) { + return eval ('class '.$class_name.' {' . + ' function '.$class_name.'() {' . + ' throw new Exception("Class not found: '.$class_name.'");' . + ' }' . + '}'); + } +} + +/** + * Exception to catch the PHP errors. + * + * @credits errd + * @see http://www.php.net/manual/en/function.set-error-handler.php + */ +class PlufErrorHandlerException extends Exception +{ + public function setLine($line) + { + $this->line = $line; + } + + public function setFile($file) + { + $this->file = $file; + } +} + +/** + * The function that is the real error handler. + */ +function PlufErrorHandler($code, $string, $file, $line) +{ + if (E_STRICT == $code && + 0 === strpos($file, Pluf::f('pear_path','/usr/share/php/'))) { + return; + } + $exception = new PlufErrorHandlerException($string, $code); + $exception->setLine($line); + $exception->setFile($file); + throw $exception; +} + +// Set the error handler only if not performing the unittests. +if (!defined('IN_UNIT_TESTS')) { + set_error_handler('PlufErrorHandler', E_ALL | E_STRICT); +} + + +/** + * Shortcut to avoid typing again and again this htmlspecialchars call. + * + * @param string Raw string. + * @return string HTML escaped string. + */ +function Pluf_esc($string) +{ + return htmlspecialchars((string)$string, ENT_COMPAT, 'UTF-8'); +} diff --git a/src/Pluf/.htaccess b/src/Pluf/.htaccess new file mode 100644 index 0000000..3a42882 --- /dev/null +++ b/src/Pluf/.htaccess @@ -0,0 +1 @@ +Deny from all diff --git a/src/Pluf/Calendar.php b/src/Pluf/Calendar.php new file mode 100644 index 0000000..a0a83bf --- /dev/null +++ b/src/Pluf/Calendar.php @@ -0,0 +1,485 @@ + + * array(array('time' => '10:15', + * 'start' => 4 , + * 'continued' => 5), + * array('time' => '11:30', + * 'start' => 3 , + * 'continued' => 0), + * ) + * '2007-03-24' => + * array(array('time' => '11:30', + * 'start' => 2 , + * 'continued' => 3), + * ) + * ) + * + */ + var $_simultaneous = array(); + var $_max_simultaneous = array(); + var $_groups = array(); + + /** + * Render the calendar based on the options. + */ + public function render() + { + if (count($this->events) == 0) { + return ''; + } + $this->cleanEventList(); + $this->getTimeIntervals(); + $this->getSimultaneous(); + $this->getMaxSimultaneous(); + $s = ''; + if ($this->summary) { + $s = 'summary="'.htmlspecialchars($this->summary).'" '; + } + $out = ''."\n"; + $out .= $this->getHead(); + $out .= $this->getBody(); + $out .= '
'."\n"; + return Pluf_Template_SafeString::markSafe($out); + } + + /** + * Event are grouped by day per default, you can group as you + * want, just subclass this method. Groups are used to make + * columns in the table with the headings. + */ + function getEventGroup($event) + { + return substr($event['start'], 0, 10); + } + + /** + * Get all the available groups. + */ + function getGroups() + { + if (count($this->_groups)) { + return $this->_groups; + } + foreach ($this->_events as $event) { + $group = $this->getEventGroup($event); + if (!in_array($group, $this->_groups)) { + $this->_groups[] = $group; + } + } + return $this->_groups; + } + + /** + * Get the name of a group to print in the headers. + */ + function getGroupName($group) + { + $dw = $this->daysOfWeek(); + $days = date('w', strtotime($group)); + return htmlspecialchars($dw[$days%7]); + } + + /** + * Generate the body of the calendar. + */ + function getBody() + { + $out = ''."\n"; + $inters = $this->getTimeIntervals(); + $groups = $this->getGroups(); + for ($i=0;$i<(count($inters)-1);$i++) { + $out .= ''."\n"; + $out .= ' '.$inters[$i].' - '.$inters[$i+1].''."\n"; + foreach ($groups as $group) { + $out .= $this->getEventCell($group, $inters[$i]); + } + $out .= ''."\n"; + } + $out .= ''."\n"; + return $out; + } + + + /** + * Get the value to print for the given cell + * + * @param string Current group + * @param string Current interval + * @return string Table cells + */ + function getEventCell($group, $inter) + { + $out = ''; + $max = $this->getMaxSimultaneous(); + $fullspanevent = false; + foreach ($this->_events as $event) { + // Get the start time of the event + $e_start = substr($event['start'], 11, 5); + if ($e_start != $inter) { + // If the event does not start at the current time, + // skip it + continue; + } + if ($group != $this->getEventGroup($event)) { + // Check if full span even at this time interval + if (!empty($event['fullspan'])) { + $fullspanevent = true; + } + continue; + } + // Find how many rows the event will span + $extra = ''; + $content = ''; + if (!isset($event['content'])) $event['content'] = ''; + $row_span = $this->getEventRowSpanning($event, $this->_time_intervals); + if ($row_span > 1) { + $extra .= ' rowspan="'.$row_span.'"'; + } + if (!empty($event['fullspan'])) { + $colspan = 0; + foreach ($max as $_s) { + $colspan += $_s; + } + $extra .= ' colspan="'.$colspan.'"'; + $fullspanevent = true; + } + if (strlen($event['color']) > 0) { + $extra .= ' style="background-color: '.$event['color'].';"'; + } + if (strlen($event['content']) > 0) { + $content .= $event['content']; + } + if (strlen($event['url']) > 0) { + $content .= ''.htmlspecialchars($event['title']).''; + } + if (strlen($event['content']) == 0 and strlen($event['url']) == 0) { + $content .= htmlspecialchars($event['title']); + } + $out .= ' '.$content.''."\n"; + } + if (!$fullspanevent) { + $sim = null; + foreach ($this->_simultaneous[$group] as $_sim) { + if ($_sim['time'] == $inter) { + $sim = $_sim; + break; + } + } + $diff = $max[$group] - ($sim['start'] + $sim['continued']); + for ($k=0; $k<$diff; $k++) { + $out .= '  '."\n"; + } + } + return $out; + } + + /** + * Get event spanning over the rows. + * + * @param array Event + * @param array Intervals + * @return int Spanning + */ + function getEventRowSpanning($event, $inters) + { + $start = substr($event['start'], 11, 5); + $end = substr($event['end'], 11, 5); + $span = 1; + foreach ($inters as $inter) { + if ($inter < $end and $inter > $start) { + $span++; + } + } + return $span; + } + + /** + * Generate the head of the calendar. + */ + function getHead() + { + $out = ''."\n".''."\n".'  '."\n"; + // Print the groups. + $groups = $this->getGroups(); + $max = $this->getMaxSimultaneous(); + foreach ($groups as $group) { + if ($max[$group] > 1) { + $span = ' colspan="'.$max[$group].'"'; + } else { + $span = ''; + } + $out .= ' '.$this->getGroupName($group).''."\n"; + } + $out .= ''."\n".''."\n"; + return $out; + } + + /** + * Get the rowspan for each day. + */ + function getDaySpanning() + { + list($start, $end) = $this->getStartEndDays(); + $inters = $this->getTimeIntervals($start, $end); + $n = $this->getDayNumber($start, $end); + $inter_n = array_fill(0, count($inters), 0); + $day_span = array_fill(0, $n+1, $inter_n); + foreach ($this->events as $event) { + // The event must be between $start and $end + $e_dstart = substr($event['start'], 0, 10); + $e_dend = substr($event['end'], 0, 10); + if ($e_dend < $start or $e_dstart > $end) { + continue; + } + + $day = $this->getDayNumber($start, substr($event['end'], 0, 10)); + $e_start = substr($event['start'], 11, 5); + $e_end = substr($event['end'], 11, 5); + $i = 0; + foreach ($inters as $inter) { + if ($inter < $e_end and $inter >= $e_start) { + $day_span[$day][$i]++; + } + $i++; + } + } + return $day_span; + } + + /** + * Get an array with the days of the week. + */ + function daysOfWeek() + { + return array( + __('Sunday'), + __('Monday'), + __('Tuesday'), + __('Wednesday'), + __('Thursday'), + __('Friday'), + __('Saturday'), + ); + } + + /** + * Get the number of days to list. + * + * @param string Start date + * @param string End date + * @return int Number of days + */ + function getDayNumber($start, $end) + { + Pluf::loadFunction('Pluf_Date_Compare'); + $diff = Pluf_Date_Compare($start.' 00:00:00', $end.' 00:00:00'); + return (int) $diff/86400; + } + + /** + * Get the start and end dates based on the event list. + * + * @return array (start day, end day) + */ + function getStartEndDays() + { + $start = '9999-12-31'; + $end = '0000-01-01'; + if (!isset($this->opts['start-day']) + or !isset($this->opts['end-day'])) { + foreach ($this->events as $event) { + $t_start = substr($event['start'], 0, 10); + $t_end = substr($event['end'], 0, 10); + if ($t_start < $start) { + $start = $t_start; + } + if ($t_end > $end) { + $end = $t_end; + } + } + } + if (isset($this->opts['start-day'])) { + $start = $this->opts['start-day']; + } else { + $this->opts['start-day'] = $start; + } + if (isset($this->opts['end-day'])) { + $end = $this->opts['end-day']; + } else { + $this->opts['end-day'] = $end; + } + return array($start, $end); + } + + /** + * Clean event list. + */ + function cleanEventList() + { + list($start, $end) = $this->getStartEndDays(); + $this->_events = array(); + foreach ($this->events as $event) { + $e_dstart = substr($event['start'], 0, 10); + $e_dend = substr($event['end'], 0, 10); + if ($e_dend < $start or $e_dstart > $end) { + continue; + } + $this->_events[] = $event; + } + return true; + } + + /** + * Get the time intervals. They span all the groups. + */ + function getTimeIntervals($start='', $end='') + { + if (count($this->_time_intervals)) { + return $this->_time_intervals; + } + $intervals = array(); + foreach ($this->_events as $event) { + $t = substr($event['start'], 11, 5); + if (!in_array($t, $intervals)) { + $intervals[] = $t; + } + $t = substr($event['end'], 11, 5); + if (!in_array($t, $intervals)) { + $intervals[] = $t; + } + } + sort($intervals); + $this->_time_intervals = $intervals; + return $intervals; + } + + /** + * Get simultaneous events at the same time slot and same group. + */ + function getSimultaneous() + { + foreach ($this->getGroups() as $group) { + $this->_simultaneous[$group] = array(); + foreach ($this->_time_intervals as $inter) { + $this->_simultaneous[$group][] = array('time' => $inter, + 'start' => 0, + 'continued' => 0); + } + } + foreach ($this->_events as $event) { + $group = $this->getEventGroup($event); + $e_tstart = substr($event['start'], 11, 5); + $e_tend = substr($event['end'], 11, 5); + foreach ($this->_simultaneous[$group] as $index=>$inter) { + if ($e_tstart == $inter['time']) { + $inter['start'] += 1; + $this->_simultaneous[$group][$index] = $inter; + continue; + } + if ($e_tstart < $inter['time'] and $e_tend > $inter['time']) { + $inter['continued'] += 1; + $this->_simultaneous[$group][$index] = $inter; + continue; + } + } + } + return $this->_simultaneous; + } + + /** + * Get maximum simultaneous events + */ + function getMaxSimultaneous() + { + if (count($this->_max_simultaneous) > 0) { + return $this->_max_simultaneous; + } + foreach ($this->getGroups() as $group) { + $this->_max_simultaneous[$group] = 0; + } + foreach ($this->_simultaneous as $group=>$choices) { + foreach ($choices as $count) { + if ($this->_max_simultaneous[$group] < $count['start'] + $count['continued']) { + $this->_max_simultaneous[$group] = $count['start'] + $count['continued']; + } + } + } + return $this->_max_simultaneous; + } + + + /** + * Overloading of the get method. + * + * @param string Property to get + */ + function __get($prop) + { + if ($prop == 'render') return $this->render(); + return $this->$prop; + } + +} \ No newline at end of file diff --git a/src/Pluf/Crypt.php b/src/Pluf/Crypt.php new file mode 100644 index 0000000..90d7bad --- /dev/null +++ b/src/Pluf/Crypt.php @@ -0,0 +1,111 @@ +key = $key; + } + + /** + * Encrypt a string with a key. + * + * If the key is not given, $this->key is used. If $this->key is + * empty an exception is raised. + * + * @param string String to encode + * @param string Encryption key ('') + * @return string Encoded string + */ + function encrypt($string, $key='') + { + if ($key == '') { + $key = $this->key; + } + if ($key == '') { + throw new Exception('No encryption key provided.'); + } + $result = ''; + $strlen = strlen($string); + $keylen = strlen($key); + for($i=0; $i<$strlen; $i++) { + $char = substr($string, $i, 1); + $keychar = substr($key, ($i % $keylen)-1, 1); + $char = chr(ord($char)+ord($keychar)); + $result.=$char; + } + $result = base64_encode($result); + return str_replace(array('+','/','='), array('-','_','~'), $result); + } + + /** + * Decrypt a string with a key. + * + * If the key is not given, $this->key is used. If $this->key is + * empty an exception is raised. + * + * @param string String to decode + * @param string Encryption key ('') + * @return string Decoded string + */ + function decrypt($string, $key='') + { + if ($key == '') { + $key = $this->key; + } + if ($key == '') { + throw new Exception('No encryption key provided.'); + } + $result = ''; + $string = str_replace(array('-','_','~'), array('+','/','='), $string); + $string = base64_decode($string); + $strlen = strlen($string); + $keylen = strlen($key); + for($i=0; $i<$strlen; $i++) { + $char = substr($string, $i, 1); + $keychar = substr($key, ($i % $keylen)-1, 1); + $char = chr(ord($char)-ord($keychar)); + $result.=$char; + } + return $result; + } +} diff --git a/src/Pluf/DB.php b/src/Pluf/DB.php new file mode 100644 index 0000000..2b6398a --- /dev/null +++ b/src/Pluf/DB.php @@ -0,0 +1,215 @@ +con_id) or is_object($GLOBALS['_PX_db']->con_id))) { + return $GLOBALS['_PX_db']; + } + $GLOBALS['_PX_db'] = Pluf_DB::get(Pluf::f('db_engine'), + Pluf::f('db_server'), + Pluf::f('db_database'), + Pluf::f('db_login'), + Pluf::f('db_password'), + Pluf::f('db_table_prefix'), + Pluf::f('debug'), + Pluf::f('db_version')); + return $GLOBALS['_PX_db']; +} + +/** + * Returns an array of default typecast and quoting for the database ORM. + * + * Foreach field type you need to provide an array with 2 functions, + * the from_db, the to_db. + * + * $value = from_db($value); + * $escaped_value = to_db($value, $dbobject); + * + * $escaped_value is ready to be put in the SQL, that is if this is a + * string, the value is quoted and escaped for example with SQLite: + * 'my string'' is escaped' or with MySQL 'my string\' is escaped' the + * starting ' and ending ' are included! + * + * @return array Default typecast. + */ +function Pluf_DB_defaultTypecast() +{ + return array( + 'Pluf_DB_Field_Boolean' => + array('Pluf_DB_BooleanFromDb', 'Pluf_DB_BooleanToDb'), + 'Pluf_DB_Field_Date' => + array('Pluf_DB_IdentityFromDb', 'Pluf_DB_IdentityToDb'), + 'Pluf_DB_Field_Datetime' => + array('Pluf_DB_IdentityFromDb', 'Pluf_DB_IdentityToDb'), + 'Pluf_DB_Field_Email' => + array('Pluf_DB_IdentityFromDb', 'Pluf_DB_IdentityToDb'), + 'Pluf_DB_Field_File' => + array('Pluf_DB_IdentityFromDb', 'Pluf_DB_IdentityToDb'), + 'Pluf_DB_Field_Float' => + array('Pluf_DB_IdentityFromDb', 'Pluf_DB_IdentityToDb'), + 'Pluf_DB_Field_Foreignkey' => + array('Pluf_DB_IntegerFromDb', 'Pluf_DB_IntegerToDb'), + 'Pluf_DB_Field_Integer' => + array('Pluf_DB_IntegerFromDb', 'Pluf_DB_IntegerToDb'), + 'Pluf_DB_Field_Password' => + array('Pluf_DB_IdentityFromDb', 'Pluf_DB_PasswordToDb'), + 'Pluf_DB_Field_Sequence' => + array('Pluf_DB_IntegerFromDb', 'Pluf_DB_IntegerToDb'), + 'Pluf_DB_Field_Text' => + array('Pluf_DB_IdentityFromDb', 'Pluf_DB_IdentityToDb'), + 'Pluf_DB_Field_Varchar' => + array('Pluf_DB_IdentityFromDb', 'Pluf_DB_IdentityToDb'), + 'Pluf_DB_Field_Serialized' => + array('Pluf_DB_SerializedFromDb', 'Pluf_DB_SerializedToDb'), + ); +} + +/** + * Identity function. + * + * @param mixed Value + * @return mixed Value + */ +function Pluf_DB_IdentityFromDb($val) +{ + return $val; +} + +/** + * Identity function. + * + * @param mixed Value. + * @param object Database handler. + * @return string Ready to use for SQL. + */ +function Pluf_DB_IdentityToDb($val, $db) +{ + if (is_null($val)) { + return 'NULL'; + } + return $db->esc($val); +} + +function Pluf_DB_SerializedFromDb($val) +{ + if ($val) { + return unserialize($val); + } + return $val; +} + +/** + * Identity function. + * + * @param mixed Value. + * @param object Database handler. + * @return string Ready to use for SQL. + */ +function Pluf_DB_SerializedToDb($val, $db) +{ + if (is_null($val)) { + return 'NULL'; + } + return $db->esc(serialize($val)); +} + +function Pluf_DB_BooleanFromDb($val) { + if ($val) { + return true; + } + return false; +} + +function Pluf_DB_BooleanToDb($val, $db) { + if (is_null($val)) { + return 'NULL'; + } + if ($val) { + return $db->esc('1'); + } + return $db->esc('0'); +} + +function Pluf_DB_IntegerFromDb($val) { + if (is_null($val)) return null; + return (int) $val; +} + +function Pluf_DB_IntegerToDb($val, $db) { + if (is_null($val)) { + return 'NULL'; + } + return (string)(int)$val; +} + +function Pluf_DB_PasswordToDb($val, $db) { + $exp = explode(':', $val); + if (in_array($exp[0], array('sha1', 'md5', 'crc32'))) { + return $db->esc($val); + } + // We need to hash the value. + $salt = Pluf_Utils::getRandomString(5); + return $db->esc('sha1:'.$salt.':'.sha1($salt.$val)); +} + diff --git a/src/Pluf/DB/Field.php b/src/Pluf/DB/Field.php new file mode 100644 index 0000000..4438e7b --- /dev/null +++ b/src/Pluf/DB/Field.php @@ -0,0 +1,110 @@ +value = $value; + $this->column = $column; + $this->extra = array_merge($this->extra, $extra); + } + + /** + * Get the form field for this field. + * + * We put this method at the field level as it allows us to easily + * create a new DB field and a new Form field and use them without + * the need to modify another place where the mapping would be + * performed. + * + * @param array Definition of the field. + * @param string Form field class. + */ + function formField($def, $form_field='Pluf_Form_Field_Varchar') + { + Pluf::loadClass('Pluf_Form_BoundField'); // To get mb_ucfirst + $defaults = array('required' => !$def['blank'], + 'label' => mb_ucfirst($def['verbose']), + 'help_text' => $def['help_text']); + unset($def['blank'], $def['verbose'], $def['help_text']); + if (isset($def['default'])) { + $defaults['initial'] = $def['default']; + unset($def['default']); + } + if (isset($def['choices'])) { + $defaults['widget'] = 'Pluf_Form_Widget_SelectInput'; + if (isset($def['widget_attrs'])) { + $def['widget_attrs']['choices'] = $def['choices']; + } else { + $def['widget_attrs'] = array('choices' => $def['choices']); + } + } + foreach (array_keys($def) as $key) { + if (!in_array($key, array('widget', 'label', 'required', + 'initial', 'choices', 'widget_attrs'))) { + unset($def[$key]); + } + } + $params = array_merge($defaults, $def); + return new $form_field($params); + } + +} + diff --git a/src/Pluf/DB/Field/Boolean.php b/src/Pluf/DB/Field/Boolean.php new file mode 100644 index 0000000..9353b40 --- /dev/null +++ b/src/Pluf/DB/Field/Boolean.php @@ -0,0 +1,32 @@ + 200); + + function formField($def, $form_field='Pluf_Form_Field_Email') + { + return parent::formField($def, $form_field); + } +} diff --git a/src/Pluf/DB/Field/File.php b/src/Pluf/DB/Field/File.php new file mode 100644 index 0000000..8974bb9 --- /dev/null +++ b/src/Pluf/DB/Field/File.php @@ -0,0 +1,76 @@ +methods = array(array(strtolower($column).'_url', 'Pluf_DB_Field_File_Url'), + array(strtolower($column).'_path', 'Pluf_DB_Field_File_Path') + ); + } + + function formField($def, $form_field='Pluf_Form_Field_File') + { + return parent::formField($def, $form_field); + } +} + +/** + * Returns the url to access the file. + */ +function Pluf_DB_Field_File_Url($field, $method, $model, $args=null) +{ + if (strlen($model->$field) != 0) { + return Pluf::f('upload_url').'/'.$model->$field; + } + return ''; +} + +/** + * Returns the path to access the file. + */ +function Pluf_DB_Field_File_Path($field, $method, $model, $args=null) +{ + if (strlen($model->$field) != 0) { + return Pluf::f('upload_path').'/'.$model->$field; + } + return ''; +} + diff --git a/src/Pluf/DB/Field/Float.php b/src/Pluf/DB/Field/Float.php new file mode 100644 index 0000000..5b70b81 --- /dev/null +++ b/src/Pluf/DB/Field/Float.php @@ -0,0 +1,27 @@ +getList() as $item) { + $choices[(string) $item] = $item->id; + } + $def['choices'] = $choices; + return parent::formField($def, $form_field); + } +} diff --git a/src/Pluf/DB/Field/Integer.php b/src/Pluf/DB/Field/Integer.php new file mode 100644 index 0000000..c83d43b --- /dev/null +++ b/src/Pluf/DB/Field/Integer.php @@ -0,0 +1,27 @@ +$method(); + $choices = array(); + foreach ($items as $item) { + $choices[(string) $item] = $item->id; + } + $def['choices'] = $choices; + if (!isset($def['widget'])) { + $def['widget'] = 'Pluf_Form_Widget_SelectMultipleInput'; + } + return parent::formField($def, $form_field); + } +} diff --git a/src/Pluf/DB/Field/Password.php b/src/Pluf/DB/Field/Password.php new file mode 100644 index 0000000..b327945 --- /dev/null +++ b/src/Pluf/DB/Field/Password.php @@ -0,0 +1,33 @@ +int = Pluf::factory('Pluf_DB_Introspect_'.$db->engine, $db); + $this->backend = $db->engine; + } + + /** + * Get the list of tables in the current database. The search + * automatically limit the list to the visible ones. + * + * @param object DB connection. + * @return array List of tables. + */ + function listTables() + { + if (!method_exists($this->int, 'listTables')) { + throw new Pluf_Exception_NotImplemented(); + } + return $this->int->listTables(); + } +} + + diff --git a/src/Pluf/DB/Introspect/PostgreSQL.php b/src/Pluf/DB/Introspect/PostgreSQL.php new file mode 100644 index 0000000..ddd6c09 --- /dev/null +++ b/src/Pluf/DB/Introspect/PostgreSQL.php @@ -0,0 +1,56 @@ +db = $db; + } + + /** + * Get the list of tables in the current database. The search + * automatically limit the list to the visible ones. + * + * @param object DB connection. + * @return array List of tables. + */ + function listTables() + { + $sql = 'SELECT c.relname AS name + FROM pg_catalog.pg_class c + LEFT JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN (\'r\', \'v\', \'\') + AND n.nspname NOT IN (\'pg_catalog\', \'pg_toast\') + AND pg_catalog.pg_table_is_visible(c.oid)'; + $res = $this->db->select($sql); + $tables = array(); + foreach ($res as $t) { + $tables[] = $t['name']; + } + return $tables; + } +} \ No newline at end of file diff --git a/src/Pluf/DB/MySQL.php b/src/Pluf/DB/MySQL.php new file mode 100644 index 0000000..4a17dc1 --- /dev/null +++ b/src/Pluf/DB/MySQL.php @@ -0,0 +1,172 @@ +type_cast = Pluf_DB_defaultTypecast(); + $this->debug('* MYSQL CONNECT'); + $this->con_id = @mysql_connect($server, $user, $pwd); + $this->debug = $debug; + $this->pfx = $pfx; + if (!$this->con_id) { + throw new Exception($this->getError()); + } + $this->database($dbname); + $this->execute('SET NAMES \'utf8\''); + } + + function database($dbname) + { + $db = @mysql_select_db($dbname); + $this->debug('* USE DATABASE '.$dbname); + if (!$db) { + throw new Exception($this->getError()); + } + return true; + } + + /** + * Get the version of the MySQL server. + * + * @return string Version string + */ + function getServerInfo() + { + return mysql_get_server_info($this->con_id); + } + + /** + * Log the queries. Keep track of the last query and if in debug mode + * keep track of all the queries in + * $GLOBALS['_PX_debug_data']['sql_queries'] + * + * @param string Query to keep track + * @return bool true + */ + function debug($query) + { + $this->lastquery = $query; + if (!$this->debug) return true; + if (!isset($GLOBALS['_PX_debug_data']['sql_queries'])) + $GLOBALS['_PX_debug_data']['sql_queries'] = array(); + $GLOBALS['_PX_debug_data']['sql_queries'][] = $query; + return true; + } + + function close() + { + if ($this->con_id) { + mysql_close($this->con_id); + return true; + } else { + return false; + } + } + + function select($query) + { + $this->debug($query); + $cur = mysql_query($query, $this->con_id); + if ($cur) { + $res = array(); + while ($row = mysql_fetch_assoc($cur)) { + $res[] = $row; + } + mysql_free_result($cur); + return $res; + } else { + throw new Exception($this->getError()); + } + } + + function execute($query) + { + $this->debug($query); + $cur = mysql_query($query, $this->con_id); + if (!$cur) { + throw new Exception($this->getError()); + } else { + return true; + } + } + + function getLastID() + { + $this->debug('* GET LAST ID'); + return (int) mysql_insert_id($this->con_id); + } + + /** + * Returns a string ready to be used in the exception. + * + * @return string Error string + */ + function getError() + { + + if ($this->con_id) { + return mysql_errno($this->con_id).' - ' + .mysql_error($this->con_id).' - '.$this->lastquery; + } else { + return mysql_errno().' - ' + .mysql_error().' - '.$this->lastquery; + } + } + + function esc($str) + { + return '\''.mysql_real_escape_string($str, $this->con_id).'\''; + } + + /** + * Quote the column name. + * + * @param string Name of the column + * @return string Escaped name + */ + function qn($col) + { + return '`'.$col.'`'; + } + + function __toString() + { + return 'con_id.')>'; + } + +} + diff --git a/src/Pluf/DB/PostgreSQL.php b/src/Pluf/DB/PostgreSQL.php new file mode 100644 index 0000000..8a48d3f --- /dev/null +++ b/src/Pluf/DB/PostgreSQL.php @@ -0,0 +1,220 @@ +type_cast = Pluf_DB_defaultTypecast(); + $this->type_cast['Pluf_DB_Field_Boolean'] = array('Pluf_DB_PostgreSQL_BooleanFromDb', 'Pluf_DB_BooleanToDb'); + + $this->debug('* POSTGRESQL CONNECT'); + $cstring = ''; + if ($server) { + $cstring .= 'host='.$server.' '; + } + $cstring .= 'dbname='.$dbname.' user='.$user; + if ($pwd) { + $cstring .= ' password='.$pwd; + } + $this->con_id = pg_connect($cstring); + $this->debug = $debug; + $this->pfx = $pfx; + $this->cur = null; //Current query cursor. + if (!$this->con_id) { + throw new Exception($this->getError()); + } + } + + + /** + * Get the version of the PostgreSQL server. + * + * Requires PostgreSQL 7.4 or later. + * + * @return string Version string + */ + function getServerInfo() + { + $ver = pg_version($this->con_id); + return $ver['server']; + } + + /** + * Log the queries. Keep track of the last query and if in debug mode + * keep track of all the queries in + * $GLOBALS['_PX_debug_data']['sql_queries'] + * + * @param string Query to keep track + * @return bool true + */ + function debug($query) + { + $this->lastquery = $query; + if (!$this->debug) return true; + if (!isset($GLOBALS['_PX_debug_data']['sql_queries'])) + $GLOBALS['_PX_debug_data']['sql_queries'] = array(); + $GLOBALS['_PX_debug_data']['sql_queries'][] = $query; + return true; + } + + function close() + { + if ($this->con_id) { + pg_close($this->con_id); + return true; + } else { + return false; + } + } + + function select($query) + { + $this->debug($query); + try { + $this->cur = @pg_query($this->con_id, $query); + } catch (Exception $e) { + throw new Exception($this->getError()); + } + $res = array(); + while ($row = @pg_fetch_assoc($this->cur)) { + $res[] = $row; + } + @pg_free_result($this->cur); + $this->cur = null; + return $res; + } + + function execute($query) + { + $this->debug($query); + try { + $this->cur = @pg_query($this->con_id, $query); + $this->cur = null; + return true; + } catch (Exception $e) { + throw new Exception($this->getError()); + } + } + + function getLastID() + { + $this->debug('* GET LAST ID'); + $res = $this->select('SELECT lastval() AS last_id'); + return (int) $res[0]['last_id']; + } + + /** + * Returns a string ready to be used in the exception. + * + * @return string Error string + */ + function getError() + { + if ($this->cur) { + return pg_result_error($this->cur).' - '.$this->lastquery; + } + if ($this->con_id) { + return pg_last_error($this->con_id).' - '.$this->lastquery; + } else { + return pg_last_error().' - '.$this->lastquery; + } + } + + function esc($str) + { + return '\''.pg_escape_string($this->con_id, $str).'\''; + } + + /** + * Set the current search path. + */ + function setSearchPath($search_path='public') + { + if (preg_match('/[^\w\s\,]/', $search_path)) { + throw new Exception('The search path: "'.$search_path.'" is not valid.'); + } + $this->execute('SET search_path TO '.$search_path); + return true; + } + + /** + * Quote the column name. + * + * @param string Name of the column + * @return string Escaped name + */ + function qn($col) + { + return '"'.$col.'"'; + } + + /** + * Start a transaction. + */ + function begin() + { + $this->execute('BEGIN'); + } + + /** + * Commit a transaction. + */ + function commit() + { + $this->execute('COMMIT'); + } + + /** + * Rollback a transaction. + */ + function rollback() + { + $this->execute('ROLLBACK'); + } + + function __toString() + { + return 'con_id.')>'; + } +} + +function Pluf_DB_PostgreSQL_BooleanFromDb($val) { + if (!$val) { + return false; + } + return (strtolower(substr($val, 0, 1)) == 't'); +} + diff --git a/src/Pluf/DB/SQLite.php b/src/Pluf/DB/SQLite.php new file mode 100644 index 0000000..ee20879 --- /dev/null +++ b/src/Pluf/DB/SQLite.php @@ -0,0 +1,169 @@ +type_cast = Pluf_DB_defaultTypecast(); + $this->debug = $debug; + $this->pfx = $pfx; + $this->debug('* SQLITE OPEN'); + // Connect and let the Exception be thrown in case of problem + try { + $this->con_id = new PDO('sqlite:'.$dbname); + } catch (PDOException $e) { + throw $e; + } + } + + /** + * Get the version of the SQLite library. + * + * @return string Version string + */ + function getServerInfo() + { + return $this->con_id->getAttribute(PDO::ATTR_SERVER_INFO); + } + + /** + * Log the queries. Keep track of the last query and if in debug mode + * keep track of all the queries in + * $GLOBALS['_PX_debug_data']['sql_queries'] + * + * @param string Query to keep track + * @return bool true + */ + function debug($query) + { + $this->lastquery = $query; + if (!$this->debug) return true; + if (!isset($GLOBALS['_PX_debug_data']['sql_queries'])) + $GLOBALS['_PX_debug_data']['sql_queries'] = array(); + $GLOBALS['_PX_debug_data']['sql_queries'][] = $query; + return true; + } + + function close() + { + $this->con_id = null; + return true; + } + + function select($query) + { + $this->debug($query); + if (false === ($cur = $this->con_id->query($query))) { + throw new Exception($this->getError()); + } + return $cur->fetchAll(PDO::FETCH_ASSOC); + } + + function execute($query) + { + $this->debug($query); + if (false === ($cur = $this->con_id->exec($query))) { + throw new Exception($this->getError()); + } + return $cur; + + } + + function getLastID() + { + $this->debug('* GET LAST ID'); + return (int) $this->con_id->lastInsertId();; + } + + /** + * Returns a string ready to be used in the exception. + * + * @return string Error string + */ + function getError() + { + $err = $this->con_id->errorInfo(); + $err[] = $this->lastquery; + return implode(' - ', $err); + } + + function esc($str) + { + return $this->con_id->quote($str); + } + + /** + * Quote the column name. + * + * @param string Name of the column + * @return string Escaped name + */ + function qn($col) + { + return '"'.$col.'"'; + } + + /** + * Start a transaction. + */ + function begin() + { + $this->execute('BEGIN'); + } + + /** + * Commit a transaction. + */ + function commit() + { + $this->execute('COMMIT'); + } + + /** + * Rollback a transaction. + */ + function rollback() + { + $this->execute('ROLLBACK'); + } + + function __toString() + { + return 'con_id.')>'; + } + +} + diff --git a/src/Pluf/DB/Schema.php b/src/Pluf/DB/Schema.php new file mode 100644 index 0000000..424c2f0 --- /dev/null +++ b/src/Pluf/DB/Schema.php @@ -0,0 +1,99 @@ +con = $db; + $this->model = $model; + $this->schema = Pluf::factory('Pluf_DB_Schema_'.$db->engine, $db); + } + + + /** + * Get the schema generator. + * + * @return object Pluf_DB_Schema_XXXX + */ + function getGenerator() + { + return $this->schema; + } + + /** + * Create the tables and indexes for the current model. + * + * @return mixed True if success or database error. + */ + function createTables() + { + $sql = $this->schema->getSqlCreate($this->model); + foreach ($sql as $k => $query) { + if (false === $this->con->execute($query)) { + throw new Exception($this->con->getError()); + } + } + $sql = $this->schema->getSqlIndexes($this->model); + foreach ($sql as $k => $query) { + if (false === $this->con->execute($query)) { + throw new Exception($this->con->getError()); + } + } + return true; + } + + /** + * Drop the tables and indexes for the current model. + * + * @return mixed True if success or database error. + */ + function dropTables() + { + $sql = $this->schema->getSqlDelete($this->model); + foreach ($sql as $k => $query) { + if (false === $this->con->execute($query)) { + throw new Exception($this->con->getError()); + } + } + return true; + } +} diff --git a/src/Pluf/DB/Schema/MySQL.php b/src/Pluf/DB/Schema/MySQL.php new file mode 100644 index 0000000..13e4f0b --- /dev/null +++ b/src/Pluf/DB/Schema/MySQL.php @@ -0,0 +1,210 @@ + 'varchar(%s)', + 'sequence' => 'mediumint(9) unsigned not null auto_increment', + 'boolean' => 'bool not null', + 'date' => 'date not null', + 'datetime' => 'datetime not null', + 'file' => 'varchar(150) not null', + 'manytomany' => null, + 'foreignkey' => 'mediumint(9) unsigned not null', + 'text' => 'longtext not null', + 'html' => 'longtext not null', + 'time' => 'time not null', + 'integer' => 'integer', + 'email' => 'varchar(150) not null', + 'password' => 'varchar(150) not null', + 'float' => 'numeric(%s, %s)', + ); + + public $defaults = array( + 'varchar' => "''", + 'sequence' => null, + 'boolean' => 1, + 'date' => 0, + 'datetime' => 0, + 'file' => "''", + 'manytomany' => null, + 'foreignkey' => 0, + 'text' => "''", + 'html' => "''", + 'time' => 0, + 'integer' => 0, + 'email' => "''", + 'password' => "''", + 'float' => 0.0, + + ); + private $con = null; + + function __construct($con) + { + $this->con = $con; + } + + + + /** + * Get the SQL to generate the tables of the given model. + * + * @param Object Model + * @return array Array of SQL strings ready to execute. + */ + function getSqlCreate($model) + { + $tables = array(); + $cols = $model->_a['cols']; + $manytomany = array(); + $sql = 'CREATE TABLE `'.$this->con->pfx.$model->_a['table'].'` ('; + + foreach ($cols as $col => $val) { + $field = new $val['type'](); + if ($field->type != 'manytomany') { + $sql .= "\n`".$col.'` '; + $_tmp = $this->mappings[$field->type]; + if ($field->type == 'varchar') { + if (isset($val['size'])) { + $_tmp = sprintf($this->mappings['varchar'], $val['size']); + } else { + $_tmp = sprintf($this->mappings['varchar'], '150'); + } + } + if ($field->type == 'float') { + if (!isset($val['max_digits'])) { + $val['max_digits'] = 32; + } + if (!isset($val['decimal_places'])) { + $val['decimal_places'] = 8; + } + $_tmp = sprintf($this->mappings['float'], $val['max_digits'], $val['decimal_places']); + } + $sql .= $_tmp; + if (isset($val['default'])) { + $sql .= ' default '.$this->con->esc($val['default']); + } elseif ($field->type != 'sequence') { + $sql .= ' default '.$this->defaults[$field->type]; + } + $sql .= ','; + } else { + $manytomany[] = $col; + } + } + $sql .= "\n".'primary key (`id`)'; + $sql .= "\n".') ENGINE=MyISAM'; + $sql .=' DEFAULT CHARSET=utf8;'; + $tables[$this->con->pfx.$model->_a['table']] = $sql; + + //Now for the many to many + foreach ($manytomany as $many) { + $omodel = new $cols[$many]['model'](); + $hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model'])); + sort($hay); + $table = $hay[0].'_'.$hay[1].'_assoc'; + $sql = 'CREATE TABLE `'.$this->con->pfx.$table.'` ('; + $sql .= "\n".'`'.strtolower($model->_a['model']).'_id` '.$this->mappings['foreignkey'].' default 0,'; + $sql .= "\n".'`'.strtolower($omodel->_a['model']).'_id` '.$this->mappings['foreignkey'].' default 0,'; + $sql .= "\n".'primary key (`'.strtolower($model->_a['model']).'_id`, `'.strtolower($omodel->_a['model']).'_id`)'; + $sql .= "\n".') ENGINE=MyISAM'; + $sql .=' DEFAULT CHARSET=utf8;'; + $tables[$this->con->pfx.$table] = $sql; + } + return $tables; + } + + /** + * Get the SQL to generate the indexes of the given model. + * + * @param Object Model + * @return array Array of SQL strings ready to execute. + */ + function getSqlIndexes($model) + { + $index = array(); + foreach ($model->_a['idx'] as $idx => $val) { + if (!isset($val['col'])) { + $val['col'] = $idx; + } + $index[$this->con->pfx.$model->_a['table'].'_'.$idx] = + sprintf('CREATE INDEX `%s` ON `%s` (`%s`);', + $idx, $this->con->pfx.$model->_a['table'], $val['col']); + } + foreach ($model->_a['cols'] as $col => $val) { + $field = new $val['type'](); + if ($field->type == 'foreignkey') { + $index[$this->con->pfx.$model->_a['table'].'_'.$col.'_foreignkey'] = + sprintf('CREATE INDEX `%s` ON `%s` (`%s`);', + $col.'_foreignkey_idx', $this->con->pfx.$model->_a['table'], $col); + } + if (isset($val['unique']) and $val['unique'] == true) { + $index[$this->con->pfx.$model->_a['table'].'_'.$col.'_unique'] = + sprintf('CREATE UNIQUE INDEX `%s` ON `%s` (`%s`);', + $col.'_unique_idx', $this->con->pfx.$model->_a['table'], $col); + } + } + return $index; + } + + /** + * Get the SQL to drop the tables corresponding to the model. + * + * @param Object Model + * @return string SQL string ready to execute. + */ + function getSqlDelete($model) + { + $cols = $model->_a['cols']; + $manytomany = array(); + $sql = 'DROP TABLE IF EXISTS `'.$this->con->pfx.$model->_a['table'].'`'; + + foreach ($cols as $col => $val) { + $field = new $val['type'](); + if ($field->type == 'manytomany') { + $manytomany[] = $col; + } + } + + //Now for the many to many + foreach ($manytomany as $many) { + $omodel = new $cols[$many]['model'](); + $hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model'])); + sort($hay); + $table = $hay[0].'_'.$hay[1].'_assoc'; + $sql .= ', `'.$this->con->pfx.$table.'`'; + } + return array($sql); + + } +} diff --git a/src/Pluf/DB/Schema/PostgreSQL.php b/src/Pluf/DB/Schema/PostgreSQL.php new file mode 100644 index 0000000..eedff82 --- /dev/null +++ b/src/Pluf/DB/Schema/PostgreSQL.php @@ -0,0 +1,210 @@ + 'character varying', + 'sequence' => 'serial', + 'boolean' => 'boolean', + 'date' => 'date', + 'datetime' => 'timestamp', + 'file' => 'character varying', + 'manytomany' => null, + 'foreignkey' => 'integer', + 'text' => 'text', + 'html' => 'text', + 'time' => 'time', + 'integer' => 'integer', + 'email' => 'character varying', + 'password' => 'character varying', + 'float' => 'real', + ); + + public $defaults = array( + 'varchar' => "''", + 'sequence' => null, + 'boolean' => 'FALSE', + 'date' => "'0001-01-01'", + 'datetime' => "'0001-01-01 00:00:00'", + 'file' => "''", + 'manytomany' => null, + 'foreignkey' => 0, + 'text' => "''", + 'html' => "''", + 'time' => "'00:00:00'", + 'integer' => 0, + 'email' => "''", + 'password' => "''", + 'float' => 0.0, + + ); + private $con = null; + + function __construct($con) + { + $this->con = $con; + } + + + + /** + * Get the SQL to generate the tables of the given model. + * + * @param Object Model + * @return array Array of SQL strings ready to execute. + */ + function getSqlCreate($model) + { + $tables = array(); + $cols = $model->_a['cols']; + $manytomany = array(); + $query = 'CREATE TABLE '.$this->con->pfx.$model->_a['table'].' ('; + $sql_col = array(); + $constraints = array(); + foreach ($cols as $col => $val) { + $field = new $val['type'](); + if ($field->type != 'manytomany') { + $sql = $this->con->qn($col).' '; + $sql .= $this->mappings[$field->type]; + if (empty($val['is_null'])) { + $sql .= ' NOT NULL'; + } + if (isset($val['default'])) { + $sql .= ' default '; + $sql .= $model->_toDb($val['default'], $col); + } elseif ($field->type != 'sequence') { + $sql .= ' default '.$this->defaults[$field->type]; + } + $sql_col[] = $sql; + } else { + $manytomany[] = $col; + } + if ($field->type == 'foreignkey') { + // Add the foreignkey constraints + $referto = new $val['model'](); + $_c = 'CONSTRAINT '.$this->con->pfx.$model->_a['table'].'_'.$col.'_fkey FOREIGN KEY ('.$this->con->qn($col).') + REFERENCES '.$this->con->pfx.$referto->_a['table'].' (id) MATCH SIMPLE + ON UPDATE NO ACTION ON DELETE NO ACTION'; + $constraints[] = $_c; + } + } + $sql_col[] = 'CONSTRAINT '.$this->con->pfx.$model->_a['table'].'_pkey PRIMARY KEY (id)'; + $sql_col = array_merge($sql_col, $constraints); + $query = $query."\n".implode(",\n", $sql_col)."\n".');'; + $tables[$this->con->pfx.$model->_a['table']] = $query; + //Now for the many to many + //FIXME add index on the second column + foreach ($manytomany as $many) { + $omodel = new $cols[$many]['model'](); + $hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model'])); + sort($hay); + $table = $hay[0].'_'.$hay[1].'_assoc'; + $sql = 'CREATE TABLE '.$this->con->pfx.$table.' ('; + $sql .= "\n".strtolower($model->_a['model']).'_id '.$this->mappings['foreignkey'].' default 0,'; + $sql .= "\n".strtolower($omodel->_a['model']).'_id '.$this->mappings['foreignkey'].' default 0,'; + $sql .= "\n".'CONSTRAINT '.$this->con->pfx.$table.'_pkey PRIMARY KEY ('.strtolower($model->_a['model']).'_id, '.strtolower($omodel->_a['model']).'_id)'; + $sql .= "\n".');'; + $tables[$this->con->pfx.$table] = $sql; + } + return $tables; + } + + /** + * Get the SQL to generate the indexes of the given model. + * + * @param Object Model + * @return array Array of SQL strings ready to execute. + */ + function getSqlIndexes($model) + { + $index = array(); + foreach ($model->_a['idx'] as $idx => $val) { + if (!isset($val['col'])) { + $val['col'] = $idx; + } + if ($val['type'] == 'unique') { + $unique = 'UNIQUE '; + } else { + $unique = ''; + } + $index[$this->con->pfx.$model->_a['table'].'_'.$idx] = + sprintf('CREATE '.$unique.'INDEX %s ON %s (%s);', + $this->con->pfx.$model->_a['table'].'_'.$idx, + $this->con->pfx.$model->_a['table'], + $val['col']); + } + foreach ($model->_a['cols'] as $col => $val) { + $field = new $val['type'](); + if (isset($val['unique']) and $val['unique'] == true) { + $index[$this->con->pfx.$model->_a['table'].'_'.$col.'_unique'] = + sprintf('CREATE UNIQUE INDEX %s ON %s (%s);', + $this->con->pfx.$model->_a['table'].'_'.$col.'_unique_idx', + $this->con->pfx.$model->_a['table'], + $col); + } + } + return $index; + } + + /** + * Get the SQL to drop the tables corresponding to the model. + * + * @param Object Model + * @return string SQL string ready to execute. + */ + function getSqlDelete($model) + { + $cols = $model->_a['cols']; + $manytomany = array(); + $sql = array(); + $sql[] = 'DROP TABLE IF EXISTS '.$this->con->pfx.$model->_a['table'].' CASCADE'; + foreach ($cols as $col => $val) { + $field = new $val['type'](); + if ($field->type == 'manytomany') { + $manytomany[] = $col; + } + } + + //Now for the many to many + foreach ($manytomany as $many) { + $omodel = new $cols[$many]['model'](); + $hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model'])); + sort($hay); + $table = $hay[0].'_'.$hay[1].'_assoc'; + $sql[] = 'DROP TABLE IF EXISTS '.$this->con->pfx.$table.' CASCADE'; + } + return $sql; + } +} + diff --git a/src/Pluf/DB/Schema/SQLite.php b/src/Pluf/DB/Schema/SQLite.php new file mode 100644 index 0000000..5676992 --- /dev/null +++ b/src/Pluf/DB/Schema/SQLite.php @@ -0,0 +1,227 @@ + 'varchar(%s)', + 'sequence' => 'integer primary key autoincrement', + 'boolean' => 'bool', + 'date' => 'date', + 'datetime' => 'datetime', + 'file' => 'varchar(150)', + 'manytomany' => null, + 'foreignkey' => 'integer', + 'text' => 'text', + 'html' => 'text', + 'time' => 'time', + 'integer' => 'integer', + 'email' => 'varchar(150)', + 'password' => 'varchar(150)', + 'float' => 'real', + ); + + public $defaults = array( + 'varchar' => "''", + 'sequence' => null, + 'boolean' => 1, + 'date' => 0, + 'datetime' => 0, + 'file' => "''", + 'manytomany' => null, + 'foreignkey' => 0, + 'text' => "''", + 'html' => "''", + 'time' => 0, + 'integer' => 0, + 'email' => "''", + 'password' => "''", + 'float' => 0.0, + + ); + private $con = null; + + function __construct($con) + { + $this->con = $con; + } + + + + /** + * Get the SQL to generate the tables of the given model. + * + * @param Object Model + * @return array Array of SQL strings ready to execute. + */ + function getSqlCreate($model) + { + $tables = array(); + $cols = $model->_a['cols']; + $manytomany = array(); + $query = 'CREATE TABLE '.$this->con->pfx.$model->_a['table'].' ('; + $sql_col = array(); + foreach ($cols as $col => $val) { + $field = new $val['type'](); + if ($field->type != 'manytomany') { + $sql = $this->con->qn($col).' '; + $_tmp = $this->mappings[$field->type]; + if ($field->type == 'varchar') { + if (isset($val['size'])) { + $_tmp = sprintf($this->mappings['varchar'], $val['size']); + } else { + $_tmp = sprintf($this->mappings['varchar'], '150'); + } + } + if ($field->type == 'float') { + if (!isset($val['max_digits'])) { + $val['max_digits'] = 32; + } + if (!isset($val['decimal_places'])) { + $val['decimal_places'] = 8; + } + $_tmp = sprintf($this->mappings['float'], $val['max_digits'], $val['decimal_places']); + } + $sql .= $_tmp; + if (empty($val['is_null'])) { + $sql .= ' not null'; + } + if (isset($val['default'])) { + $sql .= ' default '.$model->_toDb($val['default'], $col); + } elseif ($field->type != 'sequence') { + $sql .= ' default '.$this->defaults[$field->type]; + } + $sql_col[] = $sql; + } else { + $manytomany[] = $col; + } + } + $query = $query."\n".implode(",\n", $sql_col)."\n".');'; + $tables[$this->con->pfx.$model->_a['table']] = $query; + + //Now for the many to many + foreach ($manytomany as $many) { + $omodel = new $cols[$many]['model'](); + $hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model'])); + sort($hay); + $table = $hay[0].'_'.$hay[1].'_assoc'; + $sql = 'CREATE TABLE '.$this->con->pfx.$table.' ('; + $sql .= "\n".strtolower($model->_a['model']).'_id '.$this->mappings['foreignkey'].' default 0,'; + $sql .= "\n".strtolower($omodel->_a['model']).'_id '.$this->mappings['foreignkey'].' default 0,'; + $sql .= "\n".'primary key ('.strtolower($model->_a['model']).'_id, '.strtolower($omodel->_a['model']).'_id)'; + $sql .= "\n".');'; + $tables[$this->con->pfx.$table] = $sql; + } + return $tables; + } + + /** + * Get the SQL to generate the indexes of the given model. + * + * @param Object Model + * @return array Array of SQL strings ready to execute. + */ + function getSqlIndexes($model) + { + $index = array(); + foreach ($model->_a['idx'] as $idx => $val) { + if (!isset($val['col'])) { + $val['col'] = $idx; + } + if (false !== strpos($val['col'], ',')) { + $out = array(); + foreach (explode(',', $val['col']) as $col) { + $out[] = $this->con->qn(trim($col)); + } + $val['col'] = implode(', ', $out); + } else { + $val['col'] = $this->con->qn($val['col']); + } + $index[$this->con->pfx.$model->_a['table'].'_'.$idx] = + sprintf('CREATE INDEX %s ON %s (%s);', + $this->con->pfx.$model->_a['table'].'_'.$idx, + $this->con->pfx.$model->_a['table'], + $val['col']); + } + foreach ($model->_a['cols'] as $col => $val) { + $field = new $val['type'](); + if ($field->type == 'foreignkey') { + $index[$this->con->pfx.$model->_a['table'].'_'.$col.'_foreignkey'] = + sprintf('CREATE INDEX %s ON %s (%s);', + $this->con->pfx.$model->_a['table'].'_'.$col.'_foreignkey_idx', + $this->con->pfx.$model->_a['table'], + $this->con->qn($col)); + } + if (isset($val['unique']) and $val['unique'] == true) { + $index[$this->con->pfx.$model->_a['table'].'_'.$col.'_unique'] = + sprintf('CREATE UNIQUE INDEX %s ON %s (%s);', + $this->con->pfx.$model->_a['table'].'_'.$col.'_unique_idx', + $this->con->pfx.$model->_a['table'], + $this->con->qn($col)); + } + } + return $index; + } + + /** + * Get the SQL to drop the tables corresponding to the model. + * + * @param Object Model + * @return string SQL string ready to execute. + */ + function getSqlDelete($model) + { + $cols = $model->_a['cols']; + $manytomany = array(); + $sql = array(); + $sql[] = 'DROP TABLE IF EXISTS '.$this->con->pfx.$model->_a['table']; + foreach ($cols as $col => $val) { + $field = new $val['type'](); + if ($field->type == 'manytomany') { + $manytomany[] = $col; + } + } + + //Now for the many to many + foreach ($manytomany as $many) { + $omodel = new $cols[$many]['model'](); + $hay = array(strtolower($model->_a['model']), strtolower($omodel->_a['model'])); + sort($hay); + $table = $hay[0].'_'.$hay[1].'_assoc'; + $sql[] = 'DROP TABLE IF EXISTS '.$this->con->pfx.$table; + } + return $sql; + + } +} + diff --git a/src/Pluf/DB/SchemaInfo.php b/src/Pluf/DB/SchemaInfo.php new file mode 100644 index 0000000..ba81f1a --- /dev/null +++ b/src/Pluf/DB/SchemaInfo.php @@ -0,0 +1,53 @@ +_a['table'] = 'schema_info'; + $this->_a['model'] = 'Pluf_DB_SchemaInfo'; + $this->_a['cols'] = array( + // It is mandatory to have an "id" column. + 'id' => + array( + 'type' => 'Pluf_DB_Field_Sequence', + //It is automatically added. + 'blank' => true, + ), + 'application' => + array( + 'type' => 'Pluf_DB_Field_Varchar', + 'blank' => false, + 'unique' => true, + ), + 'version' => + array( + 'type' => 'Pluf_DB_Field_Integer', + 'blank' => false, + ), + ); + } +} diff --git a/src/Pluf/Date.php b/src/Pluf/Date.php new file mode 100644 index 0000000..7ff9004 --- /dev/null +++ b/src/Pluf/Date.php @@ -0,0 +1,186 @@ + $date2) return -1; + return 1; + } +} + +/** + * Set of functions to manage dates. + */ + +/** + * Compare two date and returns the number of seconds between the + * first and the second. If only the date is given without time, the + * end of the day is used (23:59:59). + * + * @param string Date to compare for ex: '2006-09-17 18:42:00' + * @param string Second date to compare if null use now (null) + * @return int Number of seconds between the two dates. Negative + * value if the second date is before the first. + */ +function Pluf_Date_Compare($date1, $date2=null) +{ + if (strlen($date1) == 10) { + $date1 .= ' 23:59:59'; + } + if (is_null($date2)) { + $date2 = time(); + } else { + if (strlen($date2) == 10) { + $date2 .= ' 23:59:59'; + } + $date2 = strtotime(str_replace('-', '/', $date2)); + } + $date1 = strtotime(str_replace('-', '/', $date1)); + return $date2 - $date1; +} + +/** + * Display a date in the format: + * X days Y hours ago + * X hours Y minutes ago + * X hours Y minutes left + * + * "resolution" is year, month, day, hour, minute. + * + * If not time is given, only the day, the end of the day is + * used: 23:59:59. + * + * @param string Date to compare with ex: '2006-09-17 18:42:00' + * @param string Reference date to compare with by default now (null) + * @param int Maximum number of elements to show (2) + * @param string If no delay between the two dates display ('now') + * @param bool Show ago/left suffix + * @return string Formatted date + */ +function Pluf_Date_Easy($date, $ref=null, $blocks=2, $notime='now', $show=true) +{ + if (strlen($date) == 10) { + $date .= ' 23:59:59'; + } + if (is_null($ref)) { + $ref = date('Y-m-d H:i:s'); + $tref = time(); + } else { + if (strlen($ref) == 10) { + $ref .= ' 23:59:59'; + } + $tref = strtotime(str_replace('-', '/', $ref)); + } + $tdate = strtotime(str_replace('-', '/', $date)); + $past = true; + if ($tref < $tdate) { + // date in the past + $past = false; + $_tmp = $ref; + $ref = $date; + $date = $_tmp; + } + $ref = str_replace(array(' ', ':'), '-', $ref); + $date = str_replace(array(' ', ':'), '-', $date); + $refs = split('-', $ref); + $dates = split('-', $date); + // Modulo on the month is dynamically calculated after + $modulos = array(365, 12, 31, 24, 60, 60); + // day in month + $month = $refs[1] - 1; + $modulos[2] = date('t', mktime(0, 0, 0, $month, 1, $refs[0])); + $diffs = array(); + for ($i=0; $i<6; $i++) { + $diffs[$i] = $refs[$i] - $dates[$i]; + } + $retain = 0; + for ($i=5; $i>-1; $i--) { + $diffs[$i] = $diffs[$i] - $retain; + $retain = 0; + if ($diffs[$i] < 0) { + $diffs[$i] = $modulos[$i] + $diffs[$i]; + $retain = 1; + } + } + $val = array(__('year'), __('month'), __('day'), __('hour'), + __('minute'), __('second')); + $vals = array(__('years'), __('months'), __('days'), __('hours'), + __('minutes'), __('seconds')); + $res = ''; + $total = 0; + for ($i=0; $i<5; $i++) { + if ($diffs[$i] > 0) { + $total++; + $res .= $diffs[$i].' '; + if ($diffs[$i] > 1) { + $res .= $vals[$i].' '; + } else { + $res .= $val[$i].' '; + } + } + if ($total >= $blocks) break; + } + if (strlen($res) == 0) { + return $notime; + } + if ($show) { + if ($past) { + $res .= ' '.__('ago'); + } else { + $res .= ' '.__('left'); + } + } + return $res; +} diff --git a/src/Pluf/Dispatcher.php b/src/Pluf/Dispatcher.php new file mode 100644 index 0000000..b62ff61 --- /dev/null +++ b/src/Pluf/Dispatcher.php @@ -0,0 +1,221 @@ +process_request($req); + if ($response !== false) { + // $response is a response + $response->render($req->method != 'HEAD' and !defined('IN_UNIT_TESTS')); + $skip = true; + break; + } + } + } + if ($skip === false) { + $response = self::match($req); + if (!empty($req->response_vary_on)) { + $response->headers['Vary'] = $req->response_vary_on; + } + $middleware = array_reverse($middleware); + foreach ($middleware as $mw) { + if (method_exists($mw, 'process_response')) { + $response = $mw->process_response($req, $response); + } + } + $response->render($req->method != 'HEAD' and !defined('IN_UNIT_TESTS')); + } + return array($req, $response); + } + + /** + * Match a query against the actions controllers. + * + * @param Pluf_HTTP_Request Request object + * @return Pluf_HTTP_Response Response object + */ + public static function match($req) + { + // Order the controllers by priority + foreach ($GLOBALS['_PX_views'] as $key => $control) { + $priority[$key] = $control['priority']; + } + array_multisort($priority, SORT_ASC, $GLOBALS['_PX_views']); + foreach ($GLOBALS['_PX_views'] as $key => $ctl) { + $match = array(); + if (preg_match($ctl['regex'], $req->query, $match)) { + try { + $req->view = $ctl; + $m = new $ctl['model'](); + if (isset($m->{$ctl['method'].'_precond'})) { + // Here we have preconditions to respects. If + // the "answer" is true, then ok go ahead, if + // not then it a response so return it or an + // exception so let it go. + $preconds = $m->{$ctl['method'].'_precond'}; + if (!is_array($preconds)) { + $preconds = array($preconds); + } + foreach ($preconds as $precond) { + $res = call_user_func(explode('::', $precond), $req); + if ($res !== true) { + return $res; + } + } + } + if (!isset($ctl['params'])) { + return $m->$ctl['method']($req, $match); + } else { + return $m->$ctl['method']($req, $match, $ctl['params']); + } + } catch (Pluf_HTTP_Error404 $e) { + // Need to add a 404 error handler + // something like Pluf::f('404_handler', 'class::method') + } catch (Exception $e) { + if (Pluf::f('debug', false) == true) { + return new Pluf_HTTP_Response_ServerErrorDebug($e); + } else { + return new Pluf_HTTP_Response_ServerError($e); + } + } + } + } + return new Pluf_HTTP_Response_NotFound(sprintf(__('The page %s was not found on the server.'), htmlspecialchars($req->query))); + } + + /** + * Load the controllers. + * + * @param string File including the views. + * @param string Possible prefix to add to the views. + * @return bool Success. + */ + public static function loadControllers($file, $prefix='') + { + if (file_exists($file)) { + if ($prefix == '') { + $GLOBALS['_PX_views'] = include $file; + } else { + $GLOBALS['_PX_views'] = Pluf_Dispatcher::addPrefixToViewFile($prefix, $file); + } + return true; + } + return false; + } + + + /** + * Register an action controller. + * + * - The class must provide a "standalone" action method + * class::actionmethod($request, $match) + * - The priority is to order the controller matches. + * 5: Default, if the controller provides some content + * 1: If the controller provides a control before, without providing + * content, note that in this case the return code must be a redirection. + * 8: If the controller is providing a catch all case to replace the + * default 404 error page. + * + * @param string Class name providing the action controller + * @param string The method of the plugin to be called + * @param string Regex to match on the query string + * @param int Priority (5) + * @return void + */ + public static function registerController($model, $method, $regex, $priority=5) + { + if (!isset($GLOBALS['_PX_views'])) { + $GLOBALS['_PX_views'] = array(); + } + $GLOBALS['_PX_views'][] = array('model' => $model, + 'regex' => $regex, + 'priority' => $priority, + 'method' => $method); + } + + /** + * Add the controllers of an application with a given prefix. + * + * Suppose you have a new app you want to use within another + * existing application, you may need to change the base URL not + * to conflict with the existing one. For example you want to have + * domain.com/forum-a/ and domain.com/forum-b/ to use 2 forums at + * the same time. + * + * This method do that, it takes a typical "view" file and rewrite + * the regex to append the prefix. Note that you should use the + * 'url' tag in the template and use Pluf_HTTP_URL_reverse in the + * views to not hardcode the urls or this will not work. + * + * @param string Prefix, for example '/alternate'. + * @param string File with the views. + * @return array Prefixed views. + */ + static public function addPrefixToViewFile($prefix, $file) + { + if (file_exists($file)) { + $views = include $file; + } else { + throw new Exception('View file not found: '.$file); + } + return Pluf_Dispatcher::addPrefixToViews($prefix, $views); + } + + /** + * Add a prefix to an array of views. + * + * You can use it for example to not hardcode that in your CMS the + * blog is located as /blog but is configured in the configuration + * of the CMS, that way in French this could be /carnet. + * + * @param string Prefix, for example '/alternate'. + * @param array Array of the views. + * @return array Prefixed views. + */ + static public function addPrefixToViews($prefix, $views) + { + $res = array(); + foreach ($views as $view) { + $view['regex'] = '#^'.$prefix.substr($view['regex'], 2); + $res[] = $view; + } + return $res; + } +} + diff --git a/src/Pluf/Encoder.php b/src/Pluf/Encoder.php new file mode 100644 index 0000000..17b1bc5 --- /dev/null +++ b/src/Pluf/Encoder.php @@ -0,0 +1,186 @@ +form = $form; + } + + /** + * Check if could be empty or not. + */ + function checkEmpty($data, $form=array(), $p=array()) + { + if (strlen($data) == 0 + and isset($p['blank']) and false == $p['blank']) { + throw new Pluf_Form_Invalid(__('The value must not be empty.')); + } + return true; + } + + + /** + * Validate an url. + * Only the structure is checked, no check of availability of the + * url is performed. It is a really basic validation. + */ + static function url($url, $form=array(), $p=array()) + { + $ip = '(25[0-5]|2[0-4]\d|[0-1]?\d?\d)(\.' + .'(25[0-5]|2[0-4]\d|[0-1]?\d?\d)){3}'; + $dom = '([a-z0-9\.\-]+)'; + if (preg_match('!^(http|https|ftp|gopher)\://('.$ip.'|'.$dom.')!i', $url)) { + return $url; + } else { + throw new Pluf_Form_Invalid(sprintf(__('The URL %s is not valid.'), htmlspecialchars($url))); + } + } + + static function varchar($string, $form=array(), $p=array()) + { + if (isset($p['size']) && strlen($string) > $p['size']) { + throw new Pluf_Form_Invalid(sprintf(__('The value should not be more than %s characters long.'), $p['size'])); + } + return $string; + } + + static function password($string, $form=array(), $p=array()) + { + if (strlen($string) < 6) { + throw new Pluf_Form_Invalid(sprintf(__('The password must be at least %s characters long.'), '6')); + } + return $string; + } + + static function email($string, $form=array(), $p=array()) + { + if (preg_match('/^[A-Z0-9._%-][+A-Z0-9._%-]*@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}$/i', $string)) { + return $string; + } else { + throw new Pluf_Form_Invalid(sprintf(__('The email address "%s" is not valid.'), + $string)); + } + } + + static function text($string, $form=array(), $p=array()) + { + return Pluf_Encoder::varchar($string, $form, $p); + } + + + static function sequence($id, $form=array(), $p=array()) + { + return Pluf_Encoder::integer($id, $p); + } + + static function boolean($bool, $form=array(), $p=array()) + { + if (in_array($bool, array('on', 'y', '1', 1, true))) { + return true; + } + return false; + } + + static function foreignkey($id, $form=array(), $p=array()) + { + return Pluf_Encoder::integer($id, $p); + } + + static function integer($int, $form=array(), $p=array()) + { + if (!preg_match('/[0-9]+/', $int)) { + throw new Pluf_Form_Invalid(__('The value must be an integer.')); + } + return (int) $int; + } + + static function datetime($datetime, $form=array(), $p=array()) + { + if (false === ($stamp = strtotime($datetime))) { + throw new Pluf_Form_Invalid(sprintf(__('The date and time %s are not valid.'), htmlspecialchars($datetime))); + } + //convert to GMT + return gmdate('Y-m-d H:i:s', $stamp); + } + + static function date($date, $form=array(), $p=array()) + { + $ymd = explode('-', $date); + if (count($ymd) != 3 or strlen($ymd[0]) != 4 + or false === checkdate($ymd[1], $ymd[2], $ymd[0])) { + throw new Pluf_Form_Invalid(sprintf(__('The date %s is not valid.'), htmlspecialchars($date))); + } + return $date; + } + + static function manytomany($vals, $form=array(), $p=array()) + { + $res = array(); + foreach ($vals as $val) { + $res[] = Pluf_Encoder::integer($val); + } + return $res; + } + + static function float($val, $form=array(), $p=array()) + { + return (float) $val; + } + + /* + 'file' => "''", + 'manytomany' => null, + 'foreignkey' => 0, + 'text' => "''", + 'html' => "''", + 'time' => 0, + 'integer' => 0, + ); + */ +} diff --git a/src/Pluf/Error.php b/src/Pluf/Error.php new file mode 100644 index 0000000..db723bc --- /dev/null +++ b/src/Pluf/Error.php @@ -0,0 +1,117 @@ +error = array(); + } + + /** + * Set an error. + * + * By convention the number is 4xx if the error is coming from + * the user or 5xx if coming from the system (database error for ex.) + * + * @param string Error message + * @param int Error number (0) + */ + function setError($msg, $no=0) + { + $this->error[] = array($no,$msg); + } + + + /** + * Returns the errors. + * + * @param bool Errors as HTML list (false) + * @param bool Show numbers (true) + * @return mixed array of errors, HTML list, or false if no errors + */ + function error($html=false, $with_nb=true) + { + if (count($this->error) > 0) { + if (!$html) { + return $this->error; + } else { + $res = '
    '."\n"; + foreach($this->error as $v) { + $res .= '
  • '. + (($with_nb) ? + ''.$v[0].' - ' : + ''). + ''.$v[1].'
  • '."\n"; + } + return $res."
\n"; + } + } else { + return false; + } + } + + /** + * Helper function to set the error from the DB. + * + * @param string Error message from the DB + */ + function setDbError($db_error_msg) + { + $this->setError(__('DB error:').' '.$db_error_msg, 500); + } + + /** + * Bulk set errors. + * + * Used when you went to recopy the errors of one object into + * another. You can call that way: + * $object->bulkSetErrors($otherobject->error()); + * + * @param array List of errors + * @return bool Success + */ + function bulkSetError($errors) + { + if (!is_array($errors)) { + return false; + } + foreach ($errors as $e) { + $this->setError($e[1], $e[0]); + } + return true; + } + +} diff --git a/src/Pluf/Exception.php b/src/Pluf/Exception.php new file mode 100644 index 0000000..22d1082 --- /dev/null +++ b/src/Pluf/Exception.php @@ -0,0 +1,26 @@ +data = $data; + $this->is_bound = true; + } + if ($label_suffix !== null) $this->label_suffix = $label_suffix; + + $this->initFields($extra); + $this->f = new Pluf_Form_FieldProxy($this); + } + + function initFields($extra=array()) + { + throw new Exception('Definition of the fields not implemented.'); + } + + /** + * Add the prefix to the form names. + * + * @param string Field name. + * @return string Field name or field name with form prefix. + */ + function addPrefix($field_name) + { + if ('' !== $this->prefix) { + return $this->prefix.'-'.$field_name; + } + return $field_name; + } + + /** + * Check if the form is valid. + * + * It is also encoding the data in the form to be then saved. It + * is very simple as it leaves the work to the field. It means + * that you can easily extend this form class to have a more + * complex validation procedure like checking if a field is equals + * to another in the form (like for password confirmation) etc. + * + * @param array Associative array of the request + * @return array Array of errors + */ + function isValid() + { + if ($this->is_valid !== null) { + return $this->is_valid; + } + $this->cleaned_data = array(); + $this->errors = array(); + $form_methods = get_class_methods($this); + foreach ($this->fields as $name=>$field) { + $value = $field->widget->valueFromFormData($this->addPrefix($name), + $this->data); + try { + $value = $field->clean($value); + $this->cleaned_data[$name] = $value; + if (in_array('clean_'.$name, $form_methods)) { + $m = 'clean_'.$name; + $value = $this->$m(); + $this->cleaned_data[$name] = $value; + } + } catch (Pluf_Form_Invalid $e) { + if (!isset($this->errors[$name])) $this->errors[$name] = array(); + $this->errors[$name][] = $e->getMessage(); + if (isset($this->cleaned_data[$name])) { + unset($this->cleaned_data[$name]); + } + } + } + if (empty($this->errors)) { + try { + $this->cleaned_data = $this->clean(); + } catch (Pluf_Form_Invalid $e) { + if (!isset($this->errors['__all__'])) $this->errors['__all__'] = array(); + $this->errors['__all__'][] = $e->getMessage(); + } + } + if (empty($this->errors)) { + $this->is_valid = true; + return true; + } + // as some errors, we do not have cleaned data available. + $this->cleaned_data = array(); + $this->is_valid = false; + return false; + } + + /** + * Form wide cleaning function. That way you can check that if an + * input is given, then another one somewhere is also given, + * etc. If the cleaning is not ok, your method must throw a + * Pluf_Form_Invalid exception. + * + * @return array Cleaned data. + */ + public function clean() + { + return $this->cleaned_data; + } + + /** + * Get initial data for a given field. + * + * @param string Field name. + * @return string Initial data or '' of not defined. + */ + public function initial($name) + { + if (isset($this->fields[$name])) { + return $this->fields[$name]->initial; + } + return ''; + } + + /** + * Get the top errors. + */ + public function render_top_errors() + { + $top_errors = (isset($this->errors['__all__'])) ? $this->errors['__all__'] : array(); + array_walk($top_errors, 'Pluf_Form_htmlspecialcharsArray'); + return new Pluf_Template_SafeString(Pluf_Form_renderErrorsAsHTML($top_errors), true); + } + + /** + * Get the top errors. + */ + public function get_top_errors() + { + return (isset($this->errors['__all__'])) ? $this->errors['__all__'] : array(); + } + + /** + * Helper function to render the form. + * + * See render_p() for a usage example. + * + * @credit Django Project (http://www.djangoproject.com/) + * @param string Normal row. + * @param string Error row. + * @param string Row ender. + * @param string Help text HTML. + * @param bool Should we display errors on a separate row. + * @return string HTML of the form. + */ + protected function htmlOutput($normal_row, $error_row, $row_ender, + $help_text_html, $errors_on_separate_row) + { + $top_errors = (isset($this->errors['__all__'])) ? $this->errors['__all__'] : array(); + array_walk($top_errors, 'Pluf_Form_htmlspecialcharsArray'); + $output = array(); + $hidden_fields = array(); + foreach ($this->fields as $name=>$field) { + $bf = new Pluf_Form_BoundField($this, $field, $name); + $bf_errors = $bf->errors; + array_walk($bf_errors, 'Pluf_Form_htmlspecialcharsArray'); + if ($field->widget->is_hidden) { + foreach ($bf_errors as $_e) { + $top_errors[] = sprintf(__('(Hidden field %1$s) %2$s'), + $name, $_e); + } + $hidden_fields[] = $bf; // Not rendered + } else { + if ($errors_on_separate_row and count($bf_errors)) { + $output[] = sprintf($error_row, Pluf_Form_renderErrorsAsHTML($bf_errors)); + } + if (strlen($bf->label) > 0) { + $label = htmlspecialchars($bf->label, ENT_COMPAT, 'UTF-8'); + if ($this->label_suffix) { + if (!in_array(mb_substr($label, -1, 1), + array(':','?','.','!'))) { + $label .= $this->label_suffix; + } + } + $label = $bf->labelTag($label); + } else { + $label = ''; + } + if ($bf->help_text) { + // $bf->help_text can contains HTML and is not + // escaped. + $help_text = sprintf($help_text_html, $bf->help_text); + } else { + $help_text = ''; + } + $errors = ''; + if (!$errors_on_separate_row and count($bf_errors)) { + $errors = Pluf_Form_renderErrorsAsHTML($bf_errors); + } + $output[] = sprintf($normal_row, $errors, $label, + $bf->render_w(), $help_text); + } + } + if (count($top_errors)) { + $errors = sprintf($error_row, + Pluf_Form_renderErrorsAsHTML($top_errors)); + array_unshift($output, $errors); + } + if (count($hidden_fields)) { + $_tmp = ''; + foreach ($hidden_fields as $hd) { + $_tmp .= $hd->render_w(); + } + if (count($output)) { + $last_row = array_pop($output); + $last_row = substr($last_row, 0, -strlen($row_ender)).$_tmp + .$row_ender; + $output[] = $last_row; + } else { + $output[] = $_tmp; + } + + } + return new Pluf_Template_SafeString(implode("\n", $output), true); + } + + /** + * Render the form as a list of paragraphs. + */ + public function render_p() + { + return $this->htmlOutput('

%1$s%2$s %3$s%4$s

', '%s', '

', + ' %s', true); + } + + /** + * Render the form as a list without the
    . + */ + public function render_ul() + { + return $this->htmlOutput('
  • %1$s%2$s %3$s%4$s
  • ', '
  • %s
  • ', + '', ' %s', false); + } + + /** + * Render the form as a list without the
      . + */ + public function render_table() + { + return $this->htmlOutput('%2$s%1$s%3$s%4$s', + '%s', + '', '
      %s', false); + } + + /** + * Overloading of the get method. + * + * The overloading is to be able to use property call in the + * templates. + */ + function __get($prop) + { + if (!in_array($prop, array('render_p', 'render_ul', 'render_table', 'render_top_errors', 'get_top_errors'))) { + return $this->$prop; + } + return $this->$prop(); + } + + /** + * Get a given field by key. + */ + public function field($key) + { + return new Pluf_Form_BoundField($this, $this->fields[$key], $key); + + } + + /** + * Iterator method to iterate over the fields. + * + * Get the current item. + */ + public function current() + { + $field = current($this->fields); + $name = key($this->fields); + return new Pluf_Form_BoundField($this, $field, $name); + } + + public function key() + { + return key($this->fields); + } + + public function next() + { + next($this->fields); + } + + public function rewind() + { + reset($this->fields); + } + + public function valid() + { + // We know that the boolean false will not be stored as a + // field, so we can test against false to check if valid or + // not. + return (false !== current($this->fields)); + } + +} + + +function Pluf_Form_htmlspecialcharsArray(&$item, $key) +{ + $item = htmlspecialchars($item, ENT_COMPAT, 'UTF-8'); +} + +function Pluf_Form_renderErrorsAsHTML($errors) +{ + $tmp = array(); + foreach ($errors as $err) { + $tmp[] = '
    • '.$err.'
    • '; + } + return '
        '.implode("\n", $tmp).'
      '; +} diff --git a/src/Pluf/Form/BoundField.php b/src/Pluf/Form/BoundField.php new file mode 100644 index 0000000..29fcb97 --- /dev/null +++ b/src/Pluf/Form/BoundField.php @@ -0,0 +1,156 @@ +form = $form; + $this->field = $field; + $this->name = $name; + $this->html_name = $this->form->addPrefix($name); + if ($this->field->label == '') { + $this->label = mb_ereg_replace('/\_/', '/ /', mb_ucfirst($name)); + } else { + $this->label = $this->field->label; + } + $this->help_text = ($this->field->help_text) ? $this->field->help_text : ''; + if (isset($this->form->errors[$name])) { + $this->errors = $this->form->errors[$name]; + } + } + + public function render_w($widget=null, $attrs=array()) + { + if ($widget === null) { + $widget = $this->field->widget; + } + $id = $this->autoId(); + if ($id and !array_key_exists('id', $attrs) + and !array_key_exists('id', $widget->attrs)) { + $attrs['id'] = $id; + } + if (!$this->form->is_bound) { + $data = $this->form->initial($this->name); + } else { + $data = $this->field->widget->valueFromFormData($this->html_name, $this->form->data); + } + return $widget->render($this->html_name, $data, $attrs); + } + + /** + * Returns the HTML of the label tag. Wraps the given contents in + * a