changeset 0:9ee956af41e3

Initial commit
author Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
date Sun, 27 Jun 2010 22:05:12 +0200
parents
children c2954a9e5665
files COPYING README configuration.js errors.js forms.js iso8601.js nodes.js psgxs.js save.json storage.js util.js
diffstat 11 files changed, 3171 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,661 @@
+                    GNU AFFERO GENERAL PUBLIC LICENSE
+                       Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+  The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works.  By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+  When we speak of free software, we are referring to freedom, 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
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+  Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+  A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate.  Many developers of free software are heartened and
+encouraged by the resulting cooperation.  However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+  The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community.  It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server.  Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+  An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals.  This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                       TERMS AND CONDITIONS
+
+  0. Definitions.
+
+  "This License" refers to version 3 of the GNU Affero General Public License.
+
+  "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+  "The Program" refers to any copyrightable work licensed under this
+License.  Each licensee is addressed as "you".  "Licensees" and
+"recipients" may be individuals or organizations.
+
+  To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy.  The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+  A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+  To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy.  Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+  To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies.  Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+  An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License.  If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+  1. Source Code.
+
+  The "source code" for a work means the preferred form of the work
+for making modifications to it.  "Object code" means any non-source
+form of a work.
+
+  A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+  The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form.  A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+  The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities.  However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work.  For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+  The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+  The Corresponding Source for a work in source code form is that
+same work.
+
+  2. Basic Permissions.
+
+  All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met.  This License explicitly affirms your unlimited
+permission to run the unmodified Program.  The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work.  This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+  You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force.  You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright.  Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+  Conveying under any other circumstances is permitted solely under
+the conditions stated below.  Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+  3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+  No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+  When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+  4. Conveying Verbatim Copies.
+
+  You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+  You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+  5. Conveying Modified Source Versions.
+
+  You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+    a) The work must carry prominent notices stating that you modified
+    it, and giving a relevant date.
+
+    b) The work must carry prominent notices stating that it is
+    released under this License and any conditions added under section
+    7.  This requirement modifies the requirement in section 4 to
+    "keep intact all notices".
+
+    c) You must license the entire work, as a whole, under this
+    License to anyone who comes into possession of a copy.  This
+    License will therefore apply, along with any applicable section 7
+    additional terms, to the whole of the work, and all its parts,
+    regardless of how they are packaged.  This License gives no
+    permission to license the work in any other way, but it does not
+    invalidate such permission if you have separately received it.
+
+    d) If the work has interactive user interfaces, each must display
+    Appropriate Legal Notices; however, if the Program has interactive
+    interfaces that do not display Appropriate Legal Notices, your
+    work need not make them do so.
+
+  A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit.  Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+  6. Conveying Non-Source Forms.
+
+  You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+    a) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by the
+    Corresponding Source fixed on a durable physical medium
+    customarily used for software interchange.
+
+    b) Convey the object code in, or embodied in, a physical product
+    (including a physical distribution medium), accompanied by a
+    written offer, valid for at least three years and valid for as
+    long as you offer spare parts or customer support for that product
+    model, to give anyone who possesses the object code either (1) a
+    copy of the Corresponding Source for all the software in the
+    product that is covered by this License, on a durable physical
+    medium customarily used for software interchange, for a price no
+    more than your reasonable cost of physically performing this
+    conveying of source, or (2) access to copy the
+    Corresponding Source from a network server at no charge.
+
+    c) Convey individual copies of the object code with a copy of the
+    written offer to provide the Corresponding Source.  This
+    alternative is allowed only occasionally and noncommercially, and
+    only if you received the object code with such an offer, in accord
+    with subsection 6b.
+
+    d) Convey the object code by offering access from a designated
+    place (gratis or for a charge), and offer equivalent access to the
+    Corresponding Source in the same way through the same place at no
+    further charge.  You need not require recipients to copy the
+    Corresponding Source along with the object code.  If the place to
+    copy the object code is a network server, the Corresponding Source
+    may be on a different server (operated by you or a third party)
+    that supports equivalent copying facilities, provided you maintain
+    clear directions next to the object code saying where to find the
+    Corresponding Source.  Regardless of what server hosts the
+    Corresponding Source, you remain obligated to ensure that it is
+    available for as long as needed to satisfy these requirements.
+
+    e) Convey the object code using peer-to-peer transmission, provided
+    you inform other peers where the object code and Corresponding
+    Source of the work are being offered to the general public at no
+    charge under subsection 6d.
+
+  A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+  A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling.  In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage.  For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product.  A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+  "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source.  The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+  If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information.  But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+  The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed.  Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+  Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+  7. Additional Terms.
+
+  "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law.  If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+  When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it.  (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.)  You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+  Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+    a) Disclaiming warranty or limiting liability differently from the
+    terms of sections 15 and 16 of this License; or
+
+    b) Requiring preservation of specified reasonable legal notices or
+    author attributions in that material or in the Appropriate Legal
+    Notices displayed by works containing it; or
+
+    c) Prohibiting misrepresentation of the origin of that material, or
+    requiring that modified versions of such material be marked in
+    reasonable ways as different from the original version; or
+
+    d) Limiting the use for publicity purposes of names of licensors or
+    authors of the material; or
+
+    e) Declining to grant rights under trademark law for use of some
+    trade names, trademarks, or service marks; or
+
+    f) Requiring indemnification of licensors and authors of that
+    material by anyone who conveys the material (or modified versions of
+    it) with contractual assumptions of liability to the recipient, for
+    any liability that these contractual assumptions directly impose on
+    those licensors and authors.
+
+  All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10.  If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term.  If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+  If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+  Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+  8. Termination.
+
+  You may not propagate or modify a covered work except as expressly
+provided under this License.  Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+  However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+  Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+  Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License.  If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+  9. Acceptance Not Required for Having Copies.
+
+  You are not required to accept this License in order to receive or
+run a copy of the Program.  Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance.  However,
+nothing other than this License grants you permission to propagate or
+modify any covered work.  These actions infringe copyright if you do
+not accept this License.  Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+  10. Automatic Licensing of Downstream Recipients.
+
+  Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License.  You are not responsible
+for enforcing compliance by third parties with this License.
+
+  An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations.  If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+  You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License.  For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+  11. Patents.
+
+  A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based.  The
+work thus licensed is called the contributor's "contributor version".
+
+  A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version.  For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+  Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+  In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement).  To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+  If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients.  "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+  If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+  A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License.  You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+  Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+  12. No Surrender of Others' Freedom.
+
+  If 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 convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all.  For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+  13. Remote Network Interaction; Use with the GNU General Public License.
+
+  Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software.  This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+  Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work.  The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+  14. Revised Versions of this License.
+
+  The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero 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
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation.  If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+  If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+  Later license versions may give you additional or different
+permissions.  However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+  15. Disclaimer of Warranty.
+
+  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "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 PROGRAM
+IS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+  16. Limitation of Liability.
+
+  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM 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 PROGRAM (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 PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+  17. Interpretation of Sections 15 and 16.
+
+  If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    <one line to give the program's name and a brief idea of what it does.>
+    Copyright (C) <year>  <name of author>
+
+    This program is free software: you can redistribute it and/or modify
+    it under the terms of the GNU Affero General Public License as published by
+    the Free Software Foundation, either version 3 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU Affero General Public License for more details.
+
+    You should have received a copy of the GNU Affero General Public License
+    along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+  If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source.  For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code.  There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+  You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
new file mode 100644
--- /dev/null
+++ b/README
@@ -0,0 +1,69 @@
+                      _
+    _____   _____   _/_\_   _____
+   |  _  \ /  ___/ /  ___| /  ___/
+   | |_| | | |___  | |     | |___
+   |  ___/ \___  \ | |  _  \___  \
+   | |      ___| | | |_| |  ___| |
+   |_|     /_____/ \_____/ /_____/
+
+Homepage: http://linkmauve.fr/dev/psgxs
+Forge page: http://codingteam.net/project/psgxs
+
+PSĜS is PubSub server written in JavaScript. Its goal is to be a
+full-featured server implementing the PubSub specification
+(<http://xmpp.org/extensions/xep-0060.html>).
+
+=======================
+        Install
+=======================
+PSĜS use node.js, the xmpp.js library and its dependance, node-xml. You
+can find these software here:
+http://nodejs.org/
+http://github.com/robrighter/node-xml
+http://xmppjs.prosody.im/
+
+Or if your distribution has these packaged (as in ArchLinux), simply
+install the packaged version.
+
+The config file is <configuration.js>, in the source directory, and the
+file for the persistent storage of the tree is <save.json>.
+
+When you have the dependances installed, you can simply run:
+ % ./psgxs
+to launch PSĜS.
+
+There is no install procedure for now.
+
+Please DO report any bug you encounter and ask for any
+feature you want.
+
+=======================
+        Authors
+=======================
+Emmanuel Gil Peyrot (Link Mauve) <linkmauve@linkmauve.fr>
+
+=======================
+    Contact/support
+=======================
+XMPP ChatRoom:     psgxs@conference.codingteam.net
+Report a bug:      http://codingteam.net/project/psgxs/bugs/add
+
+=======================
+        License
+=======================
+PSĜS is Free Software.
+(learn more: http://www.gnu.org/philosophy/free-sw.html)
+
+PSĜS is released under the Gnu AGPLv3 license
+Please read the COPYING file for details
+
+=======================
+        Thanks
+=======================
+= People =
+ Paul Sowden (<http://delete.me.uk/>) - ISO 8601 date functions
+
+======================
+       The code
+======================
+… is awful! :D
new file mode 100644
--- /dev/null
+++ b/configuration.js
@@ -0,0 +1,223 @@
+/*
+ *  Copyright (C) 2010  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ *
+ *  This file is part of PSĜS, a PubSub server written in JavaScript.
+ *
+ *  PSĜS is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License.
+ *
+ *  PSĜS is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with PSĜS.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var config = exports;
+
+config.jid = 'pubsub.example.org';
+config.password = 'pubsub.example.org';
+
+config.activated = [
+	//'access-authorize',
+	//'access-open',
+	//'access-presence', // Impossible à utiliser dans un component
+	//'access-roster', // Impossible à utiliser dans un component
+	'access-whitelist',
+	'auto-create',
+	//'auto-subscribe', // Impossible à utiliser dans un component
+	//'collections', //TODO
+	'config-node',
+	'create-and-configure',
+	'create-nodes',
+	'delete-items',
+	'delete-nodes',
+	//'filtered-notifications', // Impossible à utiliser dans un component
+	'get-pending',
+	'instant-nodes',
+	'item-ids',
+	'last-published', // Impossible de se baser sur la présence dans un component
+	//'leased-subscription', //TODO
+	'manage-subscriptions',
+	'member-affiliation',
+	'meta-data',
+	'modify-affiliations',
+	//'multi-collection', //TODO
+	//'multi-subscribe', //TODO
+	'outcast-affiliation',
+	'persistent-items',
+	//'presence-notifications', // Impossible à utiliser dans un component
+	//'presence-subscribe', // Impossible à utiliser dans un component
+	'publish',
+	'publish-options',
+	'publish-only-affiliation',
+	'publisher-affiliation',
+	'purge-nodes',
+	'retract-items',
+	'retrieve-affiliations',
+	'retrieve-default',
+	'retrieve-default-sub',
+	'retrieve-items',
+	'retrieve-subscriptions',
+	'subscribe',
+	'subscription-options',
+	'subscription-notifications',
+];
+
+config.service_configuration = {
+	subscribe_authorization: {
+		FORM_TYPE: {type: 'hidden', value: 'http://jabber.org/protocol/pubsub#subscribe_authorization'},
+		_TITLE: 'PubSub subscriber request',
+		_INSTRUCTIONS: 'To approve this entity’s subscription request,\nclick the OK button. To deny the request, click the\ncancel button.',
+		'pubsub#allow': {type: 'boolean', label: 'Whether to allow the subscription', value: false},
+		'pubsub#node': {type: 'text-single', label: 'The NodeID of the relevant node'},
+		'pubsub#subid': {type: 'hidden'},
+		'pubsub#subscriber_jid': {type: 'jid-single', label: 'The address (JID) of the subscriber'},
+	},
+	subscribe_options: {
+		FORM_TYPE: {type: 'hidden', value: 'http://jabber.org/protocol/pubsub#subscribe_options'},
+		'pubsub#deliver': {type: 'boolean', label: 'Whether an entity wants to receive or disable notifications', value: true},
+		'pubsub#digest': {type: 'boolean', label: 'Whether an entity wants to receive digests (aggregations) of notifications or all notifications individually', value: false},
+		'pubsub#digest_frequency': {type: 'text-single', label: 'The minimum number of milliseconds between sending any two notification digests', value: 60*1000},
+//		'pubsub#expire': {type: 'text-single', label: 'The DateTime at which a leased subscription will end or has ended'},
+//		'pubsub#include_body': {type: 'boolean', label: 'Whether an entity wants to receive an XMPP message body in addition to the payload format', value: false},
+//		'pubsub#show-values': {type: 'list-multi', label: 'The presence states for which an entity wants to receive notifications', options: {
+//			away: {label: 'XMPP Show Value of Away'},
+//			chat: {label: 'XMPP Show Value of Chat'},
+//			dnd: {label: 'XMPP Show Value of DND (Do Not Disturb)'},
+//			online: {label: 'Mere Availability in XMPP (No Show Value)'},
+//			xa: {label: 'XMPP Show Value of XA (Extended Away)'},
+//		}, value: ['online', 'chat', 'away']},
+//		'pubsub#subscription_type': {type: 'list-single', options: {
+//			items: {label: 'Receive notification of new items only'},
+//			nodes: {label: 'Receive notification of new nodes only'},
+//		}},
+//		'pubsub#subscription_depth': {type: 'list-single', options: {
+//			'1': {label: 'Receive notification from direct child nodes only'},
+//			all: {label: 'Receive notification from all descendent nodes'},
+//		}},
+	},
+	node_metadata: {
+		FORM_TYPE: {type: 'hidden', value: 'http://jabber.org/protocol/pubsub#meta-data'},
+		'pubsub#contact': {type: 'jid-multi', label: 'The JIDs of those to contact with questions'},
+		'pubsub#creation_date': {type: 'text-single', label: 'The datetime when the node was created', value: function(){return new Date();}},
+		'pubsub#creator': {type: 'jid-single', label: 'The JID of the node creator'},
+		'pubsub#description': {type: 'text-single', label: 'A description of the node'},
+		'pubsub#language': {type: 'list-single', label: 'The default language of the node'},
+		'pubsub#num_subscribers': {type: 'text-single', label: 'The number of subscribers to the node', value: 0},
+		'pubsub#owner': {type: 'jid-multi', label: 'The JIDs of those with an affiliation of owner'},
+		'pubsub#publisher': {type: 'jid-multi', label: 'The JIDs of those with an affiliation of publisher'},
+		'pubsub#title': {type: 'text-single', label: 'The name of the node'},
+		'pubsub#type': {type: 'text-single', label: 'Payload type'},
+	},
+	node_config: {
+		FORM_TYPE: {type: 'hidden', value: 'http://jabber.org/protocol/pubsub#node_config'},
+		'pubsub#access_model': {type: 'list-single', label: 'Who may subscribe and retrieve items', options: {
+			authorize: {label: 'Subscription requests must be approved and only subscribers may retrieve items'},
+			open: {label: 'Anyone may subscribe and retrieve items'},
+//			presence: {label: 'Anyone with a presence subscription of both or from may subscribe and retrieve items'},
+//			roster: {label: 'Anyone in the specified roster group(s) may subscribe and retrieve items'},
+			whitelist: {label: 'Only those on a whitelist may subscribe and retrieve items'},
+		}},
+//		'pubsub#body_xslt': {type: 'text-single', label: 'The URL of an XSL transformation which can be applied to payloads in order to generate an appropriate message body element.'},
+//		'pubsub#children_association_policy': {type: 'list-single', label: 'Who may associate leaf nodes with a collection', options: {
+//			all: {label: 'Anyone may associate leaf nodes with the collection'},
+//			owners: {label: 'Only collection node owners may associate leaf nodes with the collection'},
+//			whitelist: {label: 'Only those on a whitelist may associate leaf nodes with the collection'},
+//		}},
+//		'pubsub#children_association_whitelist': {type: 'jid-multi', label: 'The list of JIDs that may associate leaf nodes with a collection'},
+//		'pubsub#children': {type: 'text-multi', label: 'The child nodes (leaf or collection) associated with a collection'},
+//		'pubsub#children_max': {type: 'text-single', label: 'The maximum number of child nodes that can be associated with a collection'},
+//		'pubsub#collection': {type: 'text-multi', label: 'The collection(s) with which a node is affiliated'},
+		'pubsub#contact': {type: 'jid-multi', label: 'The JIDs of those to contact with questions'},
+//		'pubsub#dataform_xslt': {type: 'text-single', label: 'The URL of an XSL transformation which can be applied to the payload format in order to generate a valid Data Forms result that the client could display using a generic Data Forms rendering engine'},
+		'pubsub#deliver_notifications': {type: 'boolean', label: 'Whether to deliver event notifications', value: true},
+//		'pubsub#deliver_payloads': {type: 'boolean', label: 'Whether to deliver payloads with event notifications; applies only to leaf nodes'},
+		'pubsub#description': {type: 'text-single', label: 'A description of the node'},
+//		'pubsub#item_expire': {type: 'text-single', label: 'Number of seconds after which to automatically purge items'},
+//		'pubsub#itemreply': {type: 'list-single', label: 'Whether owners or publisher should receive replies to items', options: {
+//			owner: {label: 'Statically specify a replyto of the node owner(s)'},
+//			publisher: {label: 'Dynamically specify a replyto of the item publisher'},
+//		}},
+		'pubsub#language': {type: 'list-single', label: 'The default language of the node'},
+//		'pubsub#max_items': {type: 'text-single', label: 'The maximum number of items to persist'},
+//		'pubsub#max_payload_size': {type: 'text-single', label: 'The maximum payload size in bytes'},
+		'pubsub#node_type': {type: 'list-single', label: 'Whether the node is a leaf (default) or a collection', options: {
+			leaf: {label: 'The node is a leaf node (default)'},
+			collection: {label: 'The node is a collection node'},
+		}, value: 'leaf'},
+//		'pubsub#notification_type': {type: 'list-single', label: 'Specify the delivery style for notifications', options: {
+//			normal: {label: 'Messages of type normal'},
+//			headline: {label: 'Messages of type headline'},
+//			iq: {label: 'IQ stanzas (works only with presence-based delivery)'},
+//		}},
+//		'pubsub#notify_config': {type: 'boolean', label: 'Whether to notify subscribers when the node configuration changes'},
+//		'pubsub#notify_delete': {type: 'boolean', label: 'Whether to notify subscribers when the node is deleted'},
+//		'pubsub#notify_retract': {type: 'boolean', label: 'Whether to notify subscribers when items are removed from the node'},
+//		'pubsub#notify_sub': {type: 'boolean', label: 'Whether to notify owners about new subscribers and unsubscribes'},
+//		'pubsub#persist_items': {type: 'boolean', label: 'Whether to persist items to storage'},
+//		'pubsub#presence_based_delivery': {type: 'boolean', label: 'Whether to deliver notifications to available users only'},
+		'pubsub#publish_model': {type: 'list-single', label: 'The publisher model', options: {
+			publishers: {label: 'Only publishers may publish'},
+			subscribers: {label: 'Subscribers may publish'},
+			open: {label: 'Anyone may publish'},
+		}, value: 'publishers'},
+//		'pubsub#purge_offline': {type: 'boolean', label: 'Whether to purge all items when the relevant publisher goes offline'},
+//		'pubsub#roster_groups_allowed': {type: 'list-multi', label: 'The roster group(s) allowed to subscribe and retrieve items'},
+//		'pubsub#send_last_published_item': {type: 'list-single', label: 'When to send the last published item', options: {
+//			never: {label: 'Never'},
+//			on_sub: {label: 'When a new subscription is processed'},
+//			on_sub_and_presence: {label: 'When a new subscription is processed and whenever a subscriber comes online'},
+//		}, value: 'never'},
+//		'pubsub#tempsub': {type: 'boolean', label: 'Whether to make all subscriptions temporary, based on subscriber presence'},
+		'pubsub#subscribe': {type: 'boolean', label: 'Whether to allow subscriptions', value: true},
+		'pubsub#title': {type: 'text-single', label: 'A friendly name for the node'},
+		'pubsub#type': {type: 'text-single', label: 'The type of node data, usually specified by the namespace of the payload (if any)', value: 'http://www.w3.org/2005/Atom'},
+	},
+	'publish-options': {
+		FORM_TYPE: {type: 'hidden', value: 'http://jabber.org/protocol/pubsub#publish-options'},
+		'pubsub#access_model': {type: 'list-single', label: 'Precondition: node configuration with the specified access model', options: {
+			authorize: {label: 'Access model of authorize'},
+			open: {label: 'Access model of open'},
+//			presence: {label: 'Access model of presence'},
+//			roster: {label: 'Access model of roster'},
+			whitelist: {label: 'Access model of whitelist'},
+		}, value: 'open'},
+	},
+};
+
+config.Configuration = function(def, params) {
+	for (var i in def) {
+		if (typeof (def[i].value) != 'undefined') {
+			if (typeof (def[i].value) == 'function')
+				this[i] = def[i].value();
+			else
+				this[i] = def[i].value;
+		}
+	}
+	if (params)
+		for (var i in params)
+			this[i] = params[i];
+};
+
+config.enabled = function(feature) {
+	for (var i in config.activated)
+		if (typeof i == 'string' && feature == config.activated[i])
+			return true;
+	return false;
+};
+
+if (config.enabled('access-whitelist')) {
+	config.service_configuration.node_config['pubsub#access_model'].value = 'whitelist';
+	config.service_configuration['publish-options']['pubsub#access_model'].value = 'whitelist';
+} else if (config.enabled('access-authorize')) {
+	config.service_configuration.node_config['pubsub#access_model'].value = 'authorize';
+	config.service_configuration['publish-options']['pubsub#access_model'].value = 'authorize';
+} else if (config.enabled('access-open')) {
+	config.service_configuration.node_config['pubsub#access_model'].value = 'open';
+	config.service_configuration['publish-options']['pubsub#access_model'].value = 'open';
+}
new file mode 100644
--- /dev/null
+++ b/errors.js
@@ -0,0 +1,198 @@
+/*
+ *  Copyright (C) 2010  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ *
+ *  This file is part of PSĜS, a PubSub server written in JavaScript.
+ *
+ *  PSĜS is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License.
+ *
+ *  PSĜS is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with PSĜS.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var errors = exports;
+
+errors.success = true;
+errors.feature_not_implemented = {n: 1, type: 'cancel', error: 'feature-not-implemented'};
+errors.node_does_not_exist = {n: 2, type: 'cancel', error: 'item-not-found'};
+errors.bad_request = {n: 3, type: 'modify', error: 'bad-request'};
+errors.nodeid_required = {n: 4, type: 'modify', error: 'bad-request', reason: 'nodeid-required'};
+errors.itemid_required = {n: 5, type: 'modify', error: 'bad-request', reason: 'item-required'},
+errors.forbidden = {n: 6, type: 'auth', error: 'forbidden'};
+errors.subscriptions_retrieval_not_supported = {n: 23, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'retrieve-subscriptions'};
+errors.affiliations_retrieval_not_supported = {n: 29, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'retrieve-affiliations'};
+
+errors.sub = {
+	subscribe: {
+		jids_do_not_match: {n: 34, type: 'modify', error: 'bad-request', reason: 'invalid-jid'},
+		presence_subscription_required: {n: 35, type: 'auth', error: 'not-authorized', reason: 'presence-subscription-required'},
+		not_in_roster_group: {n: 36, type: 'auth', error: 'not-authorized', reason: 'not-in-roster-group'},
+		not_on_whitelist: {n: 37, type: 'cancel', error: 'not-allowed', reason: 'closed-node'},
+		payment_required: {n: 38, type: 'auth', error: 'payment-required'},
+		anonymous_subscriptions_not_allowed: {n: 39, type: 'cancel', error: 'forbidden'},
+		subscription_pending: {n: 40, type: 'auth', error: 'not-authorized', reason: 'pending-subscription'},
+		blocked: {n: 41, type: 'auth', error: 'forbidden'},
+		too_many_subscriptions: {n: 42, type: 'wait', error: 'policy-violation', reason: 'too-many-subscriptions'},
+		not_supported: {n: 43, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'subscribe'},
+		node_has_moved: {n: 44, type: 'modify', error: 'gone', text: true},
+		node_does_not_exist: {n: 45, type: 'cancel', error: 'item-not-found'},
+		configuration_required: {n: 48, type: 'modify', error: 'not-acceptable', reason: 'configuration-required'},
+	},
+	unsubscribe: {
+		no_subscription_id: {n: 53, type: 'modify', error: 'bad-request', reason: 'subid-required'},
+		no_such_subscriber: {n: 54, type: 'cancel', error: 'unexpected-request', reason: 'not-subscribed'},
+		insufficient_privileges: {n: 55, type: 'auth', error: 'forbidden'},
+		node_does_not_exist: {n: 56, type: 'cancel', error: 'item-not-found'},
+		bad_subscription_id: {n: 57, type: 'modify', error: 'not-acceptable', reason: 'invalid-subid'},
+	},
+	configure: {
+		insufficient_privileges: {n: 61, type: 'auth', error: 'forbidden'},
+		no_such_subscriber: {n: 62, type: 'modify', error: 'unexpected-request', reason: 'not-subscribed'},
+		subscriber_jid_required: {n: 63, type: 'modify', error: 'bad-request', reason: 'jid-required'},
+		subscription_id_required: {n: 64, type: 'modify', error: 'bad-request', reason: 'subid-required'},
+		invalid_subscription_id: {n: 65, type: 'modify', error: 'not-acceptable', reason: 'invalid-subid'},
+		subscription_options_not_supported: {n: 66, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'subscription-options'},
+		node_does_not_exist: {n: 67, type: 'cancel', error: 'item-not-found'},
+		form_processing_failure: {n: 70, type: 'modify', error: 'bad-request', reason: 'invalid-options'},
+	},
+	default_options: {
+		node_configuration_not_supported: {n: 76, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'subscription-options'},
+		default_subscription_configuration_retrieval_not_supported: {n: 77, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'retrieve-default-sub'},
+	},
+	retrieve: {
+		subscription_id_required: {n: 87, type: 'modify', error: 'bad-request', reason: 'subid-required'},
+		invalid_subscription_id: {n: 88, type: 'modify', error: 'not-acceptable', reason: 'invalid-subid xmlns'},
+		entity_not_subscribed: {n: 89, type: 'auth', error: 'not-authorized', reason: 'not-subscribed'},
+		persistent_items_not_supported: {n: 90, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'persistent-items'},
+		item_retrieval_not_supported: {n: 91, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'retrieve-items'},
+		presence_subscription_required: {n: 92, type: 'auth', error: 'not-authorized', reason: 'presence-subscription-required'},
+		not_in_roster_group: {n: 93, type: 'auth', error: 'not-authorized', reason: 'not-in-roster-group'},
+		not_on_whitelist: {n: 94, type: 'cancel', error: 'not-allowed', reason: 'closed-node'},
+		payment_required: {n: 95, type: 'auth', error: 'payment-required'},
+		blocked: {n: 96, type: 'auth', error: 'forbidden'},
+		node_does_not_exist: {n: 97, type: 'cancel', error: 'item-not-found'},
+	},
+};
+
+errors.pub = {
+	publish: {
+		insufficient_privileges: {n: 105, type: 'auth', error: 'forbidden'},
+		item_publication_not_supported: {n: 106, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'publish'},
+		node_does_not_exist: {n: 107, type: 'cancel', error: 'item-not-found'},
+		payload_too_big: {n: 108, type: 'modify', error: 'not-acceptable', reason: 'payload-too-big'},
+		bad_payload: {n: 109, type: 'modify', error: 'bad-request', reason: 'invalid-payload'},
+	},
+	configuration: {
+		no_item: {n: 110, type: 'modify', error: 'bad-request', reason: 'item-required'},
+		no_payload: {n: 111, type: 'modify', error: 'bad-request', reason: 'payload-required'},
+		transient_item: {n: 112, type: 'modify', error: 'bad-request', reason: 'item-forbidden'},
+		precondition: {n: 113, type: 'modify', error: 'conflict', reason: 'precondition-not-met'},
+
+	},
+	retract: {
+		insufficient_privileges: {n: 118, type: 'auth', error: 'forbidden'},
+		node_does_not_exist: {n: 119, type: 'cancel', error: 'item-not-found'},
+		nodeid_required: {n: 120, type: 'modify', error: 'bad-request', reason: 'nodeid-required'},
+		item_or_itemid_required: {n: 121, type: 'modify', error: 'bad-request', reason: 'item-required'},
+		persistent_items_not_supported: {n: 122, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'persistent-items'},
+		item_deletion_not_supported: {n: 123, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'delete-items'},
+	},
+};
+
+errors.owner = {
+	create: {
+		node_creation_not_supported: {n: 125, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'create-nodes'},
+		service_requires_registration: {n: 126, type: 'auth', error: 'registration-required'},
+		prohibited: {n: 127, type: 'auth', error: 'forbidden'},
+		nodeid_already_exists: {n: 128, type: 'cancel', error: 'conflict'},
+		instant_nodes_not_supported: {n: 129, type: 'modify', error: 'not-acceptable', reason: 'nodeid-required'},
+		specified_access_model_not_supported: {n: 135, type: 'modify', error: 'not-acceptable', reason: 'unsupported-access-model'},
+	},
+	configure: {
+		node_configuration_not_supported: {n: 140, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'config-node'},
+		insufficient_privileges: {n: 141, type: 'auth', error: 'forbidden'},
+		nodeid_required: {n: 142, type: 'modify', error: 'bad-request', reason: 'nodeid-required'},
+		no_configuration_options: {n: 143, type: 'cancel', error: 'not-allowed'},
+		node_does_not_exist: {n: 144, type: 'cancel', error: 'item-not-found'},
+		form_processing_failure: {n: 148, type: 'modify', error: 'not-acceptable'},
+	},
+	default_options: {
+		node_configuration_not_supported: {n: 153, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'config-node'},
+		default_node_configuration_retrieval_not_supported: {n: 154, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'retrieve-default'},
+	},
+	delete_node: {
+		insufficient_privileges: {n: 159, type: 'auth', error: 'forbidden'},
+		node_does_not_exist: {n: 160, type: 'cancel', error: 'item-not-found'},
+	},
+	purge: {
+		node_purging_not_supported: {n: 164, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'purge-nodes'},
+		insufficient_privileges: {n: 165, type: 'auth', error: 'forbidden'},
+		node_does_not_persist_items: {n: 166, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'persistent-items'},
+		node_does_not_exist: {n: 167, type: 'cancel', error: 'item-not-found'},
+	},
+	//manage_subscription_requests: {},
+	pending_subscription_requests: {
+		ad_hoc_commands_not_supported: {n: 176, type: 'cancel', error: 'service-unavailable'},
+		get_pending_not_supported: {n: 177, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'get-pending'},
+		insufficient_privileges: {n: 178, type: 'auth', error: 'forbidden'},
+		node_does_not_exist: {n: 179, type: 'cancel', error: 'item-not-found'},
+	},
+	manage_subscriptions: {
+		not_supported: {n: 184, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'manage-subscriptions'},
+		retrieve_list: {
+			entity_is_not_an_owner: {n: 185, type: 'auth', error: 'forbidden'},
+			node_does_not_exist: {n: 186, type: 'cancel', error: 'item-not-found'},
+		},
+		modify: {
+			entity_is_not_an_owner: {n: 190, type: 'auth', error: 'forbidden'},
+			node_does_not_exist: {n: 191, type: 'cancel', error: 'item-not-found'},
+			multiple_simultaneous_modifications: {n: 193, type: 'modify', error: 'not-acceptable'},
+		},
+	},
+	manage_affiliations: {
+		not_supported: {n: 197, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'modify-affiliations'},
+		retrieve_list: {
+			entity_is_not_an_owner: {n: 198, type: 'auth', error: 'forbidden'},
+			node_does_not_exist: {n: 199, type: 'cancel', error: 'item-not-found'},
+		},
+		modify: {
+			requested_affiliation_not_supported: {
+				publisher: {n: 203, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'publisher-affiliation'},
+				'publish-only': {n: 203, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'publish-only-affiliation'},
+				member: {n: 203, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'member-affiliation'},
+				outcast: {n: 203, type: 'cancel', error: 'feature-not-implemented', reason: 'unsupported', feature: 'outcast-affiliation'},
+			},
+			entity_is_not_an_owner: {n: 204, type: 'auth', error: 'forbidden'},
+			node_does_not_exist: {n: 205, type: 'cancel', error: 'item-not-found'},
+			multiple_simultaneous_modifications: {n: 207, type: 'modify', error: 'not-acceptable'},
+		},
+	},
+};
+
+errors.reverse = {};
+
+function reverseError(o) {
+	for (var i in o) {
+		if (i == 'reverse' || typeof (o[i]) == 'number');
+		else if (!o[i].n)
+			reverseError(o[i]);
+		else {
+			var k = {};
+			//var k = {name: i};
+			for (var j in o[i]) {
+				if (j != 'n')
+					k[j] = o[i][j];
+			}
+			errors.reverse[o[i].n] = k;
+		}
+	}
+}
+
+reverseError(errors);
new file mode 100644
--- /dev/null
+++ b/forms.js
@@ -0,0 +1,184 @@
+/*
+ *  Copyright (C) 2010  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ *
+ *  This file is part of PSĜS, a PubSub server written in JavaScript.
+ *
+ *  PSĜS is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License.
+ *
+ *  PSĜS is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with PSĜS.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var xmpp = require('xmpp')
+var service_configuration = require('./configuration').service_configuration;
+
+var parseBoolean = function(b) {
+	if (b == 'true' || b == 'True' || b == 'TRUE' || b == '1')
+		return true;
+	return false;
+}
+
+exports.build = function(type, desc, content, labels, title, instructions) {
+	var x = xmpp.stanza('x', {xmlns: 'jabber:x:data', type: type});
+
+	if (typeof desc == 'string')
+		desc = service_configuration[desc];
+
+	if (desc._TITLE)
+		x.s('title').t(desc._TITLE);
+	else if (title)
+		x.s('title').t(title);
+
+	if (desc._INSTRUCTIONS)
+		x.s('instructions').t(desc._INSTRUCTIONS);
+	else if (instructions)
+		x.s('instructions').t(instructions);
+
+	if (content == 'default') {
+		content = {};
+		for (var i in desc)
+			content[i] = desc[i].value;
+	}
+
+	for (var i in desc) {
+		if (i[0] == '_')
+			continue;
+
+		var fieldAttr = {'var': i};
+
+		if (labels) {
+			if (desc[i].type)
+				fieldAttr.type = desc[i].type;
+			if (desc[i].label)
+				fieldAttr.label = desc[i].label;
+		}
+		var field = xmpp.stanza('field', fieldAttr);
+
+		if (labels &&
+		    (desc[i].type == 'list-multi' ||
+		     desc[i].type == 'list-single')) {
+			for (var j in desc[i].options) {
+				var optAttr = {};
+				if (desc[i].options[j].label)
+					optAttr.label = desc[i].options[j].label;
+				field.s('option', optAttr).c('value').t(j);
+			}
+		}
+
+		if (i == 'FORM_TYPE')
+			field.s('value').t(desc[i].value);
+		else if (typeof content[i] != 'undefined') {
+			var md = content[i];
+			if (desc[i].type == 'jid-multi' ||
+			    desc[i].type == 'list-multi' ||
+			    desc[i].type == 'text-multi') {
+				for (var j=0; j<md.length; j++)
+					field.s('value')
+						.t(md[j].toString());
+			} else
+				field.s('value').t(md.toString());
+		}
+
+		x.cx(field);
+	}
+	if (field)
+		return x;
+	return -1; //FIXME
+}
+
+exports.parse = function(x, params) {
+	var form = {};
+
+	if (!params) {
+		var type = x.getAttribute('type');
+		if (type)
+			form.type = type;
+
+		var title = x.getChild('title');
+		if (title)
+			form.title = title;
+
+		var instructions = x.getChild('instructions');
+		if (instructions)
+			form.instructions = instructions;
+	}
+
+	form.fields = {};
+	for (var i in x.tags) {
+		var field = x.tags[i];
+		var name = field.getAttribute('var');
+		if (params && name == 'FORM_TYPE')
+			continue;
+
+		if (params) {
+			var type = field.getAttribute('type');
+			if (type == 'jid-multi' || type == 'list-multi' || type == 'text-multi') {
+				form.fields[name] = [];
+				for (var j in field.tags) {
+					var elem = field.tags[j];
+					if (elem.name == 'value')
+						form.fields[name].push(elem.getText());
+				}
+			} else if (type == 'boolean') {
+				var value = field.getChild('value');
+				if (value)
+					form.fields[name] = parseBoolean(value.getText());
+			} else {
+				var value = field.getChild('value');
+				if (value)
+					form.fields[name] = value.getText();
+			}
+		} else {
+			form.fields[name] = {};
+
+			var label = field.getAttribute('label');
+			if (label)
+				form.fields[name].label = label;
+
+			var type = field.getAttribute('type');
+			if (type)
+				form.fields[name].type = type;
+
+			if (type == 'jid-multi' || type == 'list-multi' || type == 'text-multi') {
+				form.fields[name].options = {};
+				form.fields[name].values = [];
+				for (var j in field.tags) {
+					var elem = field.tags[j];
+					if (elem.name == 'option') {
+						var value = elem.getChild('value');
+						if (!value)
+							continue;
+
+						value = value.getText();
+						if (!value)
+							continue;
+
+						form.fields[name].options[value] = {};
+
+						label = elem.getAttribute('label')
+						if (label)
+							form.fields[name].options[value].label = label;
+					} else if (elem.name == 'value')
+						form.fields[name].values.push(elem.getText());
+				}
+			} else if (type == 'boolean') {
+				var value = field.getChild('value');
+				if (value)
+					form.fields[name].value = parseBoolean(value.getText());
+			} else {
+				var value = field.getChild('value');
+				if (value)
+					form.fields[name].value = value.getText();
+			}
+		}
+	}
+	return form;
+}
new file mode 100644
--- /dev/null
+++ b/iso8601.js
@@ -0,0 +1,82 @@
+/*
+ * Thanks to Paul Sowden for this script.
+ * http://delete.me.uk/2005/03/iso8601.html
+ */
+
+Date.prototype.setISO8601 = function(dString){
+	var d;
+	var regexp = /(\d\d\d\d)(-)?(\d\d)(-)?(\d\d)(T)?(\d\d)(:)?(\d\d)(:)?(\d\d)(\.\d+)?(Z|([+-])(\d\d)(:)?(\d\d))/;
+	if ((d = dString.match(regexp))) {
+		var offset = 0;
+		this.setUTCDate(1);
+		this.setUTCFullYear(parseInt(d[1],10));
+		this.setUTCMonth(parseInt(d[3],10) - 1);
+		this.setUTCDate(parseInt(d[5],10));
+		this.setUTCHours(parseInt(d[7],10));
+		this.setUTCMinutes(parseInt(d[9],10));
+		this.setUTCSeconds(parseInt(d[11],10));
+		if (d[12])
+			this.setUTCMilliseconds(parseFloat(d[12]) * 1000);
+		else
+			this.setUTCMilliseconds(0);
+		if (d[13] != 'Z') {
+			offset = (d[15] * 60) + parseInt(d[17],10);
+			offset *= ((d[14] == '-') ? -1 : 1);
+			this.setTime(this.getTime() - offset * 60 * 1000);
+		}
+	} else
+		this.setTime(Date.parse(dString));
+	return this;
+};
+
+Date.prototype.toString = function (format, offset) {
+	/* accepted values for the format [1-6]:
+	 1 Year:
+	   YYYY (eg 1997)
+	 2 Year and month:
+	   YYYY-MM (eg 1997-07)
+	 3 Complete date:
+	   YYYY-MM-DD (eg 1997-07-16)
+	 4 Complete date plus hours and minutes:
+	   YYYY-MM-DDThh:mmTZD (eg 1997-07-16T19:20+01:00)
+	 5 Complete date plus hours, minutes and seconds:
+	   YYYY-MM-DDThh:mm:ssTZD (eg 1997-07-16T19:20:30+01:00)
+	 6 Complete date plus hours, minutes, seconds and a decimal
+	   fraction of a second
+	   YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
+	 */
+	if (!format)
+		var format = 5;
+	if (!offset) {
+		var offset = 'Z';
+		var date = this;
+	} else {
+		var d = offset.match(/([-+])([0-9]{2}):([0-9]{2})/);
+		var offsetnum = (Number(d[2]) * 60) + Number(d[3]);
+		offsetnum *= ((d[1] == '-') ? -1 : 1);
+		var date = new Date(Number(Number(this) + (offsetnum * 60000)));
+	}
+
+	var zeropad = function (num) { return ((num < 10) ? '0' : '') + num; };
+
+	var str = "";
+	str += date.getUTCFullYear();
+	if (format > 1)
+		str += "-" + zeropad(date.getUTCMonth() + 1);
+	if (format > 2)
+		str += "-" + zeropad(date.getUTCDate());
+	if (format > 3)
+		str += "T" + zeropad(date.getUTCHours()) +
+			":" + zeropad(date.getUTCMinutes());
+	if (format > 5) {
+		var secs = Number(date.getUTCSeconds() + "." +
+				((date.getUTCMilliseconds() < 100) ? '0' : '') +
+				zeropad(date.getUTCMilliseconds()));
+		str += ":" + zeropad(secs);
+	} else if (format > 4)
+		str += ":" + zeropad(date.getUTCSeconds());
+
+	if (format > 3)
+		str += offset;
+	return str;
+};
new file mode 100644
--- /dev/null
+++ b/nodes.js
@@ -0,0 +1,93 @@
+/*
+ *  Copyright (C) 2010  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ *
+ *  This file is part of PSĜS, a PubSub server written in JavaScript.
+ *
+ *  PSĜS is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License.
+ *
+ *  PSĜS is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with PSĜS.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+require('./iso8601');
+var errors = require('./errors');
+var config = require('./configuration');
+var service_configuration = config.service_configuration;
+var Configuration = config.Configuration;
+var makeRandomId = require('./util').makeRandomId;
+
+exports.Item = function() {
+	this.content = null;
+	this.date = new Date();
+	return this;
+};
+
+exports.Node = function(params) {
+	this.items = {};
+	if (config.enabled('subscribe'))
+		this.subscribers = {};
+	this.owner = ['lm@slam'];
+	if (config.enabled('publisher-affiliation'))
+		this.publisher = [];
+	if (config.enabled('publish-only-affiliation'))
+		this.publishOnly = [];
+	if (config.enabled('member-affiliation'))
+		this.member = [];
+	if (config.enabled('outcast-affiliation'))
+		this.outcast = [];
+	if (config.enabled('meta-data'))
+		this.metadata = new Configuration(service_configuration.node_metadata, params);
+	if (config.enabled('config-node'))
+		this.configuration = new Configuration(service_configuration.node_config, params);
+	if (config.enabled('subscription-options'))
+		this.subsConfig = new Configuration(service_configuration.subscribe_options, params);
+	return this;
+};
+
+exports.Node.prototype = {
+	setItem: function(name, content) {
+		if (typeof content == 'undefined') {
+			if (this.items[name]) {
+				delete this.items[name];
+				return errors.success;
+			}
+			return 42; //XXX
+		}
+
+		if (!this.items[name])
+			this.items[name] = new exports.Item();
+
+		this.items[name].content = content;
+
+		return errors.success;
+	},
+
+	setSubscriber: function(jid, type, subid, params) {
+		if (type == 'none') {
+			delete this.subscribers[jid];
+			this.metadata['pubsub#num_subscribers']--;
+			return errors.success;
+		}
+
+		if (!subid)
+			subid = makeRandomId();
+
+		this.subscribers[jid] = {
+			type: type,
+			subid: subid,
+			options: new Configuration(service_configuration.subscribe_options, params),
+		}
+
+		this.metadata['pubsub#num_subscribers']++;
+
+		return subid;
+	},
+};
new file mode 100755
--- /dev/null
+++ b/psgxs.js
@@ -0,0 +1,1074 @@
+#!/usr/bin/env node
+
+/*
+ *  Copyright (C) 2010  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ *
+ *  This file is part of PSĜS, a PubSub server written in JavaScript.
+ *
+ *  PSĜS is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License.
+ *
+ *  PSĜS is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with PSĜS.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var sys = require('sys');
+var xmpp = require('xmpp');
+var sha1 = require('sha1');
+require('./iso8601');
+var storage = require('./storage');
+var errors = require('./errors');
+var utils = require('./util');
+var toBareJID = utils.toBareJID;
+var config = require('./configuration');
+var forms = require('./forms');
+var conn = new xmpp.Connection();
+
+var service_configuration = config.service_configuration;
+var componentJID = config.jid;
+var componentPassword = config.password;
+
+conn.log = function (_, m) { sys.puts(m); };
+
+function _(m, c) {
+	if (c)
+		sys.print('\033[1;'+c+'m');
+	sys.print(sys.inspect(m, false, null));
+	if (c)
+		sys.print('\033[0m');
+	sys.puts('');
+};
+
+function onIq(stanza) {
+	var type = stanza.getAttribute('type');
+	var from = stanza.getAttribute('to');
+	var to = stanza.getAttribute('from');
+	var id = stanza.getAttribute('id');
+
+	var response;
+	if (id)
+		response = xmpp.iq({to: to, from: from, type: 'result', id: id});
+	else
+		response = xmpp.iq({to: to, from: from, type: 'result'});
+
+	if (type == 'get') {
+		if (stanza.getChild('query', 'jabber:iq:version')) {
+			var os = 'GNU/Linux';
+			var query = xmpp.stanza('query', {xmlns: 'jabber:iq:version'})
+				.s('name').t('PubSubJS')
+				.s('version').t('Pas releasé')
+				.s('os').t(os);
+			response.cx(query);
+
+		// SECTION 5.1
+		} else if (stanza.getChild('query', 'http://jabber.org/protocol/disco#info')) {
+			var query = stanza.getChild('query', 'http://jabber.org/protocol/disco#info');
+			var nodeID = query.getAttribute('node');
+
+			// SECTION 5.3
+			if (nodeID && nodeID != '') {
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var conf = storage.getConfiguration(nodeID);
+				if (typeof conf == 'number')
+					return makeError(response, conf);
+
+				var type = 'leaf'
+				if (conf['pubsub#node_type'])
+					type = conf['pubsub#node_type'];
+
+				var q = xmpp.stanza('query', {xmlns: 'http://jabber.org/protocol/disco#info'})
+				q.s('identity', {category: 'pubsub', type: type});
+				q.s('feature', {'var': 'http://jabber.org/protocol/pubsub'});
+
+				// SECTION 5.4
+				if (config.enabled('meta-data')) {
+					var x = forms.build('result', 'meta-data', storage.getMetadata(nodeID), true);
+					if (x)
+						q.cx(x);
+				}
+				response.cx(q);
+
+			// SECTION 5.1
+			} else {
+				var q = xmpp.stanza('query', {xmlns: 'http://jabber.org/protocol/disco#info'})
+					.s('identity', {category: 'pubsub', type: 'service', name: 'PubSub JavaScript Server'})
+					.s('feature', {'var': 'http://jabber.org/protocol/disco#info'})
+					.s('feature', {'var': 'http://jabber.org/protocol/disco#items'})
+					.s('feature', {'var': 'http://jabber.org/protocol/pubsub'})
+//					.s('feature', {'var': 'http://jabber.org/protocol/commands'})
+					for (var i in config.activated)
+						if (typeof i == 'string')
+							q.s('feature', {'var': 'http://jabber.org/protocol/pubsub#' + config.activated[i]});
+				response.cx(q);
+			}
+
+		// SECTION 5.2
+		} else if (stanza.getChild('query', 'http://jabber.org/protocol/disco#items')) {
+			var query = stanza.getChild('query', 'http://jabber.org/protocol/disco#items');
+			var q;
+			var children;
+			var nodeID = query.getAttribute('node');
+			if (nodeID && nodeID != '') {
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				q = xmpp.stanza('query', {xmlns: 'http://jabber.org/protocol/disco#items', node: nodeID});
+
+				children = storage.getChildren(nodeID);
+				if (typeof children == 'number')
+					return makeError(response, children);
+			} else {
+				q = xmpp.stanza('query', {xmlns: 'http://jabber.org/protocol/disco#items'});
+
+				children = storage.getChildren();
+				if (typeof children == 'number')
+					return makeError(response, children);
+			}
+
+			for (var i in children) {
+				var attr = {jid: componentJID};
+				if (children[i] == 'node') {
+					if (config.enabled('meta-data')) {
+						var metadata = storage.getMetadata(i);
+						if (metadata['pubsub#title'])
+							attr.name = metadata['pubsub#title'];
+					}
+					attr.node = i;
+
+				// SECTION 5.5
+				} else
+					attr.name = i;
+
+				q.s('item', attr);
+			}
+			response.cx(q);
+		} else if (stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub')) {
+			var pubsub = stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub');
+
+			// SECTION 5.6
+			if (pubsub.getChild('subscriptions')) {
+				if (!config.enabled('retrieve-subscriptions'))
+					return makeError(response, errors.subscriptions_retrieval_not_supported.n);
+
+				var subscriptions = pubsub.getChild('subscriptions');
+				var subs;
+
+				var nodeID = subscriptions.getAttribute('node');
+				if (nodeID && nodeID != '') {
+					if (!storage.existsNode(nodeID))
+						return makeError(response, errors.node_does_not_exist.n);
+					subs = storage.getSubscription(toBareJID(to), node);
+				} else
+					subs = storage.getSubscription(toBareJID(to));
+
+				var s = xmpp.stanza('subscriptions');
+				for (i in subs)
+					s.s('subscription', {node: i, jid: to, subscription: subs[i].type, subid: subs[i].subid});
+
+				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
+				p.cx(s);
+				response.cx(p);
+
+			// SECTION 5.7
+			} else if (pubsub.getChild('affiliations')) {
+				if (!config.enabled('retrieve-affiliations'))
+					return makeError(response, errors.affiliations_retrieval_not_supported.n);
+
+				var affiliations = pubsub.getChild('affiliations');
+				var nodeID = affiliations.getAttribute('node');
+				var affils;
+				if (nodeID && nodeID != '') {
+					if (!storage.existsNode(nodeID))
+						return makeError(response, errors.node_does_not_exist.n);
+					affils = storage.getAffiliation(toBareJID(to), nodeID);
+				} else
+					affils = storage.getAffiliationsFromJID(toBareJID(to));
+				var s = xmpp.stanza('affiliations');
+				for (i in affils)
+					s.s('affiliation', {node: i, affiliation: affils[i]});
+				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
+				p.cx(s);
+				response.cx(p);
+
+			// SECTION 6.3.2
+			} else if (pubsub.getChild('options')) {
+				if (!config.enabled('subscription-options'))
+					return makeError(response, errors.sub.configure.subscription_options_not_supported.n);
+
+				var options = pubsub.getChild('options');
+
+				var nodeID = options.getAttribute('node');
+				if (!nodeID || nodeID == '')
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var jid = options.getAttribute('jid');
+				if (!jid)
+					return makeError(response, errors.sub.configure.subscriber_jid_required.n);
+				if (toBareJID(jid) != toBareJID(to))
+					return makeError(response, errors.sub.configure.insufficient_privileges.n);
+
+				var subs = storage.getSubscription(jid, nodeID);
+				if (subs == {})
+					return makeError(response, errors.sub.configure.no_such_subscriber.n);
+
+				var s = xmpp.stanza('options', {node: nodeID, jid: jid});
+				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
+				var form = forms.build('form', 'subscribe_options', subs.options, true);
+				s.cx(form);
+				p.cx(s);
+				response.cx(p);
+
+			// SECTION 6.4
+			} else if (pubsub.getChild('default')) {
+				if (!config.enabled('retrieve-default-sub'))
+					return makeError(response, errors.sub.default_options.default_subscription_configuration_retrieval_not_supported.n);
+
+				var def = pubsub.getChild('default');
+
+				var nodeID = def.getAttribute('node');
+				if (nodeID && !storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
+				var s;
+				if (nodeID)
+					s = xmpp.stanza('default', {node: nodeID});
+				else
+					s = xmpp.stanza('default');
+
+				var form = forms.build('form', 'subscribe_options', 'default', false);
+				s.cx(form);
+				p.cx(s);
+				response.cx(p);
+
+			// SECTION 6.5
+			} else if (pubsub.getChild('items')) {
+				if (!config.enabled('retrieve-items'))
+					return makeError(response, errors.sub.default_options.node_configuration_not_supported.n);
+
+				var items = pubsub.getChild('items');
+
+				var nodeID = items.getAttribute('node');
+				if (!nodeID || nodeID == '')
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var affil = storage.getAffiliation(toBareJID(to), nodeID);
+				if (affil != 'owner' && affil != 'publisher' && affil != 'member')
+					return makeError(response, errors.pub.publish.insufficient_privileges.n);
+
+				var item = [];
+				for (var i=0; i<items.children.length; i++) {
+					var j = items.children[i];
+					if (j.name == 'item' && j.attr['id'] && j.attr['id'] != '')
+						item.push(j.attr['id']);
+				}
+
+				var max_items = items.getAttribute('max_items');
+				if (max_items)
+					max_items = Number (max_items);
+
+				if (item.length) {
+					var s = xmpp.stanza('items', {node: nodeID});
+
+					for (var i=0; i<item.length; i++) {
+						var j = storage.getItem(nodeID, item[i]);
+						if (typeof j == 'number')
+							return makeError(response, j);
+
+						var k = xmpp.stanza('item', {id: item[i]})
+						k.cx(j);
+						s.cx(k);
+					}
+				} else {
+					var s = xmpp.stanza('items', {node: nodeID});
+
+					var j;
+					if (max_items)
+						j = storage.getLastItem(nodeID, max_items);
+					else
+						j = storage.getItems(nodeID);
+					if (typeof j == 'number')
+						return makeError(response, j);
+
+					var k = 0;
+					for (var i in j) {
+						var contentItem = xmpp.stanza('item', {id: i}).t(j[i].content);
+						s.cx(contentItem);
+					}
+				}
+				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'});
+				p.cx(s);
+				response.cx(p);
+			} else
+				return makeError(response, errors.feature_not_implemented.n);
+		} else if (stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub#owner')) {
+			var pubsub = stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub#owner');
+
+			// SECTION
+			if (pubsub.getChild('configure')) {
+				if (!config.enabled('config-node'))
+					return makeError(response, errors.owner.configure.node_configuration_not_supported.n);
+
+				var nodeID = pubsub.getChild('configure').getAttribute('node');
+				if (!nodeID || nodeID == '')
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var affil = storage.getAffiliation(toBareJID(to), nodeID);
+				if (affil != 'owner' && affil != 'publish-only')
+					return makeError(response, errors.pub.publish.insufficient_privileges.n);
+
+				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub#owner'});
+				var s = xmpp.stanza('configure', {node: nodeID});
+				var form = forms.build('form', 'node_config', 'default', true);
+				s.cx(form);
+				p.cx(s);
+				response.cx(p);
+
+			// SECTION
+			} else if (pubsub.getChild('default')) {
+				if (!config.enabled('config-node'))
+					return makeError(response, errors.owner.default_options.node_configuration_not_supported.n);
+
+				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub#owner'});
+				var s = xmpp.stanza('default');
+				var form = forms.build('node_config', service_configuration.node_config, null, true);
+				s.cx(form);
+				p.cx(s);
+				response.cx(p);
+
+			// SECTION
+			} else if (pubsub.getChild('subscriptions')) {
+				if (!config.enabled('manage-subscriptions'))
+					return makeError(response, errors.owner.manage_subscriptions.not_supported.n);
+
+				var subscriptions = pubsub.getChild('subscriptions');
+
+				var nodeID = subscriptions.getAttribute('node');
+				if (!nodeID || nodeID == '')
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				if (storage.getAffiliation(toBareJID(to), nodeID) != 'owner')
+					return makeError(response, errors.forbidden.n);
+
+				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub#owner'});
+				var s = xmpp.stanza('subscriptions', {node: nodeID});
+
+				var subs = storage.getSubscriptionsFromNodeID(nodeID)
+				for (var jid in subs)
+					s.s('subscription', {jid: jid, subscription: subs[jid].type, subid: subs[jid].subid})
+
+				p.cx(s);
+				response.cx(p);
+
+			// SECTION
+			} else if (pubsub.getChild('affiliations')) {
+				if (!config.enabled('modify-affiliations'))
+					return makeError(response, errors.owner.manage_affiliations.not_supported.n);
+
+				var affiliations = pubsub.getChild('affiliations');
+
+				var nodeID = affiliations.getAttribute('node');
+				if (!nodeID || nodeID == '')
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var affil = storage.getAffiliationsFromNodeID(nodeID);
+				if (affil[toBareJID(to)] != 'owner')
+					return makeError(response, errors.owner.manage_affiliations.retrieve_list.entity_is_not_an_owner.n);
+
+				var p = xmpp.stanza('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub#owner'});
+				var s = xmpp.stanza('affiliations', {node: nodeID});
+
+				for (var jid in affil)
+					s.s('affiliation', {jid: jid, affiliation: affil[jid]})
+
+				p.cx(s);
+				response.cx(p);
+			} else
+				return makeError(response, errors.feature_not_implemented.n);
+		} else
+			return makeError(response, errors.feature_not_implemented.n);
+	} else if (type == 'set') {
+		if (stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub')) {
+			var pubsub = stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub');
+
+			// SECTION 6.1
+			if (pubsub.getChild('subscribe')) {
+				if (!config.enabled('subscribe'))
+					return makeError(response, errors.sub.subscribe.not_supported.n);
+
+				var subscribe = pubsub.getChild('subscribe');
+
+				var nodeID = subscribe.getAttribute('node');
+				if (!nodeID || nodeID == '')
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var configuration = storage.getConfiguration(nodeID);
+				if (!configuration['pubsub#subscribe'])
+					return makeError(response, errors.sub.subscribe.not_supported.n);
+
+				var affil = storage.getAffiliation(toBareJID(to), nodeID);
+				if (affil == 'publish-only' || affil == 'outcast')
+					return makeError(response, errors.pub.publish.insufficient_privileges.n);
+
+				var jid = subscribe.getAttribute('jid');
+				if (!jid || toBareJID(jid) != toBareJID(to))
+					return makeError(response, errors.sub.subscribe.jids_do_not_match.n);
+
+				// SECTION 6.3.7
+				var options = pubsub.getChild('options');
+				if (options && config.enabled('subscription-options')) {
+					if (options.getAttribute('node') || options.getAttribute('jid'))
+						return makeError(response, errors.bad_request.n);
+
+					var x = options.getChild('x', 'jabber:x:data');
+					if (!x || x.getAttribute('type') != 'submit')
+						return makeError(response, errors.bad_request.n);
+
+					var form = forms.parse(x, true);
+					if (typeof form == 'number')
+						return makeError(response, form);
+
+					var conf = form;
+				}
+
+				var subID;
+				if (configuration['pubsub#access_model'] == 'open') {
+					subID = storage.subscribe(nodeID, jid, 'subscribe', conf);
+					if (typeof subID == 'number')
+						return makeError(response, subID);
+				} else if (configuration['pubsub#access_model'] == 'authorize') {
+					subID = storage.subscribe(nodeID, jid, 'pending', conf);
+					if (typeof subID == 'number')
+						return makeError(response, subID);
+				} else if (configuration['pubsub#access_model'] == 'whitelist') {
+					var affil = storage.getAffiliation(jid, nodeID);
+					if (affil != 'owner' && affil != 'publisher' && affil != 'member')
+						return makeError(response, errors.sub.subscribe.not_on_whitelist.n);
+
+					subID = storage.subscribe(nodeID, jid, conf);
+					if (typeof subID == 'number')
+						return makeError(response, subID);
+				}
+
+				response.c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
+					.c('subscription', {node: nodeID, jid: jid, subid: subID.subid, subscription: subID.type});
+
+				if (conf)
+					response.cx(options);
+
+				if (config.enabled('get-pending')) {
+					var affiliates = storage.getAffiliationsFromNodeID(nodeID);
+					var form = forms.build('form', 'subscribe_authorization', {allow: false, node: nodeID, subscriber_jid: jid}, true); //168
+					for (var i in affiliates) {
+						if (affiliates[i] == 'owner') {
+							var message = xmpp.message({to: i}).cx(form);
+							conn.send(message);
+						}
+					}
+				}
+
+				if (config.enabled('last-published')) {
+					var last = storage.getLastItem(nodeID);
+					if (typeof last != 'number') {
+						var item = storage.getItem(nodeID, last);
+						if (typeof item != 'number') {
+							var attr = {};
+							attr[last] = {content: item};
+							sendNotifs(jid, 'items', nodeID, attr);
+						}
+					}
+				}
+
+			// SECTION 6.2
+			} else if (pubsub.getChild('unsubscribe')) {
+				if (!config.enabled('subscribe'))
+					return makeError(response, errors.sub.subscribe.not_supported.n);
+
+				var unsubscribe = pubsub.getChild('unsubscribe');
+				var nodeID = unsubscribe.getAttribute('node');
+				if (!nodeID || nodeID == '')
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var jid = unsubscribe.getAttribute('jid');
+				if (!jid || toBareJID(jid) != toBareJID(to))
+					return makeError(response, errors.sub.unsubscribe.insufficient_privileges.n);
+
+				var subID = storage.subscribe(nodeID, jid, 'none');
+				if (typeof subID == 'number')
+					return makeError(response, subID);
+
+			// SECTIONS 6.3.5
+			} else if (pubsub.getChild('options')) {
+				if (!config.enabled('subscription-options'))
+					return makeError(response, errors.sub.subscribe.not_supported.n);
+
+				var options = pubsub.getChild('options');
+
+				var nodeID = unsubscribe.getAttribute('node');
+				if (!nodeID || nodeID == '')
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var jid = unsubscribe.getAttribute('jid');
+				if (!jid || toBareJID(jid) != toBareJID(to))
+					return makeError(response, errors.sub.unsubscribe.insufficient_privileges.n);
+
+				var x = options.getChild('x', 'jabber:x:data');
+				if (!x || x.getAttribute(type) != 'submit')
+					return makeError(response, errors.bad_request);
+
+				var form = forms.parse(x, true);
+				if (typeof form == 'number')
+					return makeError(response, form);
+
+				var set = storage.configureSubscription(nodeID, jid, form);
+				if (typeof form == 'number')
+					return makeError(response, form);
+
+			// SECTION
+			} else if (pubsub.getChild('publish')) {
+				if (!config.enabled('publish'))
+					return makeError(response, errors.pub.publish.item_publication_not_supported.n);
+
+				var publish = pubsub.getChild('publish');
+				var nodeID = publish.getAttribute('node');
+				if (!nodeID || nodeID == '')
+					return makeError(response, errors.nodeid_required.n);
+
+				var affil = storage.getAffiliation(toBareJID(to), nodeID);
+				if (typeof affil == 'number')
+					return makeError(response, affil);
+				if (affil != 'owner' && affil != 'publisher' && affil != 'publish-only')
+					return makeError(response, errors.forbidden.n);
+
+				var item = publish.getChild('item');
+				var itemID = item.getAttribute('id');
+				if (!config.enabled('item-ids') && itemID)
+					return makeError(response, errors.itemid_required.n);
+				itemID = itemID? itemID: utils.makeRandomId();
+
+				if (item.tags.length != 1)
+					return makeError(response, errors.pub.publish.bad_payload.n);
+
+				var autocreate = false;
+				if (!storage.existsNode(nodeID)) {
+					if (config.enabled('auto-create'))
+						autocreate = true;
+					else
+						return makeError(response, errors.node_does_not_exist.n);
+				}
+
+				var conf = storage.getConfiguration(nodeID);
+				var publishOptions = pubsub.getChild('publish-options');
+				if (publishOptions && config.enabled('publish-options')) {
+					var x = publishOptions.getChild('x', 'jabber:x:data');
+					if (!x || x.getAttribute('type') != 'submit')
+						return makeError(response, errors.bad_request.n);
+
+					var form = forms.parse(x, true);
+					if (form.access_model != conf['pubsub#access_model'] && !autocreate)
+						return makeError(response, errors.pub.configuration.precondition.n);
+				}
+
+				if (!config.enabled('persistent-items')) {
+					var notifs = storage.purgeNode(nodeID);
+					if (typeof notifs == 'number')
+						return makeError(response, r);
+				}
+
+				if (autocreate)
+					storage.createNode(nodeID, form);
+
+				var content = item.getChild();
+				subscribers = storage.setItem(nodeID, itemID, content);
+
+				if (typeof subscribers == 'number')
+					return makeError(response, subscribers);
+
+				var attrs = {};
+				if (content)
+					attrs[itemID] = {content: content};
+				else
+					attrs[itemID] = {};
+				sendNotifs(subscribers, 'items', nodeID, attrs);
+
+				response.c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
+					.c('publish', {node: nodeID})
+						.c('item', {id: itemID});
+
+			// SECTION
+			} else if (pubsub.getChild('retract')) {
+				if (!config.enabled('retract-items'))
+					return makeError(response, errors.pub.retract.item_deletion_not_supported.n);
+
+				var retract = pubsub.getChild('retract');
+
+				var nodeID = retract.getAttribute('node');
+				if (!nodeID || nodeID == '')
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var item = retract.getChild('item');
+				if (!item)
+					return makeError(response, errors.pub.retract.item_or_itemid_required.n);
+
+				var itemID = item.getAttribute('id')
+				if (!itemID || itemID == '')
+					return makeError(response, errors.pub.retract.item_or_itemid_required.n);
+
+				var subscribers = storage.deleteItem(nodeID, itemID);
+				if (typeof subscribers == 'number')
+					return makeError(response, subscribers);
+
+				var attrs = {};
+				attrs[itemID] = {};
+				sendNotifs(subscribers, 'items', nodeID, attrs, 'retract')
+
+			// SECTION
+			} else if (pubsub.getChild('create')) {
+				if (!config.enabled('create-nodes'))
+					return makeError(response, errors.owner.create.node_creation_not_supported.n);
+
+				var instant = false;
+
+				var nodeID = pubsub.getChild('create').getAttribute('node');
+				if (!nodeID || nodeID == '') {
+					if (config.enabled('instant-nodes'))
+						return makeError(response, errors.owner.create.instant_nodes_not_supported.n);
+					nodeID = utils.makeRandomId();
+					instant = true;
+				}
+				if (storage.existsNode(nodeID))
+					return makeError(response, errors.nodeid_already_exists.n);
+
+				var affil = storage.getAffiliation(toBareJID(to), nodeID);
+				if (affil != 'owner' && affil != 'publish-only')
+					return makeError(response, errors.forbidden.n);
+
+				var configure = pubsub.getChild('configure');
+				if (configure && config.enabled('create-and-configure')) {
+					if (!config.enabled('config-node'))
+						return makeError(response, errors.owner.configure.node_configuration_not_supported.n);
+
+					if (configure.getAttribute('node'))
+						return makeError(response, errors.bad_request.n);
+
+					var x = configure.getChild('x', 'jabber:x:data');
+					if (!x || x.getAttribute('type') != 'submit')
+						return makeError(response, errors.bad_request.n);
+
+					var form = forms.parse(x, true);
+					if (typeof form == 'number')
+						return makeError(response, form);
+
+					var conf = form;
+				}
+
+				var r = storage.createNode(nodeID, conf);
+				if (typeof r == 'number')
+					return makeError(response, r);
+
+				if (instant)
+					response.c('pubsub', {xmlns: 'http://jabber.org/protocol/pubsub'})
+						.c('create', {node: nodeID});
+			} else
+				return makeError(response, errors.feature_not_implemented.n);
+		} else if (stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub#owner')) {
+			var pubsub = stanza.getChild('pubsub', 'http://jabber.org/protocol/pubsub#owner');
+
+			// SECTION
+			if (pubsub.getChild('configure')) {
+				if (!config.enabled('config-node'))
+					return makeError(response, errors.owner.configure.node_configuration_not_supported.n);
+
+				var nodeID = configure.getAttribute('node');
+				if (!nodeID)
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				var affil = storage.getAffiliation(toBareJID(to), nodeID);
+				if (affil != 'owner' && affil != 'publish-only')
+					return makeError(response, errors.forbidden.n);
+
+				var x = configure.getChild('x', 'jabber:x:data');
+				if (!x || x.getAttribute(type) != 'submit')
+					return makeError(response, errors.bad_request.n);
+
+				var form = forms.parse(x, true);
+				if (typeof form == 'number')
+					return makeError(response, form);
+
+				var conf = form;
+
+				var set = storage.configure(nodeID, conf);
+				if (typeof set == 'number')
+					return makeError(response, set);
+
+			// SECTION
+			} else if (pubsub.getChild('delete')) {
+				if (!config.enabled('delete-nodes'))
+					return makeError(response, errors.feature_not_implemented.n); //XXX
+
+				var del = pubsub.getChild('delete');
+
+				var nodeID = del.getAttribute('node');
+				if (!nodeID)
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				if (storage.getAffiliation(toBareJID(to), nodeID) != 'owner')
+					return makeError(response, errors.forbidden.n);
+
+				var notifs = storage.deleteNode(nodeID);
+				if (typeof notifs == 'number')
+					return makeError(response, r);
+
+				sendNotifs(notifs, 'delete', nodeID);
+
+			// SECTION
+			} else if (pubsub.getChild('purge')) {
+				if (!config.enabled('purge-nodes'))
+					return makeError(response, errors.owner.purge.node_purging_not_supported.n); //XXX
+
+				var purge = pubsub.getChild('purge');
+
+				var nodeID = purge.getAttribute('node');
+				if (!nodeID)
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				if (storage.getAffiliation(toBareJID(to), nodeID) != 'owner')
+					return makeError(response, errors.forbidden.n);
+
+				if (!config.enabled('persistent-items')) //FIXME: autre condition, supporté par le node
+					return makeError(response, errors.owner.purge.node_does_not_persist_items.n);
+
+				var notifs = storage.purgeNode(nodeID);
+				if (typeof notifs == 'number')
+					return makeError(response, r);
+
+				sendNotifs(notifs, 'purge', nodeID);
+
+			// SECTION
+			} else if (pubsub.getChild('subscriptions')) {
+				if (!config.enabled('manage-subscriptions'))
+					return makeError(response, errors.owner.manage_subscriptions.not_supported.n); //XXX
+
+				var subscriptions = pubsub.getChild('subscriptions');
+
+				var nodeID = subscriptions.getAttribute('node');
+				if (!nodeID)
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				if (storage.getAffiliation(toBareJID(to), nodeID) != 'owner')
+					return makeError(response, errors.forbidden.n);
+
+				var e = false;
+				for (i in subscriptions.tags) {
+					var jid = subscriptions.tags[i].getAttribute('jid');
+					var subscription = subscriptions.tags[i].getAttribute('subscription');
+
+					var set = storage.subscribe(nodeID, jid, subscription);
+					if (typeof set == 'number')
+						e = true;
+					else {
+						sendNotifs(jid, 'subscription', nodeID, {jid: jid, subscription: subscription});
+						subscriptions.tags.splice(i, 1);
+					}
+				}
+
+				if (e)
+					return makeError(response, errors.owner.manage_subscriptions.modify.multiple_simultaneous_modifications.n, pubsub);
+
+			// SECTION
+			} else if (pubsub.getChild('affiliations')) {
+				if (!config.enabled('modify-affiliations'))
+					return makeError(response, errors.owner.manage_affiliations.not_supported.n); //XXX
+
+				var affiliations = pubsub.getChild('affiliations');
+
+				var nodeID = affiliations.getAttribute('node');
+				if (!nodeID)
+					return makeError(response, errors.nodeid_required.n);
+				if (!storage.existsNode(nodeID))
+					return makeError(response, errors.node_does_not_exist.n);
+
+				if (storage.getAffiliation(toBareJID(to), nodeID) != 'owner')
+					return makeError(response, errors.forbidden.n);
+
+				var e = false;
+				for (i in affiliations.children) {
+					var jid = affiliations.children[i].getAttribute('jid');
+					var affiliation = affiliations.children[i].getAttribute('affiliation');
+
+					var set = storage.setAffiliation(nodeID, jid, affiliation);
+					if (typeof set == 'number')
+						e = true;
+					else
+						affiliations.children.splice(i, 1);
+				}
+
+				if (e)
+					return makeError(response, errors.owner.manage_affiliations.modify.multiple_simultaneous_modifications.n, pubsub);
+			} else
+				return makeError(response, errors.feature_not_implemented.n);
+		} else
+			return makeError(response, errors.feature_not_implemented.n);
+	} else
+		return makeError(response, errors.feature_not_implemented.n);
+	conn.send(response);
+}
+
+function onMessage(stanza) {
+	var from = stanza.getAttribute('to');
+	var to = stanza.getAttribute('from');
+	var id = stanza.getAttribute('id');
+
+	var response;
+	if (id)
+		response = xmpp.message({to: to, from: from, id: id});
+	else
+		response = xmpp.message({to: to, from: from});
+
+	var x = stanza.getChild('x', 'jabber:x:data');
+	if (x) {
+		var form = forms.parse(x);
+		if (form.type == 'submit' && form.fields.FORM_TYPE.value == 'subscribe_authorization') {
+			if (form.fields.subid && form.fields.subid.value)
+				var subID = form.fields.subid.value;
+			if (form.fields.node && form.fields.node.value)
+				var nodeID = form.fields.node.value;
+			if (form.fields.subscriber_jid && form.fields.subscriber_jid.value)
+				var jid = form.fields.subscriber_jid.value;
+			if (form.fields.allow && form.fields.allow.value)
+				var allow = form.fields.allow.value;
+
+			var type = allow? 'subscribed': 'none';
+			var set = storage.subscribe(nodeID, jid, type)
+			//if (set.subid != subID) //TODO: support the multi-subscribe feature
+			sendNotifs(jid, 'subscription', nodeID, {jid: jid, subscription: type});
+		} else
+			return makeError(response, errors.feature_not_implemented.n);
+	} else
+		return makeError(response, errors.feature_not_implemented.n);
+	conn.send(response)
+}
+
+function onPresence(stanza) {
+	var from = stanza.getAttribute('to');
+	var to = stanza.getAttribute('from');
+	var id = stanza.getAttribute('id');
+
+	var response;
+	if (id)
+		response = xmpp.presence({to: to, from: from, id: id});
+	else
+		response = xmpp.presence({to: to, from: from});
+
+	makeError(response, errors.feature_not_implemented.n);
+}
+
+function makeError(response, errorNumber, payload) {
+	response.attr.type = 'error';
+	if (payload)
+		response.cx(payload);
+
+	var e = errors.reverse[errorNumber];
+	var error = xmpp.stanza('error', {type: e.type});
+
+	error.s(e.error, {xmlns: 'urn:ietf:params:xml:ns:xmpp-stanzas'})
+
+	if (e.reason) {
+		if (e.feature)
+			error.s(e.reason, {xmlns: 'http://jabber.org/protocol/pubsub#errors', feature: e.feature});
+		else
+			error.s(e.reason, {xmlns: 'http://jabber.org/protocol/pubsub#errors'});
+	}
+
+	response.cx(error);
+	conn.send(response);
+}
+
+function sendNotifs(notifs, type, nodeID, a1, a2) {
+	var ev = xmpp.stanza('event', {xmlns: 'http://jabber.org/protocol/pubsub#event'});
+
+	if (type == 'collection') {
+		var collection = xmpp.stanza('collection', {node: nodeID});
+		if (a1 == 'associate')
+			collection.cx('associate', {node: nodeID});
+		else
+			collection.cx('disassociate', {node: nodeID});
+		ev.cx(collection);
+	} else if (type == 'configuration') {
+		if (!config.enabled('config-node')) {
+			_('Error #4', 41)
+			return;
+		}
+
+		var configuration = xmpp.stanza('configuration', {node: nodeID});
+		if (a1) {
+			var x = forms.build('node_config', service_configuration.node_config, storage.getConfiguration(nodeID));
+			if (x)
+				configuration.cx(x); //TODO: voir exemple 150
+		}
+		ev.cx(configuration);
+	} else if (type == 'delete') {
+		var del = xmpp.stanza('delete', {node: nodeID});
+		if (a1)
+			del.c('redirect', {uri: a1});
+		ev.cx(del);
+	} else if (type == 'items') {
+		var items = xmpp.stanza(type, {node: nodeID});
+		if (a2 == 'retract')
+			for (var i in a1)
+				items.s('retract', {id: i});
+		else {
+			for (var i in a1) {
+				var item = a1[i];
+				var args = {};
+				if (i != '')
+					args.id = i;
+				if (item.node)
+					args.node = item.node;
+				if (item.publisher)
+					args.publisher = item.publisher;
+				var it = xmpp.stanza('item', args);
+				if (item.content)
+					it.cx(item.content);
+				items.cx(it);
+			}
+		}
+		ev.cx(items);
+	} else if (type == 'purge') {
+		ev.c('purge', {node: nodeID});
+	} else if (type == 'subscription') {
+		if (!config.enabled('subscription-notifications'))
+			return;
+
+		var args = {node: nodeID};
+		for (i in a1) {
+			var attr = a1[i];
+			if (i == 'subscription') {
+				if (attr == 'none' || attr == 'pending' || attr == 'subscribed' || attr == 'unconfigured')
+					args[i] = attr;
+				else {
+					_('Error #3', 41)
+					return;
+				}
+			} else if (i == 'jid' || i == 'subid')
+				args[i] = attr;
+			else if (i == 'expiry')
+				args[i] = attr.toString();
+		}
+		if (!args.jid || args.jid == '') {
+			_('Error #2', 41)
+			return;
+		}
+		var sub = xmpp.stanza('subscription', args);
+		ev.cx(sub);
+	} else {
+		_('Error #1', 41)
+		return;
+	}
+
+	var subs;
+	if (typeof notifs == 'string') {
+		subs = {};
+		subs[notifs] = storage.getSubscription(notifs, nodeID);
+	} else
+		subs = notifs;
+
+	for (var i in subs) {
+		var sub = subs[i];
+
+		if (sub.options) {
+			if (typeof sub.options['pubsub#deliver'] != 'undefined' && !sub.options['pubsub#deliver'])
+				continue;
+
+			if (typeof sub.options['pubsub#digest'] != 'undefined' && sub.options['pubsub#digest']) {
+				if (!sub.digest)
+					sub.digest = [];
+				sub.digest.push(ev)
+
+				if (sub.digestTimeout)
+					continue;
+
+				var freq;
+				if (typeof sub.options['pubsub#digest_frequency'] == 'undefined')
+					freq = 0;
+				else
+					freq = parseInt(sub.options['pubsub#digest_frequency']);
+
+				if (freq == 0)
+					freq = 24*60*60*1000;
+
+				setTimeout(sendDigest, freq, notifs[i], nodeID);
+				sub.digestTimeout = true;
+				continue;
+			}
+		}
+
+		var message = xmpp.message({to: i, from: componentJID, id: conn.getUniqueId()});
+		message.cx(ev);
+		conn.send(message);
+	}
+}
+
+function sendDigest(jid, nodeID) {
+	var sub = storage.getSubscription(jid, nodeID);
+	if (sub.digestTimeout)
+		sub.digestTimeout = false;
+
+	var message = xmpp.message({to: jid, from: componentJID, id: conn.getUniqueId()});
+	for (var i in sub.digest)
+		message.cx(sub.digest[i]);
+	conn.send(message);
+}
+
+conn.connect(componentJID, componentPassword, function (status, condition) {
+	if (status == xmpp.Status.CONNECTED) {
+		conn.addHandler(onMessage, null, 'message', null, null,  null);
+		conn.addHandler(onIq, null, 'iq', null, null,  null);
+		conn.addHandler(onPresence, null, 'presence', null, null,  null);
+
+		if (process.argv.length >= 3)
+			storage.load(process.argv[2]);
+		else
+			storage.load();
+
+		var stdin = process.openStdin();
+		stdin.setEncoding('utf8');
+		stdin.addListener('data', storage.debug);
+	} else
+		conn.log(xmpp.LogLevel.DEBUG, 'New connection status: ' + status + (condition? (' ('+condition+')'): ''));
+});
new file mode 100644
--- /dev/null
+++ b/save.json
@@ -0,0 +1,1 @@
+{}
new file mode 100644
--- /dev/null
+++ b/storage.js
@@ -0,0 +1,536 @@
+/*
+ *  Copyright (C) 2010  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ *
+ *  This file is part of PSĜS, a PubSub server written in JavaScript.
+ *
+ *  PSĜS is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License.
+ *
+ *  PSĜS is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with PSĜS.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var sys = require('sys');
+var sha1hex = require('sha1').hex;
+require('./iso8601');
+var errors = require('./errors');
+var config = require('./configuration');
+var service_configuration = config.service_configuration;
+var Configuration = config.Configuration;
+var utils = require('./util');
+var toBareJID = utils.toBareJID;
+
+var nodes = require('./nodes');
+var Node = nodes.Node;
+var Item = nodes.Item;
+
+var fs = require('fs');
+
+var list = {};
+
+var storage = exports;
+storage.createNode = function(nodeID, params) {
+	for (var i in list)
+		if (i == nodeID)
+			return errors.owner.create.nodeid_already_exists.n;
+
+	list[nodeID] = new Node(params);
+	storage.save();
+	return errors.success;
+};
+
+storage.getMetadata = function(nodeID) {
+	var node = storage.getNode(nodeID);
+	if (typeof node == 'number')
+		return node;
+
+	if (!config.enabled('meta-data'))
+		return {};
+
+	var md = {};
+	for (var i in service_configuration.node_metadata) {
+		if (i == 'FORM_TYPE')
+			continue;
+
+		if (node.configuration[i])
+			md[i] = node.configuration[i];
+		else if (node.metadata[i])
+			md[i] = node.metadata[i];
+	}
+	return md;
+};
+
+storage.getConfiguration = function(nodeID) {
+	var node = storage.getNode(nodeID);
+	if (typeof node == 'number')
+		return node;
+
+	if (config.enabled('config-node'))
+		return node.configuration;
+
+	return {}; //FIXME
+};
+
+storage.configure = function(nodeID, params) {
+	var node = storage.getNode(nodeID);
+	if (typeof node == 'number')
+		return node;
+
+	if (!config.enabled('config-node'))
+		return 42; //FIXME
+
+	for (var i in params)
+		if (service_configuration.node_config[i])
+			node.configuration[i] = params[i];
+
+	storage.save();
+	return errors.success;
+};
+
+storage.configureSubscription = function(nodeID, jid, params) {
+	var node = storage.getNode(nodeID);
+	if (typeof node == 'number')
+		return node;
+
+	if (!config.enabled('subscription-options'))
+		return 42; //FIXME
+
+	var options = node.subscribers[jid].options;
+	if (!options)
+		return 42; //FIXME
+
+	for (var i in params)
+		if (service_configuration.subscribe_options[i])
+			options[i] = params[i];
+
+	storage.save();
+	return errors.success;
+};
+
+storage.getChildren = function(node) {
+	var n;
+	var items = {};
+	if (node)
+		n = storage.getNode(node).items;
+	else
+		n = list;
+
+	for (var i in n) {
+		var type;
+		if (n[i].content)
+			type = 'item';
+		else
+			type = 'node';
+		items[i] = type;
+	}
+	return items;
+};
+
+storage.getNode = function(nodeID) {
+	if (list[nodeID])
+		return list[nodeID];
+	return errors.sub.subscribe.node_does_not_exist.n;
+};
+
+storage.existsNode = function(nodeID) {
+	if (list[nodeID])
+		return true;
+	return false;
+};
+
+storage.purgeNode = function(nodeID) {
+	var node = storage.getNode(nodeID);
+	if (typeof n == 'number')
+		return node;
+
+	var notifs = storage.getSubscriptionsFromNodeID(nodeID);
+
+	var item = storage.getLastItem(nodeID);
+	if (typeof item == 'number') {
+		node.items = {};
+	} else {
+		items = node.items[item];
+		node.items = {};
+		node.items[item] = items;
+	}
+
+	storage.save();
+	return notifs;
+};
+
+storage.deleteNodeWithRedirect = function(nodeID, uri) {
+	var del = storage.deleteNode(nodeID);
+	if (typeof del == 'number')
+		return del;
+
+	list[nodeID] = uri;
+	storage.save();
+	return errors.success;
+};
+
+storage.deleteNode = function(nodeID) {
+	var node = storage.getNode(nodeID);
+	if (typeof n == 'number')
+		return node;
+
+	var notifs = {};
+	for (var i in node.subscribers)
+		notifs[i] = {};
+	for (var i in node.owner)
+		notifs[node.owner[i]] = {};
+	if (config.enabled('publisher-affiliation'))
+		for (var i in node.publisher)
+			notifs[node.publisher[i]] = {};
+	if (config.enabled('publish-only-affiliation'))
+		for (var i in node.publishOnly)
+			notifs[node.publishOnly[i]] = {};
+
+	delete list[nodeID];
+	storage.save();
+	return notifs;
+};
+
+storage.setItem = function(nodeID, itemID, content) {
+	var node = storage.getNode(nodeID);
+	if (typeof node == 'number')
+		return node;
+
+	if (typeof itemID != 'string')
+		itemID = utils.makeRandomId();
+
+	i = node.setItem(itemID, content);
+	if (content)
+		i.content = content;
+
+	storage.save();
+	return storage.getSubscriptionsFromNodeID(nodeID)
+};
+
+storage.deleteItem = function(nodeID, itemID) {
+	var node = storage.getNode(nodeID);
+	if (typeof node == 'number')
+		return node;
+
+	if (typeof itemID != 'string')
+		return errors.pub.retract.item_or_itemid_required.n;
+
+	item = node.setItem(itemID);
+	if (typeof item == 'number')
+		return item;
+
+	storage.save();
+	return storage.getSubscriptionsFromNodeID(nodeID)
+};
+
+storage.getItems = function(nodeID) {
+	var node;
+	if (typeof nodeID == 'string')
+		node = storage.getNode(nodeID);
+	else
+		node = nodeID;
+
+	if (typeof node == 'number')
+		return node;
+
+	return node.items;
+};
+
+storage.getLastItem = function(nodeID, number) {
+	var items = storage.getItems(nodeID);
+	if (typeof items == 'number')
+		return items;
+
+	if (items == {})
+		return 42; //FIXME
+
+	if (number) {
+		var last = [];
+		var j = 0;
+		for (var i in items) {
+			last.push({name: i, date: items[i].date, content: items[i].content});
+			j++;
+		}
+		if (j < number)
+			return last;
+
+		var cmp = function(a, b) {
+			return b.date - a.date;
+		}
+		last = last.sort(cmp).slice(0, number);
+	} else {
+		var last;
+		for (var i in items)
+			if ((typeof last == 'undefined') || (items[i].date >= items[last].date))
+				last = i;
+	}
+
+	if (last)
+		return last;
+	return 42; //FIXME
+};
+
+storage.existsItem = function(nodeID, itemID) {
+	var items = storage.getItems(nodeID);
+	if (typeof items == 'number')
+		return items;
+
+	for (var i in items)
+		if (i == itemID)
+			return items[i];
+	return false;
+};
+
+storage.getItem = function(nodeID, itemID) {
+	if (!storage.existsItem(nodeID, itemID))
+		return 42; //FIXME
+
+	var items = storage.existsItem(nodeID, itemID);
+	if (typeof items == 'number')
+		return items;
+	if (items)
+		return items.content;
+	return errors.item_not_found;
+};
+
+storage.getSubscription = function(jid, nodeID) {
+	var subs = {};
+	if (nodeID) {
+		var node = storage.getNode(nodeID);
+		for (var sub in node.subscribers)
+			if (toBareJID(sub) == jid)
+				return node.subscribers[sub]
+	} else {
+		for (var node in list) {
+			for (var sub in list[node].subscribers) {
+				if (toBareJID(sub) == jid)
+					subs[node] = list[node].subscribers[sub];
+			}
+		}
+	}
+	return subs;
+};
+
+storage.getSubscriptionsFromNodeID = function(nodeID) {
+	var node;
+	if (typeof nodeID == 'string') {
+		node = storage.getNode(nodeID);
+		if (typeof node == 'number')
+			return node;
+	} else
+		node = nodeID;
+
+	var subs = {};
+	for (var sub in node.subscribers) {
+		if (typeof sub == 'string') {
+			var subscription = {subid: node.subscribers[sub].subid, type: node.subscribers[sub].type};
+			if (node.subscribers[sub].options)
+				subscription.options = node.subscribers[sub].options;
+			subs[sub] = subscription;
+		}
+	}
+	return subs;
+};
+
+storage.subscribe = function(nodeID, jid, type, params) {
+	if (!config.enabled('subscribe'))
+		return errors.sub.subscribe.not_supported.n;
+
+	var node = storage.getNode(nodeID);
+	if (typeof node == 'number')
+		return node;
+
+	if (typeof type == 'undefined')
+		type = 'subscribed';
+
+	if (type == 'none') {
+		if (!node.subscribers[jid])
+			return errors.sub.unsubscribe.no_such_subscriber.n;
+	}
+
+	var subid = node.setSubscriber(jid, type, null, params);
+	storage.save();
+	return {subid: subid, type: type};
+};
+
+storage.setAffiliation = function(nodeID, jid, affil) {
+	var node = storage.getNode(nodeID);
+	if (typeof node == 'number')
+		return node;
+
+	if (affil == 'owner')
+		node.owner.push(jid);
+	else if (config.enabled('publisher-affiliation') && affil == 'publisher')
+		node.publisher.push(jid);
+	else if (config.enabled('publish-only-affiliation') && affil == 'publish-only')
+		node.publishOnly.push(jid);
+	else if (config.enabled('member-affiliation') && affil == 'member')
+		node.member.push(jid);
+	else if (config.enabled('outcast-affiliation') && affil == 'outcast')
+		node.outcast.push(jid);
+
+	storage.save();
+	return errors.success;
+};
+
+storage.getAffiliation = function(jid, nodeID) {
+	var node;
+	if (typeof nodeID == 'string') {
+		node = storage.getNode(nodeID);
+		if (typeof node == 'number')
+			return node;
+	} else
+		node = nodeID;
+
+	for (var affil in node.owner)
+		if (typeof affil == 'string' && node.owner[affil] == jid)
+			return 'owner';
+	if (config.enabled('publisher-affiliation'))
+		for (var affil in node.publisher)
+			if (typeof affil == 'string' && node.publisher[affil] == jid)
+				return 'publisher';
+	if (config.enabled('publish-only-affiliation'))
+		for (var affil in node.publishOnly)
+			if (typeof affil == 'string' && node.publishOnly[affil] == jid)
+				return 'publish-only';
+	if (config.enabled('outcast-affiliation'))
+		for (var affil in node.outcast)
+			if (typeof affil == 'string' && node.outcast[affil] == jid)
+				return 'outcast';
+	return 'none';
+};
+
+storage.getAffiliationsFromJID = function(jid) {
+	var affils = {};
+	for (var node in list) {
+		var n = list[node];
+		for (var affil in n.owner)
+			if (typeof affil == 'string' && n.owner[affil] == jid) {
+				affils[node] = 'owner';
+				break;
+			}
+		if (config.enabled('publisher-affiliation'))
+			for (var affil in n.publisher)
+				if (typeof affil == 'string' && n.publisher[affil] == jid) {
+					affils[node] = 'publisher';
+					break;
+				}
+		if (config.enabled('publish-only-affiliation'))
+			for (var affil in n.publishOnly)
+				if (typeof affil == 'string' && n.publishOnly[affil] == jid) {
+					affils[node] = 'publish-only';
+					break;
+				}
+		if (config.enabled('outcast-affiliation'))
+			for (var affil in n.outcast)
+				if (typeof affil == 'string' && n.outcast[affil] == jid) {
+					affils[node] = 'outcast';
+					break;
+				}
+	}
+	return affils;
+};
+
+storage.getAffiliationsFromNodeID = function(nodeID) {
+	var node;
+	if (typeof nodeID == 'string') {
+		node = storage.getNode(nodeID);
+		if (typeof node == 'number')
+			return node;
+	} else
+		node = nodeID;
+
+	var affils = {};
+	for (var jid in node.owner)
+		if (typeof jid == 'string')
+			affils[node.owner[jid]] = 'owner';
+	if (config.enabled('publisher-affiliation'))
+		for (var jid in node.publisher)
+			if (typeof jid == 'string')
+				affils[node.publisher[jid]] = 'publisher';
+	if (config.enabled('publish-only-affiliation'))
+		for (var jid in node.publishOnly)
+			if (typeof jid == 'string')
+				affils[node.publishOnly[jid]] = 'publish-only';
+	if (config.enabled('member-affiliation'))
+		for (var jid in node.member)
+			if (typeof jid == 'string')
+				affils[node.member[jid]] = 'member';
+	if (config.enabled('outcast-affiliation'))
+		for (var jid in node.outcast)
+			if (typeof jid == 'string')
+				affils[node.outcast[jid]] = 'outcast';
+	return affils;
+};
+
+storage.save = function(file) {
+	function sanitize(o) {
+		var n = {};
+		for (var i in o) {
+			if (i == 'content' || o[i].setISO8601)
+				n[i] = o[i].toString();
+			else if (o[i] instanceof Array)
+				n[i] = o[i];
+			else if (typeof o[i] == 'object')
+				n[i] = sanitize(o[i]);
+			else if (typeof o[i] == 'function')
+				;
+			else
+				n[i] = o[i];
+		}
+		return n;
+	}
+
+	var data = sanitize(list);
+
+	if (!file)
+		file = 'save.json';
+
+	fs.writeFile(file, sys.inspect(data, null, null));
+}
+
+storage.load = function(file) {
+	var xmpp = require('xmpp');
+	function parseStanza(path, content) {
+		var stanza = null;
+		var stream = new xmpp.Stream({
+			stanza: function (stanza) {
+				path[content] = stanza;
+			}
+		});
+		stream.opened = true;
+		stream.data(path[content]);
+	}
+
+	function endParsing(o) {
+		var regexp = /\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ/;
+		for (var i in o) {
+			if (typeof o[i] == 'string' && i == 'content')
+				parseStanza(o, i);
+			else if (typeof o[i] == 'string' && regexp(o[i])) {
+				var today = new Date();
+				today.setISO8601(o[i]);
+				o[i] = today;
+			} else if (typeof o[i] == 'object')
+				endParsing(o[i]);
+		}
+		return o;
+	}
+
+	if (!file)
+		file = 'save.json';
+
+	var data = fs.readFileSync(file);
+	var obj = eval('('+data+')');
+	list = endParsing(obj);
+}
+
+storage.debug = function() {
+	sys.puts('\033[1;33m' + sys.inspect(list, null, null) + '\033[0m');
+};
new file mode 100644
--- /dev/null
+++ b/util.js
@@ -0,0 +1,50 @@
+/*
+ *  Copyright (C) 2010  Emmanuel Gil Peyrot <linkmauve@linkmauve.fr>
+ *
+ *  This file is part of PSĜS, a PubSub server written in JavaScript.
+ *
+ *  PSĜS is free software: you can redistribute it and/or modify
+ *  it under the terms of the GNU Affero General Public License as
+ *  published by the Free Software Foundation, either version 3 of the
+ *  License.
+ *
+ *  PSĜS is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU Affero General Public License for more details.
+ *
+ *  You should have received a copy of the GNU Affero General Public License
+ *  along with PSĜS.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+var sha1hex = require('sha1').hex;
+
+var util = exports;
+util.makeRandomId = function() {
+	return sha1hex(Date()+Math.random());
+};
+
+var JID = function(jid) {
+	var a = jid.indexOf('@');
+	var b = jid.indexOf('/', a);
+	this.full = jid;
+	if (b == -1) {
+		this.resource = '';
+	} else {
+		this.resource = jid.substring(b+1);
+		jid = jid.substring(0, b);
+	}
+	if (a == -1) {
+		this.user = '';
+		this.server = jid;
+	} else {
+		this.user = jid.substring(0, a);
+		this.server = jid.substring(a+1);
+	}
+	this.bare = jid;
+};
+
+util.toBareJID = function(jid) {
+	var j = new JID(jid);
+	return j.bare;
+};