Mercurial > psgxs
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/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; +};