├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .travis.yml ├── LICENSE.txt ├── README.md ├── dub.sdl ├── dub.selections.json ├── public ├── scripts │ ├── jquery.js │ └── vibelog-edit.js └── styles │ ├── common.css │ └── menu.css ├── source ├── app.d └── vibelog │ ├── config.d │ ├── controller.d │ ├── db │ ├── dbcontroller.d │ └── mongo.d │ ├── info.d │ ├── internal │ ├── diskuto.d │ └── passwordhash.d │ ├── post.d │ ├── rss.d │ ├── settings.d │ ├── user.d │ ├── web.d │ └── webadmin.d └── views ├── layout.dt ├── mail.new_comment.dt ├── vibelog.admin.editconfig.dt ├── vibelog.admin.editconfiglist.dt ├── vibelog.admin.editpost.dt ├── vibelog.admin.editpostslist.dt ├── vibelog.admin.edituser.dt ├── vibelog.admin.edituserlist.dt ├── vibelog.admin.home.dt ├── vibelog.admin.inc.nav.dt ├── vibelog.admin.layout.dt ├── vibelog.admin.login.dt ├── vibelog.blocks.dt ├── vibelog.inc.header.dt ├── vibelog.inc.headlinelist.dt ├── vibelog.inc.postlist.dt ├── vibelog.layout.dt ├── vibelog.post.dt └── vibelog.postlist.dt /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test Suite 2 | 3 | # Only triggers on pushes/PRs to master 4 | on: 5 | pull_request: 6 | branches: 7 | - master 8 | push: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | test: 14 | name: CI 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | os: [ubuntu-latest] 19 | dc: [dmd-latest, ldc-latest] 20 | arch: [x86_64] 21 | 22 | runs-on: ${{ matrix.os }} 23 | steps: 24 | - uses: actions/checkout@v2 25 | 26 | - name: Install D compiler 27 | uses: dlang-community/setup-dlang@v1 28 | with: 29 | compiler: ${{ matrix.dc }} 30 | 31 | - name: Run tests 32 | env: 33 | CONFIG: ${{matrix.config}} 34 | ARCH: ${{matrix.arch}} 35 | shell: bash 36 | run: dub test 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.dub 2 | *.a 3 | *.lib 4 | *.o 5 | *.obj 6 | bin/ 7 | *.exe 8 | *.txt 9 | /vibelog 10 | /unidecode-yaml/* 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | 3 | d: 4 | - dmd 5 | - dmd-2.078.2,dub 6 | 7 | services: 8 | - mongodb 9 | 10 | addons: 11 | apt: 12 | packages: 13 | - pkg-config 14 | - zlib1g-dev 15 | - libevent-dev 16 | - libssl-dev 17 | 18 | sudo: false 19 | 20 | script: 21 | - dub upgrade --missing-only # attempt to work around DUB build issue 22 | - dub test 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | vibelog, a fast and simple weblog implementation 633 | Copyright (C) 2012-2013 rejectedsoftware e.K. 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | vibelog 2 | ======= 3 | 4 | A fast and simple embeddable blog for multi-site authoring 5 | 6 | The base package comes with a default style. For an example of a custom style, check out the [vibe.d blog](http://vibed.org/blog/) or [Aspect blog](https://aspect.bildhuus.com/blog/). 7 | 8 | [![Build Status](https://travis-ci.org/rejectedsoftware/vibelog.svg?branch=master)](https://travis-ci.org/rejectedsoftware/vibelog) 9 | 10 | 11 | Main features 12 | ------------- 13 | 14 | - Multi-site configurations 15 | - Multi-user management with access restriction 16 | - Directly embeddable in vibe.d sites 17 | - RSS feed 18 | - User comments 19 | - Customizable template based layout 20 | - Heading, sub heading, header image, automatic post slug creation 21 | 22 | Prerequisites 23 | ------------- 24 | 25 | VibeLog needs [dub](https://github.com/rejectedsoftware/dub/) and [MongoDB](http://www.mongodb.org/) installed. 26 | 27 | Running a simple stand-alone blog 28 | --------------------------------- 29 | 30 | 1. Clone vibelog 31 | 32 | $ git clone git://github.com/rejectedsoftware/vibelog.git 33 | 34 | 2. Compile and run 35 | 36 | $ cd vibelog 37 | $ dub run 38 | 39 | The blog is now accessible at . 40 | 41 | 42 | Embedding VibeLog into your own application 43 | ------------------------------------------- 44 | 45 | 1. Create a new project: 46 | 47 | $ dub init my-blog 48 | $ cd my-blog 49 | 50 | 2. Edit package.json and add the following entries to the "dependencies" section: 51 | 52 | "vibelog": ">=0.0.9" 53 | 54 | 3. Edit source/app.d: 55 | 56 | ``` 57 | import vibe.d; 58 | import vibelog.vibelog; 59 | 60 | static this() 61 | { 62 | auto router = new UrlRouter; 63 | 64 | auto blogsettings = new VibeLogSettings; 65 | blogsettings.configName = "vibelog"; 66 | blogsettings.basePath = "/"; 67 | registerVibeLog(blogsettings, router); 68 | 69 | router.get("*", serveStaticFiles("./public")); 70 | 71 | auto settings = new HttpServerSettings; 72 | settings.port = 8080; 73 | listenHttp(settings, router); 74 | } 75 | ``` 76 | 77 | If you want to run multiple blogs on the same database, you should choose a meaningful configuration name instead of "vibelog". Each blog should have its own configuration name. 78 | 79 | 4. Start the application (dub will automatically download vibelog and vibe.d as dependencies) 80 | 81 | $ dub run 82 | 83 | You will probably also want to copy the views/layout.dt file to your own project and modify it to your needs (e.g. by adding a style sheet). The blog is accessible at . 84 | 85 | 86 | Setting everything up 87 | --------------------- 88 | 89 | 1. Go to the management page on your blog (e.g. ). Use username `admin` and password `admin` when logging in for the first time. 90 | 91 | 2. Open the user management page and create a new user. Be sure to make the new user an administrator. The `admin` user will be disabled afterwards. 92 | 93 | 3. Open the configuration management page and edit the `global` configuration. You should add at least one category here. Each line in the text field represents one configuration and must not contain spaces. 94 | 95 | 4. Now edit the blog's configuration (e.g. `vibelog`) and check all categories that should be listed on the blog. 96 | 97 | 5. Start posting new articles by choosing `New post` from the management page. -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "vibelog" 2 | description "A light-weight embeddable blog implementation" 3 | homepage "https://github.com/rejectedsoftware/vibelog" 4 | authors "Sönke Ludwig" 5 | license "AGPL-3.0" 6 | 7 | dependency "vibe-d" version=">=0.7.31 <0.11.0-0" 8 | dependency "stringex" version="~>0.1.0" 9 | dependency "diskuto" version="~>1.5" 10 | 11 | configuration "standalone" { 12 | targetType "executable" 13 | versions "VibeDefaultMain" 14 | } 15 | 16 | configuration "library" { 17 | targetType "library" 18 | excludedSourceFiles "source/app.d" 19 | } 20 | -------------------------------------------------------------------------------- /dub.selections.json: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "versions": { 4 | "antispam": "0.1.7", 5 | "diet-ng": "1.8.2", 6 | "diskuto": "1.7.6", 7 | "eventcore": "0.9.35", 8 | "fuzzydate": "1.0.0", 9 | "mir-linux-kernel": "1.2.1", 10 | "openssl": "3.3.4", 11 | "openssl-static": "1.0.5+3.0.8", 12 | "stdx-allocator": "2.77.5", 13 | "stringex": "0.1.1", 14 | "taggedalgebraic": "0.11.23", 15 | "vibe-container": "1.3.1", 16 | "vibe-core": "2.9.6", 17 | "vibe-d": "0.10.1", 18 | "vibe-http": "1.1.2", 19 | "vibe-inet": "1.0.1", 20 | "vibe-serialization": "1.0.6", 21 | "vibe-stream": "1.1.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /public/scripts/jquery.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v@1.8.0 jquery.com | jquery.org/license */ 2 | (function(a,b){function G(a){var b=F[a]={};return p.each(a.split(s),function(a,c){b[c]=!0}),b}function J(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(I,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:+d+""===d?+d:H.test(d)?p.parseJSON(d):d}catch(f){}p.data(a,c,d)}else d=b}return d}function K(a){var b;for(b in a){if(b==="data"&&p.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function ba(){return!1}function bb(){return!0}function bh(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function bi(a,b){do a=a[b];while(a&&a.nodeType!==1);return a}function bj(a,b,c){b=b||0;if(p.isFunction(b))return p.grep(a,function(a,d){var e=!!b.call(a,d,a);return e===c});if(b.nodeType)return p.grep(a,function(a,d){return a===b===c});if(typeof b=="string"){var d=p.grep(a,function(a){return a.nodeType===1});if(be.test(b))return p.filter(b,d,!c);b=p.filter(b,d)}return p.grep(a,function(a,d){return p.inArray(a,b)>=0===c})}function bk(a){var b=bl.split("|"),c=a.createDocumentFragment();if(c.createElement)while(b.length)c.createElement(b.pop());return c}function bC(a,b){return a.getElementsByTagName(b)[0]||a.appendChild(a.ownerDocument.createElement(b))}function bD(a,b){if(b.nodeType!==1||!p.hasData(a))return;var c,d,e,f=p._data(a),g=p._data(b,f),h=f.events;if(h){delete g.handle,g.events={};for(c in h)for(d=0,e=h[c].length;d").appendTo(e.body),c=b.css("display");b.remove();if(c==="none"||c===""){bI=e.body.appendChild(bI||p.extend(e.createElement("iframe"),{frameBorder:0,width:0,height:0}));if(!bJ||!bI.createElement)bJ=(bI.contentWindow||bI.contentDocument).document,bJ.write(""),bJ.close();b=bJ.body.appendChild(bJ.createElement(a)),c=bH(b,"display"),e.body.removeChild(bI)}return bR[a]=c,c}function ch(a,b,c,d){var e;if(p.isArray(b))p.each(b,function(b,e){c||cd.test(a)?d(a,e):ch(a+"["+(typeof e=="object"?b:"")+"]",e,c,d)});else if(!c&&p.type(b)==="object")for(e in b)ch(a+"["+e+"]",b[e],c,d);else d(a,b)}function cy(a){return function(b,c){typeof b!="string"&&(c=b,b="*");var d,e,f,g=b.toLowerCase().split(s),h=0,i=g.length;if(p.isFunction(c))for(;h)[^>]*$|#([\w\-]*)$)/,v=/^<(\w+)\s*\/?>(?:<\/\1>|)$/,w=/^[\],:{}\s]*$/,x=/(?:^|:|,)(?:\s*\[)+/g,y=/\\(?:["\\\/bfnrt]|u[\da-fA-F]{4})/g,z=/"[^"\\\r\n]*"|true|false|null|-?(?:\d\d*\.|)\d+(?:[eE][\-+]?\d+|)/g,A=/^-ms-/,B=/-([\da-z])/gi,C=function(a,b){return(b+"").toUpperCase()},D=function(){e.addEventListener?(e.removeEventListener("DOMContentLoaded",D,!1),p.ready()):e.readyState==="complete"&&(e.detachEvent("onreadystatechange",D),p.ready())},E={};p.fn=p.prototype={constructor:p,init:function(a,c,d){var f,g,h,i;if(!a)return this;if(a.nodeType)return this.context=this[0]=a,this.length=1,this;if(typeof a=="string"){a.charAt(0)==="<"&&a.charAt(a.length-1)===">"&&a.length>=3?f=[null,a,null]:f=u.exec(a);if(f&&(f[1]||!c)){if(f[1])return c=c instanceof p?c[0]:c,i=c&&c.nodeType?c.ownerDocument||c:e,a=p.parseHTML(f[1],i,!0),v.test(f[1])&&p.isPlainObject(c)&&this.attr.call(a,c,!0),p.merge(this,a);g=e.getElementById(f[2]);if(g&&g.parentNode){if(g.id!==f[2])return d.find(a);this.length=1,this[0]=g}return this.context=e,this.selector=a,this}return!c||c.jquery?(c||d).find(a):this.constructor(c).find(a)}return p.isFunction(a)?d.ready(a):(a.selector!==b&&(this.selector=a.selector,this.context=a.context),p.makeArray(a,this))},selector:"",jquery:"1.8.0",length:0,size:function(){return this.length},toArray:function(){return k.call(this)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=p.merge(this.constructor(),a);return d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")"),d},each:function(a,b){return p.each(this,a,b)},ready:function(a){return p.ready.promise().done(a),this},eq:function(a){return a=+a,a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(k.apply(this,arguments),"slice",k.call(arguments).join(","))},map:function(a){return this.pushStack(p.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:j,sort:[].sort,splice:[].splice},p.fn.init.prototype=p.fn,p.extend=p.fn.extend=function(){var a,c,d,e,f,g,h=arguments[0]||{},i=1,j=arguments.length,k=!1;typeof h=="boolean"&&(k=h,h=arguments[1]||{},i=2),typeof h!="object"&&!p.isFunction(h)&&(h={}),j===i&&(h=this,--i);for(;i0)return;d.resolveWith(e,[p]),p.fn.trigger&&p(e).trigger("ready").off("ready")},isFunction:function(a){return p.type(a)==="function"},isArray:Array.isArray||function(a){return p.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):E[m.call(a)]||"object"},isPlainObject:function(a){if(!a||p.type(a)!=="object"||a.nodeType||p.isWindow(a))return!1;try{if(a.constructor&&!n.call(a,"constructor")&&!n.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||n.call(a,d)},isEmptyObject:function(a){var b;for(b in a)return!1;return!0},error:function(a){throw new Error(a)},parseHTML:function(a,b,c){var d;return!a||typeof a!="string"?null:(typeof b=="boolean"&&(c=b,b=0),b=b||e,(d=v.exec(a))?[b.createElement(d[1])]:(d=p.buildFragment([a],b,c?null:[]),p.merge([],(d.cacheable?p.clone(d.fragment):d.fragment).childNodes)))},parseJSON:function(b){if(!b||typeof b!="string")return null;b=p.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(w.test(b.replace(y,"@").replace(z,"]").replace(x,"")))return(new Function("return "+b))();p.error("Invalid JSON: "+b)},parseXML:function(c){var d,e;if(!c||typeof c!="string")return null;try{a.DOMParser?(e=new DOMParser,d=e.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(f){d=b}return(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&p.error("Invalid XML: "+c),d},noop:function(){},globalEval:function(b){b&&r.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(A,"ms-").replace(B,C)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var e,f=0,g=a.length,h=g===b||p.isFunction(a);if(d){if(h){for(e in a)if(c.apply(a[e],d)===!1)break}else for(;f0&&a[0]&&a[i-1]||i===0||p.isArray(a));if(j)for(;h-1)i.splice(c,1),e&&(c<=g&&g--,c<=h&&h--)}),this},has:function(a){return p.inArray(a,i)>-1},empty:function(){return i=[],this},disable:function(){return i=j=c=b,this},disabled:function(){return!i},lock:function(){return j=b,c||l.disable(),this},locked:function(){return!j},fireWith:function(a,b){return b=b||[],b=[a,b.slice?b.slice():b],i&&(!d||j)&&(e?j.push(b):k(b)),this},fire:function(){return l.fireWith(this,arguments),this},fired:function(){return!!d}};return l},p.extend({Deferred:function(a){var b=[["resolve","done",p.Callbacks("once memory"),"resolved"],["reject","fail",p.Callbacks("once memory"),"rejected"],["notify","progress",p.Callbacks("memory")]],c="pending",d={state:function(){return c},always:function(){return e.done(arguments).fail(arguments),this},then:function(){var a=arguments;return p.Deferred(function(c){p.each(b,function(b,d){var f=d[0],g=a[b];e[d[1]](p.isFunction(g)?function(){var a=g.apply(this,arguments);a&&p.isFunction(a.promise)?a.promise().done(c.resolve).fail(c.reject).progress(c.notify):c[f+"With"](this===e?c:this,[a])}:c[f])}),a=null}).promise()},promise:function(a){return typeof a=="object"?p.extend(a,d):d}},e={};return d.pipe=d.then,p.each(b,function(a,f){var g=f[2],h=f[3];d[f[1]]=g.add,h&&g.add(function(){c=h},b[a^1][2].disable,b[2][2].lock),e[f[0]]=g.fire,e[f[0]+"With"]=g.fireWith}),d.promise(e),a&&a.call(e,e),e},when:function(a){var b=0,c=k.call(arguments),d=c.length,e=d!==1||a&&p.isFunction(a.promise)?d:0,f=e===1?a:p.Deferred(),g=function(a,b,c){return function(d){b[a]=this,c[a]=arguments.length>1?k.call(arguments):d,c===h?f.notifyWith(b,c):--e||f.resolveWith(b,c)}},h,i,j;if(d>1){h=new Array(d),i=new Array(d),j=new Array(d);for(;b
a",c=n.getElementsByTagName("*"),d=n.getElementsByTagName("a")[0],d.style.cssText="top:1px;float:left;opacity:.5";if(!c||!c.length||!d)return{};f=e.createElement("select"),g=f.appendChild(e.createElement("option")),h=n.getElementsByTagName("input")[0],b={leadingWhitespace:n.firstChild.nodeType===3,tbody:!n.getElementsByTagName("tbody").length,htmlSerialize:!!n.getElementsByTagName("link").length,style:/top/.test(d.getAttribute("style")),hrefNormalized:d.getAttribute("href")==="/a",opacity:/^0.5/.test(d.style.opacity),cssFloat:!!d.style.cssFloat,checkOn:h.value==="on",optSelected:g.selected,getSetAttribute:n.className!=="t",enctype:!!e.createElement("form").enctype,html5Clone:e.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",boxModel:e.compatMode==="CSS1Compat",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,boxSizingReliable:!0,pixelPosition:!1},h.checked=!0,b.noCloneChecked=h.cloneNode(!0).checked,f.disabled=!0,b.optDisabled=!g.disabled;try{delete n.test}catch(o){b.deleteExpando=!1}!n.addEventListener&&n.attachEvent&&n.fireEvent&&(n.attachEvent("onclick",m=function(){b.noCloneEvent=!1}),n.cloneNode(!0).fireEvent("onclick"),n.detachEvent("onclick",m)),h=e.createElement("input"),h.value="t",h.setAttribute("type","radio"),b.radioValue=h.value==="t",h.setAttribute("checked","checked"),h.setAttribute("name","t"),n.appendChild(h),i=e.createDocumentFragment(),i.appendChild(n.lastChild),b.checkClone=i.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=h.checked,i.removeChild(h),i.appendChild(n);if(n.attachEvent)for(k in{submit:!0,change:!0,focusin:!0})j="on"+k,l=j in n,l||(n.setAttribute(j,"return;"),l=typeof n[j]=="function"),b[k+"Bubbles"]=l;return p(function(){var c,d,f,g,h="padding:0;margin:0;border:0;display:block;overflow:hidden;",i=e.getElementsByTagName("body")[0];if(!i)return;c=e.createElement("div"),c.style.cssText="visibility:hidden;border:0;width:0;height:0;position:static;top:0;margin-top:1px",i.insertBefore(c,i.firstChild),d=e.createElement("div"),c.appendChild(d),d.innerHTML="
t
",f=d.getElementsByTagName("td"),f[0].style.cssText="padding:0;margin:0;border:0;display:none",l=f[0].offsetHeight===0,f[0].style.display="",f[1].style.display="none",b.reliableHiddenOffsets=l&&f[0].offsetHeight===0,d.innerHTML="",d.style.cssText="box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;padding:1px;border:1px;display:block;width:4px;margin-top:1%;position:absolute;top:1%;",b.boxSizing=d.offsetWidth===4,b.doesNotIncludeMarginInBodyOffset=i.offsetTop!==1,a.getComputedStyle&&(b.pixelPosition=(a.getComputedStyle(d,null)||{}).top!=="1%",b.boxSizingReliable=(a.getComputedStyle(d,null)||{width:"4px"}).width==="4px",g=e.createElement("div"),g.style.cssText=d.style.cssText=h,g.style.marginRight=g.style.width="0",d.style.width="1px",d.appendChild(g),b.reliableMarginRight=!parseFloat((a.getComputedStyle(g,null)||{}).marginRight)),typeof d.style.zoom!="undefined"&&(d.innerHTML="",d.style.cssText=h+"width:1px;padding:1px;display:inline;zoom:1",b.inlineBlockNeedsLayout=d.offsetWidth===3,d.style.display="block",d.style.overflow="visible",d.innerHTML="
",d.firstChild.style.width="5px",b.shrinkWrapBlocks=d.offsetWidth!==3,c.style.zoom=1),i.removeChild(c),c=d=f=g=null}),i.removeChild(n),c=d=f=g=h=i=n=null,b}();var H=/^(?:\{.*\}|\[.*\])$/,I=/([A-Z])/g;p.extend({cache:{},deletedIds:[],uuid:0,expando:"jQuery"+(p.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){return a=a.nodeType?p.cache[a[p.expando]]:a[p.expando],!!a&&!K(a)},data:function(a,c,d,e){if(!p.acceptData(a))return;var f,g,h=p.expando,i=typeof c=="string",j=a.nodeType,k=j?p.cache:a,l=j?a[h]:a[h]&&h;if((!l||!k[l]||!e&&!k[l].data)&&i&&d===b)return;l||(j?a[h]=l=p.deletedIds.pop()||++p.uuid:l=h),k[l]||(k[l]={},j||(k[l].toJSON=p.noop));if(typeof c=="object"||typeof c=="function")e?k[l]=p.extend(k[l],c):k[l].data=p.extend(k[l].data,c);return f=k[l],e||(f.data||(f.data={}),f=f.data),d!==b&&(f[p.camelCase(c)]=d),i?(g=f[c],g==null&&(g=f[p.camelCase(c)])):g=f,g},removeData:function(a,b,c){if(!p.acceptData(a))return;var d,e,f,g=a.nodeType,h=g?p.cache:a,i=g?a[p.expando]:p.expando;if(!h[i])return;if(b){d=c?h[i]:h[i].data;if(d){p.isArray(b)||(b in d?b=[b]:(b=p.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,f=b.length;e1,null,!1))},removeData:function(a){return this.each(function(){p.removeData(this,a)})}}),p.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=p._data(a,b),c&&(!d||p.isArray(c)?d=p._data(a,b,p.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=p.queue(a,b),d=c.shift(),e=p._queueHooks(a,b),f=function(){p.dequeue(a,b)};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),delete e.stop,d.call(a,f,e)),!c.length&&e&&e.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return p._data(a,c)||p._data(a,c,{empty:p.Callbacks("once memory").add(function(){p.removeData(a,b+"queue",!0),p.removeData(a,c,!0)})})}}),p.fn.extend({queue:function(a,c){var d=2;return typeof a!="string"&&(c=a,a="fx",d--),arguments.length1)},removeAttr:function(a){return this.each(function(){p.removeAttr(this,a)})},prop:function(a,b){return p.access(this,p.prop,a,b,arguments.length>1)},removeProp:function(a){return a=p.propFix[a]||a,this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,f,g,h;if(p.isFunction(a))return this.each(function(b){p(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(s);for(c=0,d=this.length;c-1)d=d.replace(" "+c[f]+" "," ");e.className=a?p.trim(d):""}}}return this},toggleClass:function(a,b){var c=typeof a,d=typeof b=="boolean";return p.isFunction(a)?this.each(function(c){p(this).toggleClass(a.call(this,c,this.className,b),b)}):this.each(function(){if(c==="string"){var e,f=0,g=p(this),h=b,i=a.split(s);while(e=i[f++])h=d?h:!g.hasClass(e),g[h?"addClass":"removeClass"](e)}else if(c==="undefined"||c==="boolean")this.className&&p._data(this,"__className__",this.className),this.className=this.className||a===!1?"":p._data(this,"__className__")||""})},hasClass:function(a){var b=" "+a+" ",c=0,d=this.length;for(;c-1)return!0;return!1},val:function(a){var c,d,e,f=this[0];if(!arguments.length){if(f)return c=p.valHooks[f.type]||p.valHooks[f.nodeName.toLowerCase()],c&&"get"in c&&(d=c.get(f,"value"))!==b?d:(d=f.value,typeof d=="string"?d.replace(P,""):d==null?"":d);return}return e=p.isFunction(a),this.each(function(d){var f,g=p(this);if(this.nodeType!==1)return;e?f=a.call(this,d,g.val()):f=a,f==null?f="":typeof f=="number"?f+="":p.isArray(f)&&(f=p.map(f,function(a){return a==null?"":a+""})),c=p.valHooks[this.type]||p.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,f,"value")===b)this.value=f})}}),p.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,f=a.selectedIndex,g=[],h=a.options,i=a.type==="select-one";if(f<0)return null;c=i?f:0,d=i?f+1:h.length;for(;c=0}),c.length||(a.selectedIndex=-1),c}}},attrFn:{},attr:function(a,c,d,e){var f,g,h,i=a.nodeType;if(!a||i===3||i===8||i===2)return;if(e&&p.isFunction(p.fn[c]))return p(a)[c](d);if(typeof a.getAttribute=="undefined")return p.prop(a,c,d);h=i!==1||!p.isXMLDoc(a),h&&(c=c.toLowerCase(),g=p.attrHooks[c]||(T.test(c)?M:L));if(d!==b){if(d===null){p.removeAttr(a,c);return}return g&&"set"in g&&h&&(f=g.set(a,d,c))!==b?f:(a.setAttribute(c,""+d),d)}return g&&"get"in g&&h&&(f=g.get(a,c))!==null?f:(f=a.getAttribute(c),f===null?b:f)},removeAttr:function(a,b){var c,d,e,f,g=0;if(b&&a.nodeType===1){d=b.split(s);for(;g=0}})});var V=/^(?:textarea|input|select)$/i,W=/^([^\.]*|)(?:\.(.+)|)$/,X=/(?:^|\s)hover(\.\S+|)\b/,Y=/^key/,Z=/^(?:mouse|contextmenu)|click/,$=/^(?:focusinfocus|focusoutblur)$/,_=function(a){return p.event.special.hover?a:a.replace(X,"mouseenter$1 mouseleave$1")};p.event={add:function(a,c,d,e,f){var g,h,i,j,k,l,m,n,o,q,r;if(a.nodeType===3||a.nodeType===8||!c||!d||!(g=p._data(a)))return;d.handler&&(o=d,d=o.handler,f=o.selector),d.guid||(d.guid=p.guid++),i=g.events,i||(g.events=i={}),h=g.handle,h||(g.handle=h=function(a){return typeof p!="undefined"&&(!a||p.event.triggered!==a.type)?p.event.dispatch.apply(h.elem,arguments):b},h.elem=a),c=p.trim(_(c)).split(" ");for(j=0;j=0&&(s=s.slice(0,-1),i=!0),s.indexOf(".")>=0&&(t=s.split("."),s=t.shift(),t.sort());if((!f||p.event.customEvent[s])&&!p.event.global[s])return;c=typeof c=="object"?c[p.expando]?c:new p.Event(s,c):new p.Event(s),c.type=s,c.isTrigger=!0,c.exclusive=i,c.namespace=t.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+t.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,m=s.indexOf(":")<0?"on"+s:"";if(!f){h=p.cache;for(j in h)h[j].events&&h[j].events[s]&&p.event.trigger(c,d,h[j].handle.elem,!0);return}c.result=b,c.target||(c.target=f),d=d!=null?p.makeArray(d):[],d.unshift(c),n=p.event.special[s]||{};if(n.trigger&&n.trigger.apply(f,d)===!1)return;q=[[f,n.bindType||s]];if(!g&&!n.noBubble&&!p.isWindow(f)){r=n.delegateType||s,k=$.test(r+s)?f:f.parentNode;for(l=f;k;k=k.parentNode)q.push([k,r]),l=k;l===(f.ownerDocument||e)&&q.push([l.defaultView||l.parentWindow||a,r])}for(j=0;jq&&u.push({elem:this,matches:o.slice(q)});for(d=0;d0?this.on(b,null,a,c):this.trigger(b)},Y.test(b)&&(p.event.fixHooks[b]=p.event.keyHooks),Z.test(b)&&(p.event.fixHooks[b]=p.event.mouseHooks)}),function(a,b){function bd(a,b,c,d){var e=0,f=b.length;for(;e0?h(g,c,f):[]}function bf(a,c,d,e,f){var g,h,i,j,k,l,m,n,p=0,q=f.length,s=L.POS,t=new RegExp("^"+s.source+"(?!"+r+")","i"),u=function(){var a=1,c=arguments.length-2;for(;ai){m=a.slice(i,g.index),i=n,l=[c],B.test(m)&&(k&&(l=k),k=e);if(h=H.test(m))m=m.slice(0,-5).replace(B,"$&*");g.length>1&&g[0].replace(t,u),k=be(m,g[1],g[2],l,k,h)}}k?(j=j.concat(k),(m=a.slice(i))&&m!==")"?B.test(m)?bd(m,j,d,e):Z(m,c,d,e?e.concat(k):k):o.apply(d,j)):Z(a,c,d,e)}return q===1?d:Z.uniqueSort(d)}function bg(a,b,c){var d,e,f,g=[],i=0,j=D.exec(a),k=!j.pop()&&!j.pop(),l=k&&a.match(C)||[""],m=$.preFilter,n=$.filter,o=!c&&b!==h;for(;(e=l[i])!=null&&k;i++){g.push(d=[]),o&&(e=" "+e);while(e){k=!1;if(j=B.exec(e))e=e.slice(j[0].length),k=d.push({part:j.pop().replace(A," "),captures:j});for(f in n)(j=L[f].exec(e))&&(!m[f]||(j=m[f](j,b,c)))&&(e=e.slice(j.shift().length),k=d.push({part:f,captures:j}));if(!k)break}}return k||Z.error(a),g}function bh(a,b,e){var f=b.dir,g=m++;return a||(a=function(a){return a===e}),b.first?function(b,c){while(b=b[f])if(b.nodeType===1)return a(b,c)&&b}:function(b,e){var h,i=g+"."+d,j=i+"."+c;while(b=b[f])if(b.nodeType===1){if((h=b[q])===j)return b.sizset;if(typeof h=="string"&&h.indexOf(i)===0){if(b.sizset)return b}else{b[q]=j;if(a(b,e))return b.sizset=!0,b;b.sizset=!1}}}}function bi(a,b){return a?function(c,d){var e=b(c,d);return e&&a(e===!0?c:e,d)}:b}function bj(a,b,c){var d,e,f=0;for(;d=a[f];f++)$.relative[d.part]?e=bh(e,$.relative[d.part],b):(d.captures.push(b,c),e=bi(e,$.filter[d.part].apply(null,d.captures)));return e}function bk(a){return function(b,c){var d,e=0;for(;d=a[e];e++)if(d(b,c))return!0;return!1}}var c,d,e,f,g,h=a.document,i=h.documentElement,j="undefined",k=!1,l=!0,m=0,n=[].slice,o=[].push,q=("sizcache"+Math.random()).replace(".",""),r="[\\x20\\t\\r\\n\\f]",s="(?:\\\\.|[-\\w]|[^\\x00-\\xa0])+",t=s.replace("w","w#"),u="([*^$|!~]?=)",v="\\["+r+"*("+s+")"+r+"*(?:"+u+r+"*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|("+t+")|)|)"+r+"*\\]",w=":("+s+")(?:\\((?:(['\"])((?:\\\\.|[^\\\\])*?)\\2|((?:[^,]|\\\\,|(?:,(?=[^\\[]*\\]))|(?:,(?=[^\\(]*\\))))*))\\)|)",x=":(nth|eq|gt|lt|first|last|even|odd)(?:\\((\\d*)\\)|)(?=[^-]|$)",y=r+"*([\\x20\\t\\r\\n\\f>+~])"+r+"*",z="(?=[^\\x20\\t\\r\\n\\f])(?:\\\\.|"+v+"|"+w.replace(2,7)+"|[^\\\\(),])+",A=new RegExp("^"+r+"+|((?:^|[^\\\\])(?:\\\\.)*)"+r+"+$","g"),B=new RegExp("^"+y),C=new RegExp(z+"?(?="+r+"*,|$)","g"),D=new RegExp("^(?:(?!,)(?:(?:^|,)"+r+"*"+z+")*?|"+r+"*(.*?))(\\)|$)"),E=new RegExp(z.slice(19,-6)+"\\x20\\t\\r\\n\\f>+~])+|"+y,"g"),F=/^(?:#([\w\-]+)|(\w+)|\.([\w\-]+))$/,G=/[\x20\t\r\n\f]*[+~]/,H=/:not\($/,I=/h\d/i,J=/input|select|textarea|button/i,K=/\\(?!\\)/g,L={ID:new RegExp("^#("+s+")"),CLASS:new RegExp("^\\.("+s+")"),NAME:new RegExp("^\\[name=['\"]?("+s+")['\"]?\\]"),TAG:new RegExp("^("+s.replace("[-","[-\\*")+")"),ATTR:new RegExp("^"+v),PSEUDO:new RegExp("^"+w),CHILD:new RegExp("^:(only|nth|last|first)-child(?:\\("+r+"*(even|odd|(([+-]|)(\\d*)n|)"+r+"*(?:([+-]|)"+r+"*(\\d+)|))"+r+"*\\)|)","i"),POS:new RegExp(x,"ig"),needsContext:new RegExp("^"+r+"*[>+~]|"+x,"i")},M={},N=[],O={},P=[],Q=function(a){return a.sizzleFilter=!0,a},R=function(a){return function(b){return b.nodeName.toLowerCase()==="input"&&b.type===a}},S=function(a){return function(b){var c=b.nodeName.toLowerCase();return(c==="input"||c==="button")&&b.type===a}},T=function(a){var b=!1,c=h.createElement("div");try{b=a(c)}catch(d){}return c=null,b},U=T(function(a){a.innerHTML="";var b=typeof a.lastChild.getAttribute("multiple");return b!=="boolean"&&b!=="string"}),V=T(function(a){a.id=q+0,a.innerHTML="
",i.insertBefore(a,i.firstChild);var b=h.getElementsByName&&h.getElementsByName(q).length===2+h.getElementsByName(q+0).length;return g=!h.getElementById(q),i.removeChild(a),b}),W=T(function(a){return a.appendChild(h.createComment("")),a.getElementsByTagName("*").length===0}),X=T(function(a){return a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!==j&&a.firstChild.getAttribute("href")==="#"}),Y=T(function(a){return a.innerHTML="",!a.getElementsByClassName||a.getElementsByClassName("e").length===0?!1:(a.lastChild.className="e",a.getElementsByClassName("e").length!==1)}),Z=function(a,b,c,d){c=c||[],b=b||h;var e,f,g,i,j=b.nodeType;if(j!==1&&j!==9)return[];if(!a||typeof a!="string")return c;g=ba(b);if(!g&&!d)if(e=F.exec(a))if(i=e[1]){if(j===9){f=b.getElementById(i);if(!f||!f.parentNode)return c;if(f.id===i)return c.push(f),c}else if(b.ownerDocument&&(f=b.ownerDocument.getElementById(i))&&bb(b,f)&&f.id===i)return c.push(f),c}else{if(e[2])return o.apply(c,n.call(b.getElementsByTagName(a),0)),c;if((i=e[3])&&Y&&b.getElementsByClassName)return o.apply(c,n.call(b.getElementsByClassName(i),0)),c}return bm(a,b,c,d,g)},$=Z.selectors={cacheLength:50,match:L,order:["ID","TAG"],attrHandle:{},createPseudo:Q,find:{ID:g?function(a,b,c){if(typeof b.getElementById!==j&&!c){var d=b.getElementById(a);return d&&d.parentNode?[d]:[]}}:function(a,c,d){if(typeof c.getElementById!==j&&!d){var e=c.getElementById(a);return e?e.id===a||typeof e.getAttributeNode!==j&&e.getAttributeNode("id").value===a?[e]:b:[]}},TAG:W?function(a,b){if(typeof b.getElementsByTagName!==j)return b.getElementsByTagName(a)}:function(a,b){var c=b.getElementsByTagName(a);if(a==="*"){var d,e=[],f=0;for(;d=c[f];f++)d.nodeType===1&&e.push(d);return e}return c}},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(K,""),a[3]=(a[4]||a[5]||"").replace(K,""),a[2]==="~="&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),a[1]==="nth"?(a[2]||Z.error(a[0]),a[3]=+(a[3]?a[4]+(a[5]||1):2*(a[2]==="even"||a[2]==="odd")),a[4]=+(a[6]+a[7]||a[2]==="odd")):a[2]&&Z.error(a[0]),a},PSEUDO:function(a){var b,c=a[4];return L.CHILD.test(a[0])?null:(c&&(b=D.exec(c))&&b.pop()&&(a[0]=a[0].slice(0,b[0].length-c.length-1),c=b[0].slice(0,-1)),a.splice(2,3,c||a[3]),a)}},filter:{ID:g?function(a){return a=a.replace(K,""),function(b){return b.getAttribute("id")===a}}:function(a){return a=a.replace(K,""),function(b){var c=typeof b.getAttributeNode!==j&&b.getAttributeNode("id");return c&&c.value===a}},TAG:function(a){return a==="*"?function(){return!0}:(a=a.replace(K,"").toLowerCase(),function(b){return b.nodeName&&b.nodeName.toLowerCase()===a})},CLASS:function(a){var b=M[a];return b||(b=M[a]=new RegExp("(^|"+r+")"+a+"("+r+"|$)"),N.push(a),N.length>$.cacheLength&&delete M[N.shift()]),function(a){return b.test(a.className||typeof a.getAttribute!==j&&a.getAttribute("class")||"")}},ATTR:function(a,b,c){return b?function(d){var e=Z.attr(d,a),f=e+"";if(e==null)return b==="!=";switch(b){case"=":return f===c;case"!=":return f!==c;case"^=":return c&&f.indexOf(c)===0;case"*=":return c&&f.indexOf(c)>-1;case"$=":return c&&f.substr(f.length-c.length)===c;case"~=":return(" "+f+" ").indexOf(c)>-1;case"|=":return f===c||f.substr(0,c.length+1)===c+"-"}}:function(b){return Z.attr(b,a)!=null}},CHILD:function(a,b,c,d){if(a==="nth"){var e=m++;return function(a){var b,f,g=0,h=a;if(c===1&&d===0)return!0;b=a.parentNode;if(b&&(b[q]!==e||!a.sizset)){for(h=b.firstChild;h;h=h.nextSibling)if(h.nodeType===1){h.sizset=++g;if(h===a)break}b[q]=e}return f=a.sizset-d,c===0?f===0:f%c===0&&f/c>=0}}return function(b){var c=b;switch(a){case"only":case"first":while(c=c.previousSibling)if(c.nodeType===1)return!1;if(a==="first")return!0;c=b;case"last":while(c=c.nextSibling)if(c.nodeType===1)return!1;return!0}}},PSEUDO:function(a,b,c,d){var e=$.pseudos[a]||$.pseudos[a.toLowerCase()];return e||Z.error("unsupported pseudo: "+a),e.sizzleFilter?e(b,c,d):e}},pseudos:{not:Q(function(a,b,c){var d=bl(a.replace(A,"$1"),b,c);return function(a){return!d(a)}}),enabled:function(a){return a.disabled===!1},disabled:function(a){return a.disabled===!0},checked:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&!!a.checked||b==="option"&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},parent:function(a){return!$.pseudos.empty(a)},empty:function(a){var b;a=a.firstChild;while(a){if(a.nodeName>"@"||(b=a.nodeType)===3||b===4)return!1;a=a.nextSibling}return!0},contains:Q(function(a){return function(b){return(b.textContent||b.innerText||bc(b)).indexOf(a)>-1}}),has:Q(function(a){return function(b){return Z(a,b).length>0}}),header:function(a){return I.test(a.nodeName)},text:function(a){var b,c;return a.nodeName.toLowerCase()==="input"&&(b=a.type)==="text"&&((c=a.getAttribute("type"))==null||c.toLowerCase()===b)},radio:R("radio"),checkbox:R("checkbox"),file:R("file"),password:R("password"),image:R("image"),submit:S("submit"),reset:S("reset"),button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&a.type==="button"||b==="button"},input:function(a){return J.test(a.nodeName)},focus:function(a){var b=a.ownerDocument;return a===b.activeElement&&(!b.hasFocus||b.hasFocus())&&(!!a.type||!!a.href)},active:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b,c){return c?a.slice(1):[a[0]]},last:function(a,b,c){var d=a.pop();return c?a:[d]},even:function(a,b,c){var d=[],e=c?1:0,f=a.length;for(;e$.cacheLength&&delete O[P.shift()],g};Z.matches=function(a,b){return Z(a,null,null,b)},Z.matchesSelector=function(a,b){return Z(b,null,null,[a]).length>0};var bm=function(a,b,e,f,g){a=a.replace(A,"$1");var h,i,j,k,l,m,p,q,r,s=a.match(C),t=a.match(E),u=b.nodeType;if(L.POS.test(a))return bf(a,b,e,f,s);if(f)h=n.call(f,0);else if(s&&s.length===1){if(t.length>1&&u===9&&!g&&(s=L.ID.exec(t[0]))){b=$.find.ID(s[1],b,g)[0];if(!b)return e;a=a.slice(t.shift().length)}q=(s=G.exec(t[0]))&&!s.index&&b.parentNode||b,r=t.pop(),m=r.split(":not")[0];for(j=0,k=$.order.length;j",a.querySelectorAll("[selected]").length||e.push("\\["+r+"*(?:checked|disabled|ismap|multiple|readonly|selected|value)"),a.querySelectorAll(":checked").length||e.push(":checked")}),T(function(a){a.innerHTML="

",a.querySelectorAll("[test^='']").length&&e.push("[*^$]="+r+"*(?:\"\"|'')"),a.innerHTML="",a.querySelectorAll(":enabled").length||e.push(":enabled",":disabled")}),e=e.length&&new RegExp(e.join("|")),bm=function(a,d,f,g,h){if(!g&&!h&&(!e||!e.test(a)))if(d.nodeType===9)try{return o.apply(f,n.call(d.querySelectorAll(a),0)),f}catch(i){}else if(d.nodeType===1&&d.nodeName.toLowerCase()!=="object"){var j=d.getAttribute("id"),k=j||q,l=G.test(a)&&d.parentNode||d;j?k=k.replace(c,"\\$&"):d.setAttribute("id",k);try{return o.apply(f,n.call(l.querySelectorAll(a.replace(C,"[id='"+k+"'] $&")),0)),f}catch(i){}finally{j||d.removeAttribute("id")}}return b(a,d,f,g,h)},g&&(T(function(b){a=g.call(b,"div");try{g.call(b,"[test!='']:sizzle"),f.push($.match.PSEUDO)}catch(c){}}),f=new RegExp(f.join("|")),Z.matchesSelector=function(b,c){c=c.replace(d,"='$1']");if(!ba(b)&&!f.test(c)&&(!e||!e.test(c)))try{var h=g.call(b,c);if(h||a||b.document&&b.document.nodeType!==11)return h}catch(i){}return Z(c,null,null,[b]).length>0})}(),Z.attr=p.attr,p.find=Z,p.expr=Z.selectors,p.expr[":"]=p.expr.pseudos,p.unique=Z.uniqueSort,p.text=Z.getText,p.isXMLDoc=Z.isXML,p.contains=Z.contains}(a);var bc=/Until$/,bd=/^(?:parents|prev(?:Until|All))/,be=/^.[^:#\[\.,]*$/,bf=p.expr.match.needsContext,bg={children:!0,contents:!0,next:!0,prev:!0};p.fn.extend({find:function(a){var b,c,d,e,f,g,h=this;if(typeof a!="string")return p(a).filter(function(){for(b=0,c=h.length;b0)for(e=d;e=0:p.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c,d=0,e=this.length,f=[],g=bf.test(a)||typeof a!="string"?p(a,b||this.context):0;for(;d-1:p.find.matchesSelector(c,a)){f.push(c);break}c=c.parentNode}}return f=f.length>1?p.unique(f):f,this.pushStack(f,"closest",a)},index:function(a){return a?typeof a=="string"?p.inArray(this[0],p(a)):p.inArray(a.jquery?a[0]:a,this):this[0]&&this[0].parentNode?this.prevAll().length:-1},add:function(a,b){var c=typeof a=="string"?p(a,b):p.makeArray(a&&a.nodeType?[a]:a),d=p.merge(this.get(),c);return this.pushStack(bh(c[0])||bh(d[0])?d:p.unique(d))},addBack:function(a){return this.add(a==null?this.prevObject:this.prevObject.filter(a))}}),p.fn.andSelf=p.fn.addBack,p.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return p.dir(a,"parentNode")},parentsUntil:function(a,b,c){return p.dir(a,"parentNode",c)},next:function(a){return bi(a,"nextSibling")},prev:function(a){return bi(a,"previousSibling")},nextAll:function(a){return p.dir(a,"nextSibling")},prevAll:function(a){return p.dir(a,"previousSibling")},nextUntil:function(a,b,c){return p.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return p.dir(a,"previousSibling",c)},siblings:function(a){return p.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return p.sibling(a.firstChild)},contents:function(a){return p.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:p.merge([],a.childNodes)}},function(a,b){p.fn[a]=function(c,d){var e=p.map(this,b,c);return bc.test(a)||(d=c),d&&typeof d=="string"&&(e=p.filter(d,e)),e=this.length>1&&!bg[a]?p.unique(e):e,this.length>1&&bd.test(a)&&(e=e.reverse()),this.pushStack(e,a,k.call(arguments).join(","))}}),p.extend({filter:function(a,b,c){return c&&(a=":not("+a+")"),b.length===1?p.find.matchesSelector(b[0],a)?[b[0]]:[]:p.find.matches(a,b)},dir:function(a,c,d){var e=[],f=a[c];while(f&&f.nodeType!==9&&(d===b||f.nodeType!==1||!p(f).is(d)))f.nodeType===1&&e.push(f),f=f[c];return e},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var bl="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",bm=/ jQuery\d+="(?:null|\d+)"/g,bn=/^\s+/,bo=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/gi,bp=/<([\w:]+)/,bq=/]","i"),bv=/^(?:checkbox|radio)$/,bw=/checked\s*(?:[^=]|=\s*.checked.)/i,bx=/\/(java|ecma)script/i,by=/^\s*\s*$/g,bz={option:[1,""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bA=bk(e),bB=bA.appendChild(e.createElement("div"));bz.optgroup=bz.option,bz.tbody=bz.tfoot=bz.colgroup=bz.caption=bz.thead,bz.th=bz.td,p.support.htmlSerialize||(bz._default=[1,"X
","
"]),p.fn.extend({text:function(a){return p.access(this,function(a){return a===b?p.text(this):this.empty().append((this[0]&&this[0].ownerDocument||e).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(p.isFunction(a))return this.each(function(b){p(this).wrapAll(a.call(this,b))});if(this[0]){var b=p(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){return p.isFunction(a)?this.each(function(b){p(this).wrapInner(a.call(this,b))}):this.each(function(){var b=p(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=p.isFunction(a);return this.each(function(c){p(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){p.nodeName(this,"body")||p(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){(this.nodeType===1||this.nodeType===11)&&this.insertBefore(a,this.firstChild)})},before:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(a,this),"before",this.selector)}},after:function(){if(!bh(this[0]))return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=p.clean(arguments);return this.pushStack(p.merge(this,a),"after",this.selector)}},remove:function(a,b){var c,d=0;for(;(c=this[d])!=null;d++)if(!a||p.filter(a,[c]).length)!b&&c.nodeType===1&&(p.cleanData(c.getElementsByTagName("*")),p.cleanData([c])),c.parentNode&&c.parentNode.removeChild(c);return this},empty:function(){var a,b=0;for(;(a=this[b])!=null;b++){a.nodeType===1&&p.cleanData(a.getElementsByTagName("*"));while(a.firstChild)a.removeChild(a.firstChild)}return this},clone:function(a,b){return a=a==null?!1:a,b=b==null?a:b,this.map(function(){return p.clone(this,a,b)})},html:function(a){return p.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(bm,""):b;if(typeof a=="string"&&!bs.test(a)&&(p.support.htmlSerialize||!bu.test(a))&&(p.support.leadingWhitespace||!bn.test(a))&&!bz[(bp.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(bo,"<$1>");try{for(;d1&&typeof j=="string"&&bw.test(j))return this.each(function(){p(this).domManip(a,c,d)});if(p.isFunction(j))return this.each(function(e){var f=p(this);a[0]=j.call(this,e,c?f.html():b),f.domManip(a,c,d)});if(this[0]){e=p.buildFragment(a,this,k),g=e.fragment,f=g.firstChild,g.childNodes.length===1&&(g=f);if(f){c=c&&p.nodeName(f,"tr");for(h=e.cacheable||l-1;i0?this.clone(!0):this).get(),p(g[e])[b](d),f=f.concat(d);return this.pushStack(f,a,g.selector)}}),p.extend({clone:function(a,b,c){var d,e,f,g;p.support.html5Clone||p.isXMLDoc(a)||!bu.test("<"+a.nodeName+">")?g=a.cloneNode(!0):(bB.innerHTML=a.outerHTML,bB.removeChild(g=bB.firstChild));if((!p.support.noCloneEvent||!p.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!p.isXMLDoc(a)){bE(a,g),d=bF(a),e=bF(g);for(f=0;d[f];++f)e[f]&&bE(d[f],e[f])}if(b){bD(a,g);if(c){d=bF(a),e=bF(g);for(f=0;d[f];++f)bD(d[f],e[f])}}return d=e=null,g},clean:function(a,b,c,d){var f,g,h,i,j,k,l,m,n,o,q,r,s=0,t=[];if(!b||typeof b.createDocumentFragment=="undefined")b=e;for(g=b===e&&bA;(h=a[s])!=null;s++){typeof h=="number"&&(h+="");if(!h)continue;if(typeof h=="string")if(!br.test(h))h=b.createTextNode(h);else{g=g||bk(b),l=l||g.appendChild(b.createElement("div")),h=h.replace(bo,"<$1>"),i=(bp.exec(h)||["",""])[1].toLowerCase(),j=bz[i]||bz._default,k=j[0],l.innerHTML=j[1]+h+j[2];while(k--)l=l.lastChild;if(!p.support.tbody){m=bq.test(h),n=i==="table"&&!m?l.firstChild&&l.firstChild.childNodes:j[1]===""&&!m?l.childNodes:[];for(f=n.length-1;f>=0;--f)p.nodeName(n[f],"tbody")&&!n[f].childNodes.length&&n[f].parentNode.removeChild(n[f])}!p.support.leadingWhitespace&&bn.test(h)&&l.insertBefore(b.createTextNode(bn.exec(h)[0]),l.firstChild),h=l.childNodes,l=g.lastChild}h.nodeType?t.push(h):t=p.merge(t,h)}l&&(g.removeChild(l),h=l=g=null);if(!p.support.appendChecked)for(s=0;(h=t[s])!=null;s++)p.nodeName(h,"input")?bG(h):typeof h.getElementsByTagName!="undefined"&&p.grep(h.getElementsByTagName("input"),bG);if(c){q=function(a){if(!a.type||bx.test(a.type))return d?d.push(a.parentNode?a.parentNode.removeChild(a):a):c.appendChild(a)};for(s=0;(h=t[s])!=null;s++)if(!p.nodeName(h,"script")||!q(h))c.appendChild(h),typeof h.getElementsByTagName!="undefined"&&(r=p.grep(p.merge([],h.getElementsByTagName("script")),q),t.splice.apply(t,[s+1,0].concat(r)),s+=r.length)}return t},cleanData:function(a,b){var c,d,e,f,g=0,h=p.expando,i=p.cache,j=p.support.deleteExpando,k=p.event.special;for(;(e=a[g])!=null;g++)if(b||p.acceptData(e)){d=e[h],c=d&&i[d];if(c){if(c.events)for(f in c.events)k[f]?p.event.remove(e,f):p.removeEvent(e,f,c.handle);i[d]&&(delete i[d],j?delete e[h]:e.removeAttribute?e.removeAttribute(h):e[h]=null,p.deletedIds.push(d))}}}}),function(){var a,b;p.uaMatch=function(a){a=a.toLowerCase();var b=/(chrome)[ \/]([\w.]+)/.exec(a)||/(webkit)[ \/]([\w.]+)/.exec(a)||/(opera)(?:.*version|)[ \/]([\w.]+)/.exec(a)||/(msie) ([\w.]+)/.exec(a)||a.indexOf("compatible")<0&&/(mozilla)(?:.*? rv:([\w.]+)|)/.exec(a)||[];return{browser:b[1]||"",version:b[2]||"0"}},a=p.uaMatch(g.userAgent),b={},a.browser&&(b[a.browser]=!0,b.version=a.version),b.webkit&&(b.safari=!0),p.browser=b,p.sub=function(){function a(b,c){return new a.fn.init(b,c)}p.extend(!0,a,this),a.superclass=this,a.fn=a.prototype=this(),a.fn.constructor=a,a.sub=this.sub,a.fn.init=function c(c,d){return d&&d instanceof p&&!(d instanceof a)&&(d=a(d)),p.fn.init.call(this,c,d,b)},a.fn.init.prototype=a.fn;var b=a(e);return a}}();var bH,bI,bJ,bK=/alpha\([^)]*\)/i,bL=/opacity=([^)]*)/,bM=/^(top|right|bottom|left)$/,bN=/^margin/,bO=new RegExp("^("+q+")(.*)$","i"),bP=new RegExp("^("+q+")(?!px)[a-z%]+$","i"),bQ=new RegExp("^([-+])=("+q+")","i"),bR={},bS={position:"absolute",visibility:"hidden",display:"block"},bT={letterSpacing:0,fontWeight:400,lineHeight:1},bU=["Top","Right","Bottom","Left"],bV=["Webkit","O","Moz","ms"],bW=p.fn.toggle;p.fn.extend({css:function(a,c){return p.access(this,function(a,c,d){return d!==b?p.style(a,c,d):p.css(a,c)},a,c,arguments.length>1)},show:function(){return bZ(this,!0)},hide:function(){return bZ(this)},toggle:function(a,b){var c=typeof a=="boolean";return p.isFunction(a)&&p.isFunction(b)?bW.apply(this,arguments):this.each(function(){(c?a:bY(this))?p(this).show():p(this).hide()})}}),p.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=bH(a,"opacity");return c===""?"1":c}}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":p.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!a||a.nodeType===3||a.nodeType===8||!a.style)return;var f,g,h,i=p.camelCase(c),j=a.style;c=p.cssProps[i]||(p.cssProps[i]=bX(j,i)),h=p.cssHooks[c]||p.cssHooks[i];if(d===b)return h&&"get"in h&&(f=h.get(a,!1,e))!==b?f:j[c];g=typeof d,g==="string"&&(f=bQ.exec(d))&&(d=(f[1]+1)*f[2]+parseFloat(p.css(a,c)),g="number");if(d==null||g==="number"&&isNaN(d))return;g==="number"&&!p.cssNumber[i]&&(d+="px");if(!h||!("set"in h)||(d=h.set(a,d,e))!==b)try{j[c]=d}catch(k){}},css:function(a,c,d,e){var f,g,h,i=p.camelCase(c);return c=p.cssProps[i]||(p.cssProps[i]=bX(a.style,i)),h=p.cssHooks[c]||p.cssHooks[i],h&&"get"in h&&(f=h.get(a,!0,e)),f===b&&(f=bH(a,c)),f==="normal"&&c in bT&&(f=bT[c]),d||e!==b?(g=parseFloat(f),d||p.isNumeric(g)?g||0:f):f},swap:function(a,b,c){var d,e,f={};for(e in b)f[e]=a.style[e],a.style[e]=b[e];d=c.call(a);for(e in b)a.style[e]=f[e];return d}}),a.getComputedStyle?bH=function(a,b){var c,d,e,f,g=getComputedStyle(a,null),h=a.style;return g&&(c=g[b],c===""&&!p.contains(a.ownerDocument.documentElement,a)&&(c=p.style(a,b)),bP.test(c)&&bN.test(b)&&(d=h.width,e=h.minWidth,f=h.maxWidth,h.minWidth=h.maxWidth=h.width=c,c=g.width,h.width=d,h.minWidth=e,h.maxWidth=f)),c}:e.documentElement.currentStyle&&(bH=function(a,b){var c,d,e=a.currentStyle&&a.currentStyle[b],f=a.style;return e==null&&f&&f[b]&&(e=f[b]),bP.test(e)&&!bM.test(b)&&(c=f.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),f.left=b==="fontSize"?"1em":e,e=f.pixelLeft+"px",f.left=c,d&&(a.runtimeStyle.left=d)),e===""?"auto":e}),p.each(["height","width"],function(a,b){p.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0||bH(a,"display")!=="none"?ca(a,b,d):p.swap(a,bS,function(){return ca(a,b,d)})},set:function(a,c,d){return b$(a,c,d?b_(a,b,d,p.support.boxSizing&&p.css(a,"boxSizing")==="border-box"):0)}}}),p.support.opacity||(p.cssHooks.opacity={get:function(a,b){return bL.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?.01*parseFloat(RegExp.$1)+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=p.isNumeric(b)?"alpha(opacity="+b*100+")":"",f=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&p.trim(f.replace(bK,""))===""&&c.removeAttribute){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bK.test(f)?f.replace(bK,e):f+" "+e}}),p(function(){p.support.reliableMarginRight||(p.cssHooks.marginRight={get:function(a,b){return p.swap(a,{display:"inline-block"},function(){if(b)return bH(a,"marginRight")})}}),!p.support.pixelPosition&&p.fn.position&&p.each(["top","left"],function(a,b){p.cssHooks[b]={get:function(a,c){if(c){var d=bH(a,b);return bP.test(d)?p(a).position()[b]+"px":d}}}})}),p.expr&&p.expr.filters&&(p.expr.filters.hidden=function(a){return a.offsetWidth===0&&a.offsetHeight===0||!p.support.reliableHiddenOffsets&&(a.style&&a.style.display||bH(a,"display"))==="none"},p.expr.filters.visible=function(a){return!p.expr.filters.hidden(a)}),p.each({margin:"",padding:"",border:"Width"},function(a,b){p.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bU[d]+b]=e[d]||e[d-2]||e[0];return f}},bN.test(a)||(p.cssHooks[a+b].set=b$)});var cc=/%20/g,cd=/\[\]$/,ce=/\r?\n/g,cf=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,cg=/^(?:select|textarea)/i;p.fn.extend({serialize:function(){return p.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?p.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||cg.test(this.nodeName)||cf.test(this.type))}).map(function(a,b){var c=p(this).val();return c==null?null:p.isArray(c)?p.map(c,function(a,c){return{name:b.name,value:a.replace(ce,"\r\n")}}):{name:b.name,value:c.replace(ce,"\r\n")}}).get()}}),p.param=function(a,c){var d,e=[],f=function(a,b){b=p.isFunction(b)?b():b==null?"":b,e[e.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=p.ajaxSettings&&p.ajaxSettings.traditional);if(p.isArray(a)||a.jquery&&!p.isPlainObject(a))p.each(a,function(){f(this.name,this.value)});else for(d in a)ch(d,a[d],c,f);return e.join("&").replace(cc,"+")};var ci,cj,ck=/#.*$/,cl=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,cm=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,cn=/^(?:GET|HEAD)$/,co=/^\/\//,cp=/\?/,cq=/)<[^<]*)*<\/script>/gi,cr=/([?&])_=[^&]*/,cs=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+)|)|)/,ct=p.fn.load,cu={},cv={},cw=["*/"]+["*"];try{ci=f.href}catch(cx){ci=e.createElement("a"),ci.href="",ci=ci.href}cj=cs.exec(ci.toLowerCase())||[],p.fn.load=function(a,c,d){if(typeof a!="string"&&ct)return ct.apply(this,arguments);if(!this.length)return this;var e,f,g,h=this,i=a.indexOf(" ");return i>=0&&(e=a.slice(i,a.length),a=a.slice(0,i)),p.isFunction(c)?(d=c,c=b):typeof c=="object"&&(f="POST"),p.ajax({url:a,type:f,dataType:"html",data:c,complete:function(a,b){d&&h.each(d,g||[a.responseText,b,a])}}).done(function(a){g=arguments,h.html(e?p("
").append(a.replace(cq,"")).find(e):a)}),this},p.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){p.fn[b]=function(a){return this.on(b,a)}}),p.each(["get","post"],function(a,c){p[c]=function(a,d,e,f){return p.isFunction(d)&&(f=f||e,e=d,d=b),p.ajax({type:c,url:a,data:d,success:e,dataType:f})}}),p.extend({getScript:function(a,c){return p.get(a,b,c,"script")},getJSON:function(a,b,c){return p.get(a,b,c,"json")},ajaxSetup:function(a,b){return b?cA(a,p.ajaxSettings):(b=a,a=p.ajaxSettings),cA(a,b),a},ajaxSettings:{url:ci,isLocal:cm.test(cj[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":cw},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":p.parseJSON,"text xml":p.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:cy(cu),ajaxTransport:cy(cv),ajax:function(a,c){function y(a,c,f,i){var k,s,t,u,w,y=c;if(v===2)return;v=2,h&&clearTimeout(h),g=b,e=i||"",x.readyState=a>0?4:0,f&&(u=cB(l,x,f));if(a>=200&&a<300||a===304)l.ifModified&&(w=x.getResponseHeader("Last-Modified"),w&&(p.lastModified[d]=w),w=x.getResponseHeader("Etag"),w&&(p.etag[d]=w)),a===304?(y="notmodified",k=!0):(k=cC(l,u),y=k.state,s=k.data,t=k.error,k=!t);else{t=y;if(!y||a)y="error",a<0&&(a=0)}x.status=a,x.statusText=""+(c||y),k?o.resolveWith(m,[s,y,x]):o.rejectWith(m,[x,y,t]),x.statusCode(r),r=b,j&&n.trigger("ajax"+(k?"Success":"Error"),[x,l,k?s:t]),q.fireWith(m,[x,y]),j&&(n.trigger("ajaxComplete",[x,l]),--p.active||p.event.trigger("ajaxStop"))}typeof a=="object"&&(c=a,a=b),c=c||{};var d,e,f,g,h,i,j,k,l=p.ajaxSetup({},c),m=l.context||l,n=m!==l&&(m.nodeType||m instanceof p)?p(m):p.event,o=p.Deferred(),q=p.Callbacks("once memory"),r=l.statusCode||{},t={},u={},v=0,w="canceled",x={readyState:0,setRequestHeader:function(a,b){if(!v){var c=a.toLowerCase();a=u[c]=u[c]||a,t[a]=b}return this},getAllResponseHeaders:function(){return v===2?e:null},getResponseHeader:function(a){var c;if(v===2){if(!f){f={};while(c=cl.exec(e))f[c[1].toLowerCase()]=c[2]}c=f[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){return v||(l.mimeType=a),this},abort:function(a){return a=a||w,g&&g.abort(a),y(0,a),this}};o.promise(x),x.success=x.done,x.error=x.fail,x.complete=q.add,x.statusCode=function(a){if(a){var b;if(v<2)for(b in a)r[b]=[r[b],a[b]];else b=a[x.status],x.always(b)}return this},l.url=((a||l.url)+"").replace(ck,"").replace(co,cj[1]+"//"),l.dataTypes=p.trim(l.dataType||"*").toLowerCase().split(s),l.crossDomain==null&&(i=cs.exec(l.url.toLowerCase()),l.crossDomain=!(!i||i[1]==cj[1]&&i[2]==cj[2]&&(i[3]||(i[1]==="http:"?80:443))==(cj[3]||(cj[1]==="http:"?80:443)))),l.data&&l.processData&&typeof l.data!="string"&&(l.data=p.param(l.data,l.traditional)),cz(cu,l,c,x);if(v===2)return x;j=l.global,l.type=l.type.toUpperCase(),l.hasContent=!cn.test(l.type),j&&p.active++===0&&p.event.trigger("ajaxStart");if(!l.hasContent){l.data&&(l.url+=(cp.test(l.url)?"&":"?")+l.data,delete l.data),d=l.url;if(l.cache===!1){var z=p.now(),A=l.url.replace(cr,"$1_="+z);l.url=A+(A===l.url?(cp.test(l.url)?"&":"?")+"_="+z:"")}}(l.data&&l.hasContent&&l.contentType!==!1||c.contentType)&&x.setRequestHeader("Content-Type",l.contentType),l.ifModified&&(d=d||l.url,p.lastModified[d]&&x.setRequestHeader("If-Modified-Since",p.lastModified[d]),p.etag[d]&&x.setRequestHeader("If-None-Match",p.etag[d])),x.setRequestHeader("Accept",l.dataTypes[0]&&l.accepts[l.dataTypes[0]]?l.accepts[l.dataTypes[0]]+(l.dataTypes[0]!=="*"?", "+cw+"; q=0.01":""):l.accepts["*"]);for(k in l.headers)x.setRequestHeader(k,l.headers[k]);if(!l.beforeSend||l.beforeSend.call(m,x,l)!==!1&&v!==2){w="abort";for(k in{success:1,error:1,complete:1})x[k](l[k]);g=cz(cv,l,c,x);if(!g)y(-1,"No Transport");else{x.readyState=1,j&&n.trigger("ajaxSend",[x,l]),l.async&&l.timeout>0&&(h=setTimeout(function(){x.abort("timeout")},l.timeout));try{v=1,g.send(t,y)}catch(B){if(v<2)y(-1,B);else throw B}}return x}return x.abort()},active:0,lastModified:{},etag:{}});var cD=[],cE=/\?/,cF=/(=)\?(?=&|$)|\?\?/,cG=p.now();p.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var a=cD.pop()||p.expando+"_"+cG++;return this[a]=!0,a}}),p.ajaxPrefilter("json jsonp",function(c,d,e){var f,g,h,i=c.data,j=c.url,k=c.jsonp!==!1,l=k&&cF.test(j),m=k&&!l&&typeof i=="string"&&!(c.contentType||"").indexOf("application/x-www-form-urlencoded")&&cF.test(i);if(c.dataTypes[0]==="jsonp"||l||m)return f=c.jsonpCallback=p.isFunction(c.jsonpCallback)?c.jsonpCallback():c.jsonpCallback,g=a[f],l?c.url=j.replace(cF,"$1"+f):m?c.data=i.replace(cF,"$1"+f):k&&(c.url+=(cE.test(j)?"&":"?")+c.jsonp+"="+f),c.converters["script json"]=function(){return h||p.error(f+" was not called"),h[0]},c.dataTypes[0]="json",a[f]=function(){h=arguments},e.always(function(){a[f]=g,c[f]&&(c.jsonpCallback=d.jsonpCallback,cD.push(f)),h&&p.isFunction(g)&&g(h[0]),h=g=b}),"script"}),p.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){return p.globalEval(a),a}}}),p.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),p.ajaxTransport("script",function(a){if(a.crossDomain){var c,d=e.head||e.getElementsByTagName("head")[0]||e.documentElement;return{send:function(f,g){c=e.createElement("script"),c.async="async",a.scriptCharset&&(c.charset=a.scriptCharset),c.src=a.url,c.onload=c.onreadystatechange=function(a,e){if(e||!c.readyState||/loaded|complete/.test(c.readyState))c.onload=c.onreadystatechange=null,d&&c.parentNode&&d.removeChild(c),c=b,e||g(200,"success")},d.insertBefore(c,d.firstChild)},abort:function(){c&&c.onload(0,1)}}}});var cH,cI=a.ActiveXObject?function(){for(var a in cH)cH[a](0,1)}:!1,cJ=0;p.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&cK()||cL()}:cK,function(a){p.extend(p.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(p.ajaxSettings.xhr()),p.support.ajax&&p.ajaxTransport(function(c){if(!c.crossDomain||p.support.cors){var d;return{send:function(e,f){var g,h,i=c.xhr();c.username?i.open(c.type,c.url,c.async,c.username,c.password):i.open(c.type,c.url,c.async);if(c.xhrFields)for(h in c.xhrFields)i[h]=c.xhrFields[h];c.mimeType&&i.overrideMimeType&&i.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(h in e)i.setRequestHeader(h,e[h])}catch(j){}i.send(c.hasContent&&c.data||null),d=function(a,e){var h,j,k,l,m;try{if(d&&(e||i.readyState===4)){d=b,g&&(i.onreadystatechange=p.noop,cI&&delete cH[g]);if(e)i.readyState!==4&&i.abort();else{h=i.status,k=i.getAllResponseHeaders(),l={},m=i.responseXML,m&&m.documentElement&&(l.xml=m);try{l.text=i.responseText}catch(a){}try{j=i.statusText}catch(n){j=""}!h&&c.isLocal&&!c.crossDomain?h=l.text?200:404:h===1223&&(h=204)}}}catch(o){e||f(-1,o)}l&&f(h,j,l,k)},c.async?i.readyState===4?setTimeout(d,0):(g=++cJ,cI&&(cH||(cH={},p(a).unload(cI)),cH[g]=d),i.onreadystatechange=d):d()},abort:function(){d&&d(0,1)}}}});var cM,cN,cO=/^(?:toggle|show|hide)$/,cP=new RegExp("^(?:([-+])=|)("+q+")([a-z%]*)$","i"),cQ=/queueHooks$/,cR=[cX],cS={"*":[function(a,b){var c,d,e,f=this.createTween(a,b),g=cP.exec(b),h=f.cur(),i=+h||0,j=1;if(g){c=+g[2],d=g[3]||(p.cssNumber[a]?"":"px");if(d!=="px"&&i){i=p.css(f.elem,a,!0)||c||1;do e=j=j||".5",i=i/j,p.style(f.elem,a,i+d),j=f.cur()/h;while(j!==1&&j!==e)}f.unit=d,f.start=i,f.end=g[1]?i+(g[1]+1)*c:c}return f}]};p.Animation=p.extend(cV,{tweener:function(a,b){p.isFunction(a)?(b=a,a=["*"]):a=a.split(" ");var c,d=0,e=a.length;for(;d-1,j={},k={},l,m;i?(k=e.position(),l=k.top,m=k.left):(l=parseFloat(g)||0,m=parseFloat(h)||0),p.isFunction(b)&&(b=b.call(a,c,f)),b.top!=null&&(j.top=b.top-f.top+l),b.left!=null&&(j.left=b.left-f.left+m),"using"in b?b.using.call(a,j):e.css(j)}},p.fn.extend({position:function(){if(!this[0])return;var a=this[0],b=this.offsetParent(),c=this.offset(),d=c$.test(b[0].nodeName)?{top:0,left:0}:b.offset();return c.top-=parseFloat(p.css(a,"marginTop"))||0,c.left-=parseFloat(p.css(a,"marginLeft"))||0,d.top+=parseFloat(p.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(p.css(b[0],"borderLeftWidth"))||0,{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||e.body;while(a&&!c$.test(a.nodeName)&&p.css(a,"position")==="static")a=a.offsetParent;return a||e.body})}}),p.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);p.fn[a]=function(e){return p.access(this,function(a,e,f){var g=c_(a);if(f===b)return g?c in g?g[c]:g.document.documentElement[e]:a[e];g?g.scrollTo(d?p(g).scrollLeft():f,d?f:p(g).scrollTop()):a[e]=f},a,e,arguments.length,null)}}),p.each({Height:"height",Width:"width"},function(a,c){p.each({padding:"inner"+a,content:c,"":"outer"+a},function(d,e){p.fn[e]=function(e,f){var g=arguments.length&&(d||typeof e!="boolean"),h=d||(e===!0||f===!0?"margin":"border");return p.access(this,function(c,d,e){var f;return p.isWindow(c)?c.document.documentElement["client"+a]:c.nodeType===9?(f=c.documentElement,Math.max(c.body["scroll"+a],f["scroll"+a],c.body["offset"+a],f["offset"+a],f["client"+a])):e===b?p.css(c,d,e,h):p.style(c,d,e,h)},c,g?e:b,g)}})}),a.jQuery=a.$=p,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return p})})(window); -------------------------------------------------------------------------------- /public/scripts/vibelog-edit.js: -------------------------------------------------------------------------------- 1 | function previewUpdate() 2 | { 3 | var enabled = $('#preview-checkbox').is(':checked'); 4 | if( enabled ) 5 | { 6 | var message = $('#message'); 7 | var preview = $('#message-preview'); 8 | var filters = $('#filters-field').val(); 9 | 10 | if (filters != "") 11 | { 12 | $.get(window.rootDir + 'filter', {message: message.val(), filters: filters}, function(data){ preview.html(data) }); 13 | } 14 | else 15 | { 16 | preview.html(message.val()); 17 | } 18 | 19 | preview.height(message.height()); 20 | message.hide(); 21 | preview.show(); 22 | } 23 | else 24 | { 25 | $('#message').show(); 26 | $('#message-preview').hide(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/styles/common.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | font-family: sans-serif; 5 | font-size: 10pt; 6 | background-color: #fdfdfd; 7 | } 8 | 9 | body > header { 10 | padding: 1px; 11 | color: white; 12 | text-align: center; 13 | background-color: #841; 14 | background: linear-gradient(#952, #841); 15 | border-bottom: 1px solid #420; 16 | box-shadow: 0 10px 40px #aaa; 17 | } 18 | 19 | a { 20 | color: black; 21 | } 22 | 23 | h1, h2 { 24 | font-weight: lighter; 25 | } 26 | 27 | h1 a, h2 a { text-decoration: none; } 28 | h1 a:hover, h2 a:hover { text-decoration: underline; } 29 | 30 | table { width: 100%; } 31 | col.caption { width: 150pt; } 32 | input { width: 100%; } 33 | input.checkbox { width: auto; } 34 | textarea { width: 100%; resize: vertical; } 35 | 36 | h1 { 37 | font-size: 24pt; 38 | } 39 | 40 | .vibelog-login-form { 41 | display: flex; 42 | max-width: 300pt; 43 | justify-content: flex-end; 44 | align-items: stretch; 45 | position: absolute; 46 | right: 0; 47 | } 48 | .vibelog-login-form input { 49 | background-color: rgba(255,255,255,0.2); 50 | border: 1px solid rgba(255,255,255,0.3); 51 | margin: 0; 52 | margin-right: 4pt; 53 | } 54 | 55 | .vibelog-login-form button { 56 | flex-basis: auto; 57 | flex-shrink: 0; 58 | } 59 | 60 | .vibelog-user-area { position: absolute; right: 0; } 61 | .vibelog-user-area p { margin: 0.5em; } 62 | .vibelog-user-area a { color: #da8; text-decoration: none; } 63 | 64 | 65 | #vibelogContent { 66 | margin: 0; 67 | padding: 0; 68 | max-width: 600pt; 69 | margin: 0 auto; 70 | /*box-shadow: 0 0 100px #888;*/ 71 | } 72 | 73 | #vibelogContent .blogMultiPost, #vibelogContent .blogSinglePost { 74 | border: 1px solid #888; 75 | border-top: 5pt solid #222; 76 | background-color: white; 77 | margin-top: 30pt; 78 | padding: 0; 79 | box-shadow: 0 10px 40px #aaa; 80 | } 81 | 82 | #vibelogContent .blogMultiPost h2 { 83 | font-size: 18pt; 84 | } 85 | 86 | #vibelogContent .headerImage { margin-right: 1em; } 87 | 88 | #vibelogContent .blogPostHeaderContent { 89 | display: flex; 90 | flex-wrap: wrap; 91 | align-items: flex-start; 92 | } 93 | 94 | #vibelogContent .blogPostHeaderText { 95 | flex-basis: 0; 96 | flex-grow: 1; 97 | } 98 | 99 | #vibelogContent .blogPostDate { 100 | position: absolute; 101 | display: block; 102 | left: 20pt; 103 | top: -13pt; 104 | width: 100pt; 105 | padding: 5pt; 106 | color: white; 107 | background-color: #841; 108 | background: linear-gradient(#952, #841); 109 | text-align: center; 110 | } 111 | 112 | #vibelogContent .blogPost header { 113 | position: relative; 114 | padding: 10pt 20pt 20pt 20pt; 115 | background-color: #fff4ee; 116 | background: linear-gradient(#fffaf8, #f4f0ee); 117 | border-bottom: 1px solid #e0e0e0; 118 | } 119 | 120 | #vibelogContent .blogPostContent { 121 | padding: 10pt 20pt; 122 | } 123 | 124 | #vibelogContent .blogPost footer { 125 | padding: 5pt 20pt; 126 | text-align: center; 127 | color: #bbb; 128 | background-color: #222; 129 | border-top: 1px solid #000; 130 | } 131 | 132 | #vibelogContent .blogPost footer p { 133 | margin: 0; 134 | } 135 | 136 | #vibelogContent .blogPost footer a { 137 | color: #ddd; 138 | } 139 | 140 | #vibelogContent .blogPostHeaderContent h1 { 141 | flex-basis: 100%; 142 | } 143 | 144 | #vibelogRecentList { 145 | float: right; 146 | margin-right: 10pt; 147 | padding: 10pt; 148 | padding-bottom: 0; 149 | background-color: rgba(255, 255, 255, 0.75); 150 | border: 1px solid rgba(200, 200, 200, 0.75); 151 | border-top: none; 152 | } 153 | 154 | #vibelogRecentList ul { 155 | margin: 0; 156 | padding: 0; 157 | } 158 | 159 | #vibelogRecentList ul > li { 160 | font-weight: bold; 161 | list-style: none; 162 | margin-bottom: 10pt; 163 | } 164 | 165 | #vibelogRecentList ul ul > li { 166 | font-weight: normal; 167 | margin-bottom: 0; 168 | } 169 | 170 | #vibelogContent section.comments { 171 | padding: 10pt 20pt; 172 | } 173 | -------------------------------------------------------------------------------- /public/styles/menu.css: -------------------------------------------------------------------------------- 1 | ul { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | outline: 0; 6 | list-style:none; text-decoration:none; 7 | } 8 | 9 | #menuContainer { 10 | margin: 30pt 0 0 0; 11 | background-color:#413b3b; 12 | text-align: center; 13 | } 14 | 15 | .headerMenu { 16 | height:30px; 17 | position:relative; 18 | display:table; 19 | margin:0 auto; 20 | } 21 | 22 | .headerMenu a { 23 | width:auto; 24 | height:30px; 25 | float:left; 26 | font-size:12px; 27 | color:white; 28 | padding-left:18px; 29 | padding-right:18px; 30 | line-height:30px; 31 | text-decoration: none; 32 | } 33 | 34 | .headerMenu a:hover { 35 | background-color: #f57e20; 36 | } 37 | 38 | .headerMenu li { 39 | width:auto; 40 | height:30px; 41 | float:left; 42 | position:relative; 43 | border-right:solid 1px #fdfdfd; 44 | display: table-cell; 45 | } 46 | 47 | .headerMenu li:last-child { 48 | border-right: none; 49 | } 50 | -------------------------------------------------------------------------------- /source/app.d: -------------------------------------------------------------------------------- 1 | import vibe.http.router; 2 | import vibe.http.server; 3 | import vibe.http.session : MemorySessionStore; 4 | 5 | import vibelog.controller; 6 | import vibelog.web; 7 | import vibelog.webadmin; 8 | 9 | shared static this() 10 | { 11 | //setLogLevel(LogLevel.Trace); 12 | 13 | auto router = new URLRouter; 14 | 15 | auto blogsettings = new VibeLogSettings; 16 | blogsettings.configName = "example"; 17 | blogsettings.siteURL = URL("http://localhost:8080/"); 18 | blogsettings.blogName = "VibeLog"; 19 | blogsettings.blogDescription = "Publishing software utilizing the vibe.d framework"; 20 | 21 | auto ctrl = new VibeLogController(blogsettings); 22 | router.registerVibeLogWeb(ctrl); 23 | router.registerVibeLogWebAdmin(ctrl); 24 | 25 | auto settings = new HTTPServerSettings; 26 | settings.port = 8080; 27 | settings.bindAddresses = ["127.0.0.1"]; 28 | settings.sessionStore = new MemorySessionStore; 29 | listenHTTP(settings, router); 30 | } 31 | -------------------------------------------------------------------------------- /source/vibelog/config.d: -------------------------------------------------------------------------------- 1 | module vibelog.config; 2 | 3 | import vibe.data.bson; 4 | 5 | final class Config { 6 | BsonObjectID id; 7 | string name; 8 | string[] categories; 9 | string language = "en-us"; 10 | string copyrightString; 11 | string mailServer; 12 | string feedTitle; 13 | string feedLink; 14 | string feedDescription; 15 | string feedImageTitle; 16 | string feedImageUrl; 17 | 18 | this() 19 | { 20 | id = BsonObjectID.generate(); 21 | } 22 | 23 | @property string[] groups() const { return ["admin"]; } 24 | 25 | bool hasCategory(string cat) const { 26 | foreach( c; categories ) 27 | if( c == cat ) 28 | return true; 29 | return false; 30 | } 31 | 32 | static Config fromBson(Bson bson) 33 | { 34 | auto ret = new Config; 35 | ret.id = bson["_id"].opt!BsonObjectID(); 36 | ret.name = bson["name"].opt!string(); 37 | foreach( grp; cast(Bson[])bson["categories"] ) 38 | ret.categories ~= grp.opt!string(); 39 | ret.language = bson["language"].opt!string(language.init); 40 | ret.copyrightString = bson["copyrightString"].opt!string(); 41 | ret.mailServer = bson["mailServer"].opt!string(); 42 | ret.feedTitle = bson["feedTitle"].opt!string(); 43 | ret.feedLink = bson["feedLink"].opt!string(); 44 | ret.feedDescription = bson["feedDescription"].opt!string(); 45 | ret.feedImageTitle = bson["feedImageTitle"].opt!string(); 46 | ret.feedImageUrl = bson["feedImageUrl"].opt!string(); 47 | return ret; 48 | } 49 | 50 | Bson toBson() 51 | const { 52 | Bson[] bcategories; 53 | foreach( grp; categories ) 54 | bcategories ~= Bson(grp); 55 | 56 | // Create a default category if none is specified 57 | if(bcategories.length < 1) 58 | { 59 | bcategories ~= Bson("general"); 60 | } 61 | 62 | // Could use a switch here 63 | Bson[string] ret; 64 | ret["_id"] = Bson(id); 65 | ret["name"] = Bson(name); 66 | ret["categories"] = Bson(bcategories); 67 | ret["language"] = Bson(language); 68 | ret["copyrightString"] = Bson(copyrightString); 69 | ret["mailServer"] = Bson(mailServer); 70 | ret["feedTitle"] = Bson(feedTitle); 71 | ret["feedLink"] = Bson(feedLink); 72 | ret["feedDescription"] = Bson(feedDescription); 73 | ret["feedImageTitle"] = Bson(feedImageTitle); 74 | ret["feedImageUrl"] = Bson(feedImageUrl); 75 | 76 | return Bson(ret); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /source/vibelog/controller.d: -------------------------------------------------------------------------------- 1 | module vibelog.controller; 2 | 3 | public import vibelog.settings; 4 | 5 | import vibelog.db.dbcontroller; 6 | import diskuto.commentstore : DiskutoCommentStore; 7 | import diskuto.commentstores.mongodb; 8 | 9 | import std.conv : to; 10 | import vibe.data.bson : BsonObjectID; 11 | 12 | 13 | class VibeLogController { 14 | private { 15 | DBController m_db; 16 | VibeLogSettings m_settings; 17 | Config m_config; 18 | DiskutoCommentStore m_diskuto; 19 | } 20 | 21 | this(VibeLogSettings settings) 22 | { 23 | m_settings = settings; 24 | m_diskuto = new MongoDBCommentStore(settings.databaseURL); 25 | m_db = createDBController(settings); 26 | 27 | try m_config = m_db.getConfig(settings.configName, true); 28 | catch( Exception e ){ 29 | import vibe.core.log; 30 | logError("Error reading configuration '%s': %s", settings.configName, e.msg); 31 | throw e; 32 | } 33 | m_db.invokeOnConfigChange({ m_config = m_db.getConfig(settings.configName, true); }); 34 | } 35 | 36 | @property inout(DBController) db() inout { return m_db; } 37 | @property inout(VibeLogSettings) settings() inout { return m_settings; } 38 | @property inout(Config) config() inout { return m_config; } 39 | @property inout(DiskutoCommentStore) diskuto() inout { return m_diskuto; } 40 | 41 | PostListInfo getPostListInfo(int page = 0, int page_size = 0) 42 | { 43 | auto info = PostListInfo(m_settings); 44 | info.users = m_db.getAllUsers(); 45 | info.pageCount = getPageCount(page_size); 46 | info.pageNumber = page; 47 | info.posts = getPostsForPage(info.pageNumber, page_size); 48 | foreach (p; info.posts) info.commentCount ~= m_diskuto.getActiveCommentCount("vibelog-"~p.id.toString()); 49 | info.recentPosts = getRecentPosts(); 50 | return info; 51 | } 52 | 53 | int getPageCount(int page_size = 0) 54 | { 55 | if (page_size <= 0) page_size = m_settings.postsPerPage; 56 | int cnt = m_db.countPostsForCategory(m_config.categories); 57 | return (cnt + page_size - 1) / page_size; 58 | } 59 | 60 | int getPostPage(BsonObjectID post_id, int page_size = 0) 61 | { 62 | if (page_size <= 0) page_size = m_settings.postsPerPage; 63 | try { 64 | int cnt = 0; 65 | m_db.getPublicPostsForCategory(m_config.categories, 0, (size_t i, Post p){ 66 | if (p.id == post_id) 67 | return false; 68 | cnt++; 69 | return true; 70 | }); 71 | return cnt / page_size + 1; 72 | } catch (Exception e) return 1; 73 | } 74 | 75 | Post[] getPostsForPage(int n, int page_size = 0) 76 | { 77 | if (page_size <= 0) page_size = m_settings.postsPerPage; 78 | Post[] ret; 79 | try { 80 | size_t cnt = 0; 81 | m_db.getPublicPostsForCategory(m_config.categories, n*page_size, (size_t i, Post p){ 82 | ret ~= p; 83 | if( ++cnt >= page_size ) 84 | return false; 85 | return true; 86 | }); 87 | } catch( Exception e ){ 88 | auto p = new Post; 89 | p.header = "ERROR"; 90 | p.subHeader = e.msg; 91 | ret ~= p; 92 | } 93 | return ret; 94 | } 95 | 96 | Post[] getRecentPosts() 97 | { 98 | Post[] ret; 99 | m_db.getPublicPostsForCategory(m_config.categories, 0, (i, p){ 100 | if (i >= m_settings.maxRecentPosts) return false; 101 | ret ~= p; 102 | return true; 103 | }); 104 | return ret; 105 | } 106 | 107 | string getShowPagePath(int page) 108 | { 109 | return m_settings.rootDir ~ "?page=" ~ to!string(page+1); 110 | } 111 | } 112 | 113 | struct PostListInfo 114 | { 115 | import vibelog.info : VibeLogInfo; 116 | VibeLogInfo vli; 117 | alias vli this; 118 | 119 | User[string] users; 120 | int pageNumber = 0; 121 | int pageCount; 122 | Post[] posts; 123 | long[] commentCount; 124 | Post[] recentPosts; 125 | 126 | import vibelog.settings : VibeLogSettings; 127 | this(VibeLogSettings settings) 128 | { 129 | vli = VibeLogInfo(settings); 130 | } 131 | } 132 | 133 | struct VibelogHeadlineListConfig 134 | { 135 | bool showSummaries = true; 136 | size_t maxPosts = 10; 137 | size_t headerLevel = 2; 138 | bool headerLinks = true; 139 | bool footerLinks = false; 140 | bool dateFirst = true; 141 | } 142 | -------------------------------------------------------------------------------- /source/vibelog/db/dbcontroller.d: -------------------------------------------------------------------------------- 1 | module vibelog.db.dbcontroller; 2 | 3 | public import vibelog.config; 4 | public import vibelog.post; 5 | public import vibelog.user; 6 | import vibelog.settings; 7 | import vibe.core.stream; 8 | import vibe.data.bson; 9 | 10 | 11 | DBController createDBController(VibeLogSettings settings) 12 | { 13 | import vibelog.db.mongo : MongoDBController; 14 | import std.exception : enforce; 15 | import std.string : startsWith; 16 | 17 | enforce(settings.databaseURL.startsWith("mongodb:"), "Only mongodb: database URLs supported."); 18 | return new MongoDBController(settings.databaseURL); 19 | } 20 | 21 | interface DBController { 22 | Config getConfig(string name, bool createdefault = false); 23 | void setConfig(Config cfg); 24 | void invokeOnConfigChange(void delegate() del); 25 | void deleteConfig(string name); 26 | Config[] getAllConfigs(); 27 | 28 | User[string] getAllUsers(); 29 | User getUser(BsonObjectID userid); 30 | User getUserByName(string name); 31 | User getUserByEmail(string email); 32 | BsonObjectID addUser(User user); 33 | void modifyUser(User user); 34 | void deleteUser(BsonObjectID id); 35 | 36 | int countPostsForCategory(string[] categories); 37 | void getPostsForCategory(string[] categories, int nskip, bool delegate(size_t idx, Post post) del); 38 | void getPublicPostsForCategory(string[] categories, int nskip, bool delegate(size_t idx, Post post) del); 39 | void getAllPosts(int nskip, bool delegate(size_t idx, Post post) del); 40 | Post getPost(BsonObjectID postid); 41 | Post getPost(string name); 42 | bool hasPost(string name); 43 | BsonObjectID addPost(Post post); 44 | void modifyPost(Post post); 45 | void deletePost(BsonObjectID id); 46 | void addFile(string post_name, string file_name, in ubyte[] contents); 47 | string[] getFiles(string post_name); 48 | InputStream getFile(string post_name, string file_name); 49 | void removeFile(string post_name, string file_name); 50 | } 51 | -------------------------------------------------------------------------------- /source/vibelog/db/mongo.d: -------------------------------------------------------------------------------- 1 | module vibelog.db.mongo; 2 | 3 | import vibelog.db.dbcontroller; 4 | 5 | import vibe.core.log; 6 | import vibe.core.stream; 7 | import vibe.data.bson; 8 | import vibe.db.mongo.mongo; 9 | import vibe.mail.smtp; 10 | import vibe.stream.memory; 11 | 12 | import std.exception; 13 | import std.variant; 14 | 15 | 16 | final class MongoDBController : DBController { 17 | private { 18 | MongoCollection m_configs; 19 | MongoCollection m_users; 20 | MongoCollection m_posts; 21 | MongoCollection m_postFiles; 22 | void delegate()[] m_onConfigChange; 23 | } 24 | 25 | this(string db_url) 26 | { 27 | string database = "vibelog"; 28 | MongoClientSettings dbsettings; 29 | if (parseMongoDBUrl(dbsettings, db_url)) 30 | database = dbsettings.database; 31 | 32 | auto db = connectMongoDB(db_url).getDatabase(database); 33 | m_configs = db["configs"]; 34 | m_users = db["users"]; 35 | m_posts = db["posts"]; 36 | m_postFiles = db["postFiles"]; 37 | 38 | m_posts.createIndex(["category": 1, "date": -1, "isPublic": 1]); 39 | 40 | upgradeComments(db); 41 | } 42 | 43 | Config getConfig(string name, bool createdefault = false) 44 | { 45 | auto configbson = m_configs.findOne(["name": Bson(name)]); 46 | if( !configbson.isNull() ) 47 | return Config.fromBson(configbson); 48 | enforce(createdefault, "Configuration does not exist."); 49 | auto cfg = new Config; 50 | cfg.name = name; 51 | m_configs.insertOne(cfg.toBson()); 52 | return cfg; 53 | } 54 | 55 | void setConfig(Config cfg) 56 | { 57 | Bson update = cfg.toBson(); 58 | m_configs.replaceOne(["name": Bson(cfg.name)], update); 59 | foreach (d; m_onConfigChange) d(); 60 | } 61 | 62 | void invokeOnConfigChange(void delegate() del) 63 | { 64 | m_onConfigChange ~= del; 65 | } 66 | 67 | void deleteConfig(string name) 68 | { 69 | m_configs.deleteOne(["name": Bson(name)]); 70 | } 71 | 72 | Config[] getAllConfigs() 73 | { 74 | Bson[string] query; 75 | Config[] ret; 76 | foreach( config; m_configs.find(query) ){ 77 | auto c = Config.fromBson(config); 78 | ret ~= c; 79 | } 80 | return ret; 81 | } 82 | 83 | User[string] getAllUsers() 84 | { 85 | import vibelog.internal.passwordhash : generatePasswordHash; 86 | 87 | Bson[string] query; 88 | User[string] ret; 89 | foreach( user; m_users.find(query) ){ 90 | auto u = User.fromBson(user); 91 | ret[u.username] = u; 92 | } 93 | if( ret.length == 0 ){ 94 | auto initial_admin = new User; 95 | initial_admin.username = "admin"; 96 | initial_admin.password = generatePasswordHash("admin"); 97 | initial_admin.name = "Default Administrator"; 98 | initial_admin.groups ~= "admin"; 99 | m_users.insertOne(initial_admin); 100 | ret["admin"] = initial_admin; 101 | } 102 | return ret; 103 | } 104 | 105 | User getUser(BsonObjectID userid) 106 | { 107 | auto userbson = m_users.findOne(["_id": Bson(userid)]); 108 | return User.fromBson(userbson); 109 | } 110 | 111 | User getUserByName(string name) 112 | { 113 | auto userbson = m_users.findOne(["username": Bson(name)]); 114 | if (userbson.isNull()) { 115 | try { 116 | auto id = BsonObjectID.fromHexString(name); 117 | logDebug("%s <-> %s", name, id.toString()); 118 | assert(id.toString() == name); 119 | userbson = m_users.findOne(["_id": Bson(id)]); 120 | } catch (Exception e) { 121 | return null; 122 | } 123 | } 124 | //auto userbson = m_users.findOne(Bson(["name" : Bson(name)])); 125 | return User.fromBson(userbson); 126 | } 127 | 128 | User getUserByEmail(string email) 129 | { 130 | auto userbson = m_users.findOne(["email": Bson(email)]); 131 | return User.fromBson(userbson); 132 | } 133 | 134 | BsonObjectID addUser(User user) 135 | { 136 | auto id = BsonObjectID.generate(); 137 | Bson userbson = user.toBson(); 138 | userbson["_id"] = Bson(id); 139 | m_users.insertOne(userbson); 140 | return id; 141 | } 142 | 143 | void modifyUser(User user) 144 | { 145 | assert(user._id.valid); 146 | Bson update = user.toBson(); 147 | m_users.replaceOne(["_id": Bson(user._id)], update); 148 | } 149 | 150 | void deleteUser(BsonObjectID id) 151 | { 152 | assert(id.valid); 153 | m_users.deleteOne(["_id": Bson(id)]); 154 | } 155 | 156 | int countPostsForCategory(string[] categories) 157 | { 158 | static struct CQ { @name("$in") string[] categories; } 159 | static struct Q { CQ category; bool isPublic; } 160 | return cast(int)m_posts.countDocuments(Q(CQ(categories), true)); 161 | } 162 | 163 | void getPostsForCategory(string[] categories, int nskip, bool delegate(size_t idx, Post post) del) 164 | { 165 | auto cats = new Bson[categories.length]; 166 | foreach( i; 0 .. categories.length ) cats[i] = Bson(categories[i]); 167 | Bson category = Bson(["$in" : Bson(cats)]); 168 | Bson[string] query = ["category" : category]; 169 | FindOptions opts; 170 | opts.skip = nskip; 171 | opts.sort = Bson(["date" : Bson(-1)]); 172 | foreach (idx, post; m_posts.find(query, opts).byPair) { 173 | if (!del(idx, Post.fromBson(post))) 174 | break; 175 | } 176 | } 177 | 178 | void getPublicPostsForCategory(string[] categories, int nskip, bool delegate(size_t idx, Post post) del) 179 | { 180 | auto cats = new Bson[categories.length]; 181 | foreach( i; 0 .. categories.length ) cats[i] = Bson(categories[i]); 182 | Bson category = Bson(["$in" : Bson(cats)]); 183 | Bson[string] query = ["category" : category, "isPublic": Bson(true)]; 184 | FindOptions opts; 185 | opts.skip = nskip; 186 | opts.sort = Bson(["date" : Bson(-1)]); 187 | foreach (idx, post; m_posts.find(query, opts).byPair) { 188 | if (!del(idx, Post.fromBson(post))) 189 | break; 190 | } 191 | } 192 | 193 | void getAllPosts(int nskip, bool delegate(size_t idx, Post post) del) 194 | { 195 | Bson[string] query; 196 | FindOptions opts; 197 | opts.skip = nskip; 198 | opts.sort = Bson(["date" : Bson(-1)]); 199 | foreach (idx, post; m_posts.find(query, opts).byPair) { 200 | if (!del(idx, Post.fromBson(post))) 201 | break; 202 | } 203 | } 204 | 205 | 206 | Post getPost(BsonObjectID postid) 207 | { 208 | auto postbson = m_posts.findOne(["_id": Bson(postid)]); 209 | return Post.fromBson(postbson); 210 | } 211 | 212 | Post getPost(string name) 213 | { 214 | auto postbson = m_posts.findOne(["slug": Bson(name)]); 215 | if( postbson.isNull() ) 216 | postbson = m_posts.findOne(["_id" : Bson(BsonObjectID.fromHexString(name))]); 217 | return Post.fromBson(postbson); 218 | } 219 | 220 | bool hasPost(string name) 221 | { 222 | return !m_posts.findOne(["slug": Bson(name)]).isNull(); 223 | 224 | } 225 | 226 | BsonObjectID addPost(Post post) 227 | { 228 | auto id = BsonObjectID.generate(); 229 | Bson postbson = post.toBson(); 230 | postbson["_id"] = Bson(id); 231 | m_posts.insertOne(postbson); 232 | return id; 233 | } 234 | 235 | void modifyPost(Post post) 236 | { 237 | assert(post.id.valid); 238 | Bson update = post.toBson(); 239 | m_posts.replaceOne(["_id": Bson(post.id)], update); 240 | } 241 | 242 | void deletePost(BsonObjectID id) 243 | { 244 | assert(id.valid); 245 | m_posts.deleteOne(["_id": Bson(id)]); 246 | } 247 | 248 | void addFile(string post_name, string file_name, in ubyte[] contents) 249 | { 250 | import vibe.stream.operations : readAll; 251 | struct I { 252 | string postName; 253 | string fileName; 254 | } 255 | m_postFiles.insertOne(PostFile(post_name, file_name, cast(ubyte[])contents)); 256 | } 257 | 258 | string[] getFiles(string post_name) 259 | { 260 | import std.algorithm.iteration : map; 261 | import std.array : array; 262 | return m_postFiles.find(["postName": post_name], ["fileName": true]).map!(p => p["fileName"].get!string).array; 263 | } 264 | 265 | InputStream getFile(string post_name, string file_name) 266 | { 267 | auto f = m_postFiles.findOne!PostFile(["postName": post_name, "fileName": file_name]); 268 | if (f.isNull) return null; 269 | return createMemoryStream(f.get.contents); 270 | } 271 | 272 | void removeFile(string post_name, string file_name) 273 | { 274 | m_postFiles.deleteOne(["postName": post_name, "fileName": file_name]); 275 | } 276 | 277 | private void upgradeComments(MongoDatabase db) 278 | { 279 | import diskuto.commentstore : StoredComment; 280 | import diskuto.commentstores.mongodb : MongoStruct; 281 | 282 | auto comments = db["comments"]; 283 | 284 | // Upgrade post contained comments to their collection 285 | foreach( p; m_posts.find(["comments": ["$exists": true]], ["comments": 1]) ){ 286 | foreach( c; p["comments"] ){ 287 | c["_id"] = BsonObjectID.generate(); 288 | c["postId"] = p["_id"]; 289 | comments.insertOne(c); 290 | } 291 | m_posts.updateOne(["_id": p["_id"]], ["$unset": ["comments": 1]]); 292 | } 293 | 294 | // Upgrade old comments to Diskuto format 295 | foreach (c; comments.find(["postId": ["$exists": true]])) { 296 | auto oldc = OldComment.fromBson(c); 297 | StoredComment newc; 298 | newc.id = oldc.id.toString(); 299 | newc.status = oldc.isPublic ? StoredComment.Status.active : StoredComment.Status.disabled; 300 | newc.topic = "vibelog-" ~ oldc.postId.toString(); 301 | newc.author = "vibelog-..."; 302 | newc.clientAddress = oldc.authorIP; 303 | newc.name = oldc.authorName; 304 | newc.email = oldc.authorMail; 305 | newc.website = oldc.authorHomepage; 306 | newc.text = oldc.content; 307 | newc.time = oldc.date; 308 | comments.replaceOne(["_id": c["_id"]], MongoStruct!StoredComment(newc)); 309 | } 310 | } 311 | } 312 | 313 | struct PostFile { 314 | string postName; 315 | string fileName; 316 | ubyte[] contents; 317 | } 318 | 319 | 320 | final class OldComment { 321 | BsonObjectID id; 322 | BsonObjectID postId; 323 | bool isPublic; 324 | SysTime date; 325 | int answerTo; 326 | string authorName; 327 | string authorMail; 328 | string authorHomepage; 329 | string authorIP; 330 | string header; 331 | string content; 332 | 333 | static OldComment fromBson(Bson bson) 334 | { 335 | auto ret = new OldComment; 336 | ret.id = cast(BsonObjectID)bson["_id"]; 337 | ret.postId = cast(BsonObjectID)bson["postId"]; 338 | ret.isPublic = cast(bool)bson["isPublic"]; 339 | ret.date = SysTime.fromISOExtString(cast(string)bson["date"]); 340 | ret.answerTo = cast(int)bson["answerTo"]; 341 | ret.authorName = cast(string)bson["authorName"]; 342 | ret.authorMail = cast(string)bson["authorMail"]; 343 | ret.authorHomepage = cast(string)bson["authorHomepage"]; 344 | ret.authorIP = bson["authorIP"].opt!string(); 345 | ret.header = cast(string)bson["header"]; 346 | ret.content = cast(string)bson["content"]; 347 | return ret; 348 | } 349 | } 350 | -------------------------------------------------------------------------------- /source/vibelog/info.d: -------------------------------------------------------------------------------- 1 | module vibelog.info; 2 | 3 | struct VibeLogInfo 4 | { 5 | import vibelog.settings : VibeLogSettings; 6 | VibeLogSettings settings; 7 | } 8 | -------------------------------------------------------------------------------- /source/vibelog/internal/diskuto.d: -------------------------------------------------------------------------------- 1 | module vibelog.internal.diskuto; 2 | 3 | import vibelog.controller; 4 | import vibelog.user; 5 | 6 | import diskuto.commentstore : StoredComment; 7 | import diskuto.commentstores.mongodb; 8 | import diskuto.web : DiskutoWeb, registerDiskutoWeb; 9 | import diskuto.settings : DiskutoSettings; 10 | import diskuto.userstore : StoredUser, DiskutoUserStore; 11 | import vibe.data.json : parseJsonString; 12 | import vibe.http.router : URLRouter; 13 | import vibe.http.server : HTTPServerRequest; 14 | import std.typecons : Nullable; 15 | 16 | DiskutoWeb registerDiskuto(URLRouter router, VibeLogController ctrl) 17 | { 18 | import antispam.antispam : AntispamState, SpamFilter; 19 | import antispam.filters.bayes : BayesSpamFilter; 20 | AntispamState.registerFilter("bayes", () => cast(SpamFilter)new BayesSpamFilter); 21 | 22 | auto dsettings = new DiskutoSettings; 23 | dsettings.commentStore = ctrl.diskuto; 24 | dsettings.userStore = new UserStore(ctrl); 25 | dsettings.antispam = parseJsonString(`[{"filter": "bayes", "settings": {}}]`); 26 | return router.registerDiskutoWeb(dsettings); 27 | } 28 | 29 | private final class UserStore : DiskutoUserStore { 30 | private { 31 | VibeLogController m_ctrl; 32 | } 33 | 34 | this(VibeLogController ctrl) 35 | { 36 | m_ctrl = ctrl; 37 | } 38 | 39 | override Nullable!StoredUser getLoggedInUser(HTTPServerRequest req) 40 | @trusted { 41 | try { 42 | if (req.session) { 43 | auto usr = req.session.get("vibelog.loggedInUser", ""); 44 | if (usr.length) 45 | return Nullable!StoredUser(toStoredUser(m_ctrl.db.getUserByName(usr))); 46 | } 47 | } catch (Exception e) {} 48 | return Nullable!StoredUser.init; 49 | } 50 | 51 | override Nullable!StoredUser getUserForEmail(string email) 52 | @trusted { 53 | try return Nullable!StoredUser(toStoredUser(m_ctrl.db.getUserByEmail(email))); 54 | catch (Exception e) { return Nullable!StoredUser.init; } 55 | } 56 | 57 | override StoredUser.Role getUserRole(StoredUser.ID user, string topic) 58 | @trusted { 59 | import std.algorithm.searching : startsWith; 60 | import vibe.data.bson : BsonObjectID; 61 | 62 | User dbuser; 63 | try { 64 | if (user.startsWith("vibelog-")) { 65 | auto user_id = BsonObjectID.fromString(user[8 .. $]); 66 | dbuser = m_ctrl.db.getUser(user_id); 67 | if (dbuser.inGroup("admin")) return StoredUser.Role.moderator; 68 | } 69 | } catch (Exception e) {} 70 | 71 | if (!topic.startsWith("vibelog-")) 72 | return StoredUser.Role.member; 73 | 74 | try { 75 | auto post_id = BsonObjectID.fromString(topic[8 .. $]); 76 | auto post = m_ctrl.db.getPost(post_id); 77 | if (dbuser && dbuser.mayPostInCategory(post.category)) 78 | return StoredUser.Role.moderator; 79 | return post.commentsAllowed ? StoredUser.Role.member : StoredUser.Role.reader; 80 | } catch (Exception e) { 81 | return StoredUser.Role.reader; 82 | } 83 | } 84 | 85 | private StoredUser toStoredUser(User usr) 86 | @safe { 87 | StoredUser ret; 88 | ret.id = "vibelog-" ~ () @trusted { return usr._id.toString(); } (); 89 | ret.name = usr.name; 90 | ret.email = usr.email; 91 | return ret; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /source/vibelog/internal/passwordhash.d: -------------------------------------------------------------------------------- 1 | module vibelog.internal.passwordhash; 2 | 3 | 4 | string generatePasswordHash(string password) 5 | @safe { 6 | import std.base64 : Base64; 7 | import std.random : uniform; 8 | 9 | // FIXME: use a more secure hash method 10 | ubyte[4] salt; 11 | foreach( i; 0 .. 4 ) salt[i] = cast(ubyte)uniform(0, 256); 12 | ubyte[16] hash = md5hash(salt, password); 13 | return Base64.encode(salt ~ hash).idup; 14 | } 15 | 16 | bool validatePasswordHash(string password_hash, string password) 17 | @safe { 18 | import std.base64 : Base64; 19 | import std.exception : enforce; 20 | 21 | // FIXME: use a more secure hash method 22 | import std.string : format; 23 | ubyte[] upass = Base64.decode(password_hash); 24 | enforce(upass.length == 20, format("Invalid binary password hash length: %s", upass.length)); 25 | auto salt = upass[0 .. 4]; 26 | auto hashcmp = upass[4 .. 20]; 27 | ubyte[16] hash = md5hash(salt, password); 28 | return hash == hashcmp; 29 | } 30 | 31 | unittest { 32 | auto h = generatePasswordHash("foobar"); 33 | assert(!validatePasswordHash(h, "foo")); 34 | assert(validatePasswordHash(h, "foobar")); 35 | } 36 | 37 | private ubyte[16] md5hash(ubyte[] salt, string[] strs...) 38 | @safe { 39 | import std.digest.md; 40 | MD5 ctx; 41 | ctx.start(); 42 | ctx.put(salt); 43 | foreach( s; strs ) ctx.put(cast(const(ubyte)[])s); 44 | return ctx.finish(); 45 | } 46 | -------------------------------------------------------------------------------- /source/vibelog/post.d: -------------------------------------------------------------------------------- 1 | module vibelog.post; 2 | 3 | import vibelog.settings; 4 | 5 | import vibe.data.bson; 6 | import vibe.textfilter.markdown; 7 | import vibe.textfilter.html; 8 | 9 | import std.array; 10 | import std.conv; 11 | import std.string : strip; 12 | public import std.datetime; 13 | 14 | import stringex.unidecode; 15 | 16 | 17 | final class Post { 18 | BsonObjectID id; 19 | bool isPublic; 20 | bool commentsAllowed; 21 | string slug; // url entity to identify this post - generated from the header by default 22 | string author; // user name 23 | string category; // can be hierarchical using dotted.syntax.format 24 | SysTime date; 25 | string header; // Title/heading 26 | string headerImage; // URL of large header image 27 | string summaryImage; // Image to use for the summary (fall back to headerImage) 28 | string summaryTitle; // Short title used for the summary (<=70chars) 29 | string summary; // Short summary of the article (<=240 chars), displayed on cards 30 | string subHeader; // First paragraph of the article, displayed on overview pages 31 | string content; 32 | string[] filters; 33 | string[] tags; 34 | string[] trackbacks; 35 | 36 | this() 37 | { 38 | id = BsonObjectID.generate(); 39 | date = Clock.currTime().toUTC(); 40 | } 41 | 42 | @property string name() const { return slug.length ? slug : id.toString(); } 43 | 44 | static Post fromBson(Bson bson) 45 | { 46 | auto ret = new Post; 47 | ret.id = cast(BsonObjectID)bson["_id"]; 48 | ret.isPublic = cast(bool)bson["isPublic"]; 49 | ret.commentsAllowed = cast(bool)bson["commentsAllowed"]; 50 | ret.slug = cast(string)bson["slug"]; 51 | ret.author = cast(string)bson["author"]; 52 | ret.category = cast(string)bson["category"]; 53 | ret.date = SysTime.fromISOExtString(cast(string)bson["date"]); 54 | ret.headerImage = cast(string)bson["headerImage"]; 55 | ret.header = cast(string)bson["header"]; 56 | ret.subHeader = cast(string)bson["subHeader"]; 57 | ret.content = cast(string)bson["content"]; 58 | ret.summaryImage = bson["summaryImage"].opt!string; 59 | ret.summary = bson["summary"].opt!string; 60 | ret.summaryTitle = bson["summaryTitle"].opt!string; 61 | 62 | if (bson["filters"].isNull) ret.filters = ["markdown"]; 63 | else { 64 | foreach (f; cast(Bson[])bson["filters"]) 65 | ret.filters ~= cast(string)f; 66 | } 67 | 68 | if (!bson["tags"].isNull) 69 | foreach (t; cast(Bson[])bson["tags"]) 70 | ret.tags ~= cast(string)t; 71 | 72 | return ret; 73 | } 74 | 75 | Bson toBson() 76 | const { 77 | 78 | Bson[string] ret; 79 | ret["_id"] = Bson(id); 80 | ret["isPublic"] = Bson(isPublic); 81 | ret["commentsAllowed"] = Bson(commentsAllowed); 82 | ret["slug"] = Bson(slug); 83 | ret["author"] = Bson(author); 84 | ret["category"] = Bson(category); 85 | ret["date"] = Bson(date.toISOExtString()); 86 | ret["headerImage"] = Bson(headerImage); 87 | ret["header"] = Bson(header); 88 | ret["subHeader"] = Bson(subHeader); 89 | ret["content"] = Bson(content); 90 | ret["summaryImage"] = Bson(summaryImage); 91 | ret["summary"] = Bson(summary); 92 | ret["summaryTitle"] = Bson(summaryTitle); 93 | 94 | import std.algorithm : map; 95 | import std.array : array; 96 | ret["filters"] = Bson(filters.map!Bson.array); 97 | ret["tags"] = Bson(tags.map!Bson.array); 98 | 99 | return Bson(ret); 100 | } 101 | 102 | string renderSubHeaderAsHtml(VibeLogSettings settings) 103 | const { 104 | import std.algorithm : canFind; 105 | if (filters.canFind("markdown")) 106 | { 107 | auto ret = appender!string(); 108 | filterMarkdown(ret, subHeader, settings.markdownSettings); 109 | return ret.data; 110 | } 111 | else 112 | { 113 | return subHeader; 114 | } 115 | } 116 | 117 | string renderContentAsHtml(VibeLogSettings settings, string page_path = "", int header_level_nesting = 0) 118 | const { 119 | 120 | import std.algorithm : canFind; 121 | string html = content; 122 | if (filters.canFind("markdown")) 123 | { 124 | scope ms = new MarkdownSettings; 125 | ms.flags = settings.markdownSettings.flags; 126 | ms.headingBaseLevel = settings.markdownSettings.headingBaseLevel + header_level_nesting; 127 | if (page_path != "") 128 | { 129 | ms.urlFilter = (lnk, is_image) { 130 | import std.algorithm : startsWith; 131 | if (lnk.startsWith("http://") || lnk.startsWith("https://")) 132 | return lnk; 133 | if (lnk.startsWith("#")) return lnk; 134 | auto pp = InetPath(page_path); 135 | if (!pp.endsWithSlash) 136 | pp = pp.parentPath; 137 | return (settings.siteURL.path~("posts/"~slug~"/"~lnk)).relativeTo(pp).toString(); 138 | }; 139 | } 140 | html = filterMarkdown(html, ms); 141 | } 142 | foreach (flt; settings.textFilters) 143 | html = flt(html); 144 | return html; 145 | } 146 | } 147 | 148 | string makeSlugFromHeader(string header) 149 | { 150 | Appender!string ret; 151 | auto decoded_header = unidecode(header).replace("[?]", "-"); 152 | foreach (dchar ch; strip(decoded_header)) { 153 | switch (ch) { 154 | default: 155 | ret.put('-'); 156 | break; 157 | case '"', '\'', '´', '`', '.', ',', ';', '!', '?', '¿', '¡': 158 | break; 159 | case 'A': .. case 'Z'+1: 160 | ret.put(cast(dchar)(ch - 'A' + 'a')); 161 | break; 162 | case 'a': .. case 'z'+1: 163 | case '0': .. case '9'+1: 164 | ret.put(ch); 165 | break; 166 | } 167 | } 168 | return ret.data; 169 | } 170 | 171 | unittest { 172 | assert(makeSlugFromHeader("sample title") == "sample-title"); 173 | assert(makeSlugFromHeader("Sample Title") == "sample-title"); 174 | assert(makeSlugFromHeader(" Sample Title2 ") == "sample-title2"); 175 | assert(makeSlugFromHeader("反清復明") == "fan-qing-fu-ming"); 176 | assert(makeSlugFromHeader("φύλλο") == "phullo"); 177 | assert(makeSlugFromHeader("ខេមរភាសា") == "khemrbhaasaa"); 178 | assert(makeSlugFromHeader("zweitgrößte der Europäischen Union") == "zweitgrosste-der-europaischen-union"); 179 | assert(makeSlugFromHeader("østlige og vestlige del udviklede sig uafhængigt ") == "ostlige-og-vestlige-del-udviklede-sig-uafhaengigt"); 180 | assert(makeSlugFromHeader("¿pchnąć w tę łódź jeża lub ośm skrzyń fig?") == "pchnac-w-te-lodz-jeza-lub-osm-skrzyn-fig"); 181 | assert(makeSlugFromHeader("¼ €") == "1-4-eu"); 182 | } 183 | -------------------------------------------------------------------------------- /source/vibelog/rss.d: -------------------------------------------------------------------------------- 1 | module vibelog.rss; 2 | 3 | import vibe.core.stream; 4 | import vibe.inet.message : toRFC822DateTimeString; 5 | 6 | import std.datetime; 7 | 8 | 9 | final class RssFeed { 10 | RssChannel[] channels; 11 | 12 | void render(OutputStream)(OutputStream dst) 13 | if (isOutputStream!OutputStream) 14 | { 15 | dst.write("\n"); 16 | dst.write("\n"); 17 | foreach( ch; channels ) 18 | ch.render(dst); 19 | dst.write("\n"); 20 | dst.flush(); 21 | } 22 | } 23 | 24 | final class RssChannel { 25 | string title; 26 | string link; 27 | string webLink; 28 | string description; 29 | string language = "en-us"; 30 | string copyright; 31 | SysTime pubDate; 32 | 33 | string imageTitle; 34 | string imageUrl; 35 | string imageLink; 36 | 37 | RssEntry[] entries; 38 | 39 | void render(OutputStream)(OutputStream dst) 40 | if (isOutputStream!OutputStream) 41 | { 42 | dst.write("\t\n"); 43 | dst.write("\t\t"); dst.write(title); dst.write("\n"); 44 | dst.write("\t\t"); dst.write(link); dst.write("\n"); 45 | dst.write("\t\t\n"); 46 | dst.write("\t\t"); dst.write(description); dst.write("\n"); 47 | dst.write("\t\t"); dst.write(language); dst.write("\n"); 48 | dst.write("\t\t"); dst.write(copyright); dst.write("\n"); 49 | dst.write("\t\t"); dst.write(toRFC822DateTimeString(pubDate)); dst.write("\n"); 50 | if( imageUrl.length ){ 51 | dst.write("\t\t\n"); 52 | dst.write("\t\t\t"); dst.write(imageUrl); dst.write("\n"); 53 | dst.write("\t\t\t"); dst.write(imageTitle); dst.write("\n"); 54 | dst.write("\t\t\t"); dst.write(imageLink); dst.write("\n"); 55 | dst.write("\t\t\n"); 56 | } 57 | foreach( e; entries ) 58 | e.render(dst); 59 | dst.write("\t\n"); 60 | } 61 | } 62 | 63 | final class RssEntry { 64 | string title; 65 | string description; 66 | string link; 67 | string language = "en-us"; 68 | string author; 69 | string guid; 70 | SysTime pubDate; 71 | 72 | void render(OutputStream)(OutputStream dst) 73 | if (isOutputStream!OutputStream) 74 | { 75 | import vibe.textfilter.html : htmlEscape; 76 | 77 | dst.write("\t\t\n"); 78 | dst.write("\t\t\t"); dst.write(title); dst.write("\n"); 79 | dst.write("\t\t\t"); dst.write(description); dst.write("\n"); 80 | dst.write("\t\t\t"); dst.write(link); dst.write("\n"); 81 | dst.write("\t\t\t"); dst.write(htmlEscape(author)); dst.write("\n"); 82 | dst.write("\t\t\t"); dst.write(guid); dst.write("\n"); 83 | dst.write("\t\t\t"); dst.write(toRFC822DateTimeString(pubDate)); dst.write("\n"); 84 | dst.write("\t\t\n"); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /source/vibelog/settings.d: -------------------------------------------------------------------------------- 1 | module vibelog.settings; 2 | 3 | public import vibe.inet.url; 4 | import vibe.db.mongo.connection; 5 | import vibe.textfilter.markdown; 6 | 7 | final class VibeLogSettings { 8 | string databaseURL = "mongodb://127.0.0.1/vibelog"; 9 | string configName = "global"; 10 | string blogName = "VibeLog"; 11 | string blogDescription = "Web publishing based on the vibe.d framework"; 12 | int postsPerPage = 4; 13 | int maxRecentPosts = 20; 14 | bool showFullPostsInPostList = true; 15 | bool placePostHeaderImageFirst = false; 16 | bool enableBackButton = false; 17 | bool inlineReadMoreButton = false; 18 | URL siteURL = URL("http", "localhost", 8080, InetPath("/")); 19 | deprecated("Use siteURL instead.") alias siteUrl = siteURL; 20 | string adminPrefix = "manage/"; 21 | string delegate(string) @safe [] textFilters; 22 | MarkdownSettings markdownSettings; 23 | 24 | this() 25 | { 26 | markdownSettings = new MarkdownSettings; 27 | markdownSettings.flags = MarkdownFlags.backtickCodeBlocks; 28 | } 29 | 30 | @property 31 | { 32 | string rootDir() 33 | { 34 | return siteURL.path.toString(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /source/vibelog/user.d: -------------------------------------------------------------------------------- 1 | module vibelog.user; 2 | 3 | import vibe.data.bson; 4 | import vibe.textfilter.markdown; 5 | import vibe.textfilter.html; 6 | 7 | import std.array; 8 | import std.base64; 9 | import std.conv; 10 | import std.exception; 11 | import std.random; 12 | public import std.datetime; 13 | 14 | 15 | class User { 16 | BsonObjectID _id; 17 | string username; 18 | string name; 19 | string email; 20 | string password; 21 | string[] groups; 22 | string[] allowedCategories; 23 | 24 | this() 25 | { 26 | _id = BsonObjectID.generate(); 27 | } 28 | 29 | bool inGroup(string group) const { 30 | foreach( g; groups ) 31 | if( g == group ) 32 | return true; 33 | return false; 34 | } 35 | 36 | bool isConfigAdmin() const { return inGroup("admin"); } 37 | bool isUserAdmin() const { return inGroup("admin"); } 38 | bool isPostAdmin() const { return inGroup("admin"); } 39 | bool mayPostInCategory(string category){ 40 | if( isPostAdmin() ) return true; 41 | foreach( c; allowedCategories ) 42 | if( c == category ) 43 | return true; 44 | return false; 45 | } 46 | 47 | static User fromBson(Bson bson) 48 | { 49 | auto ret = new User; 50 | ret._id = cast(BsonObjectID)bson["_id"]; 51 | ret.username = cast(string)bson["username"]; 52 | ret.name = cast(string)bson["name"]; 53 | ret.email = cast(string)bson["email"]; 54 | ret.password = cast(string)bson["password"]; 55 | foreach( grp; cast(Bson[])bson["groups"] ) 56 | ret.groups ~= cast(string)grp; 57 | foreach( cat; cast(Bson[])bson["allowedCategories"] ) 58 | ret.allowedCategories ~= cast(string)cat; 59 | return ret; 60 | } 61 | 62 | Bson toBson() 63 | const { 64 | Bson[] bgroups; 65 | foreach( grp; groups ) 66 | bgroups ~= Bson(grp); 67 | 68 | Bson[] bcats; 69 | foreach( cat; allowedCategories ) 70 | bcats ~= Bson(cat); 71 | 72 | Bson[string] ret; 73 | ret["_id"] = Bson(_id); 74 | ret["username"] = Bson(username); 75 | ret["name"] = Bson(name); 76 | ret["email"] = Bson(email); 77 | ret["password"] = Bson(password); 78 | ret["groups"] = Bson(bgroups); 79 | ret["allowedCategories"] = Bson(bcats); 80 | 81 | return Bson(ret); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /source/vibelog/web.d: -------------------------------------------------------------------------------- 1 | module vibelog.web; 2 | 3 | public import vibelog.controller; 4 | 5 | import vibelog.config; 6 | import vibelog.post; 7 | import vibelog.rss; 8 | import vibelog.settings; 9 | import vibelog.user; 10 | 11 | import diskuto.web; 12 | import vibe.core.log; 13 | import vibe.db.mongo.connection; 14 | import vibe.http.fileserver; 15 | import vibe.http.router; 16 | import vibe.inet.url; 17 | import vibe.textfilter.markdown; 18 | import vibe.web.web; 19 | 20 | import std.conv; 21 | import std.datetime; 22 | import std.exception; 23 | import std.string; 24 | 25 | 26 | void registerVibeLogWeb(URLRouter router, VibeLogController controller) 27 | { 28 | import vibelog.internal.diskuto; 29 | 30 | string sub_path = controller.settings.rootDir; 31 | assert(sub_path.endsWith("/"), "Blog site URL must end with '/'."); 32 | 33 | if (sub_path.length > 1) router.get(sub_path[0 .. $-1], staticRedirect(sub_path)); 34 | 35 | auto diskuto = router.registerDiskuto(controller); 36 | 37 | auto web = new VibeLogWeb(controller, diskuto); 38 | 39 | auto websettings = new WebInterfaceSettings; 40 | websettings.urlPrefix = sub_path; 41 | router.registerWebInterface(web, websettings); 42 | 43 | auto fsettings = new HTTPFileServerSettings; 44 | fsettings.serverPathPrefix = sub_path; 45 | router.get(sub_path ~ "*", serveStaticFiles("public", fsettings)); 46 | } 47 | 48 | 49 | /// private 50 | /*private*/ final class VibeLogWeb { 51 | private { 52 | VibeLogController m_ctrl; 53 | VibeLogSettings m_settings; 54 | DiskutoWeb m_diskuto; 55 | SessionVar!(string, "vibelog.loggedInUser") m_loggedInUser; 56 | } 57 | 58 | this(VibeLogController controller, DiskutoWeb diskuto) 59 | { 60 | m_settings = controller.settings; 61 | m_ctrl = controller; 62 | m_diskuto = diskuto; 63 | 64 | enforce(m_settings.rootDir.startsWith("/") && m_settings.rootDir.endsWith("/"), "All local URLs must start with and end with '/'."); 65 | } 66 | 67 | // 68 | // public pages 69 | // 70 | 71 | void get(int page = 1, string _error = null) 72 | { 73 | auto info = PageInfo(m_settings, m_ctrl.getPostListInfo(page - 1)); 74 | info.refPath = m_settings.rootDir; 75 | info.loginError = _error; 76 | render!("vibelog.postlist.dt", info); 77 | } 78 | 79 | @errorDisplay!getPost 80 | @path("posts/:postname") 81 | void getPost(string _postname, string _error) 82 | { 83 | m_diskuto.setupRequest(); 84 | 85 | auto info = PostInfo(m_settings); 86 | info.users = m_ctrl.db.getAllUsers(); 87 | try info.post = m_ctrl.db.getPost(_postname); 88 | catch(Exception e){ return; } // -> gives 404 error 89 | if (m_settings.enableBackButton) 90 | info.postPage = m_ctrl.getPostPage(info.post.id); 91 | info.recentPosts = m_ctrl.getRecentPosts(); 92 | info.refPath = m_settings.rootDir~"posts/"~_postname; 93 | info.error = _error; 94 | info.diskuto = m_diskuto; 95 | 96 | render!("vibelog.post.dt", info); 97 | } 98 | 99 | @path("posts/:postname/:filename") 100 | void getPostFile(string _postname, string _filename, HTTPServerResponse res) 101 | { 102 | import vibe.core.stream : pipe; 103 | import vibe.inet.mimetypes : getMimeTypeForFile; 104 | 105 | auto f = m_ctrl.db.getFile(_postname, _filename); 106 | if (f) { 107 | res.contentType = getMimeTypeForFile(_filename); 108 | f.pipe(res.bodyWriter); 109 | } 110 | } 111 | 112 | @path("feed/rss") 113 | void getRSSFeed(HTTPServerResponse res) 114 | { 115 | auto cfg = m_ctrl.config; 116 | 117 | auto ch = new RssChannel; 118 | ch.title = cfg.feedTitle; 119 | ch.link = cfg.feedLink; 120 | ch.description = cfg.feedDescription; 121 | ch.copyright = cfg.copyrightString; 122 | ch.pubDate = Clock.currTime(UTC()); 123 | ch.imageTitle = cfg.feedImageTitle; 124 | ch.imageUrl = cfg.feedImageUrl; 125 | ch.imageLink = cfg.feedLink; 126 | 127 | m_ctrl.db.getPostsForCategory(cfg.categories, 0, (size_t i, Post p){ 128 | if( !p.isPublic ) return true; 129 | 130 | auto usr = m_ctrl.db.getUserByName(p.author); 131 | 132 | auto itm = new RssEntry; 133 | itm.title = p.header; 134 | itm.description = p.subHeader; 135 | itm.link = m_settings.siteURL.toString() ~ "posts/" ~ p.name; 136 | itm.author = usr 137 | ? usr.email ~ " (" ~ usr.name ~ ")" 138 | : "unknown@unknown.unknown (Unknown)"; 139 | itm.guid = p.id.toString; 140 | itm.pubDate = p.date; 141 | ch.entries ~= itm; 142 | return i < 10; 143 | }); 144 | 145 | auto feed = new RssFeed; 146 | feed.channels ~= ch; 147 | 148 | res.headers["Content-Type"] = "application/rss+xml"; 149 | feed.render(res.bodyWriter); 150 | } 151 | 152 | @path("/filter") 153 | void getFilter(string message, string filters, HTTPServerResponse res) 154 | { 155 | auto p = new Post; 156 | p.content = message; 157 | import std.array : split; 158 | p.filters = filters.split(); 159 | res.writeBody(p.renderContentAsHtml(m_settings)); 160 | } 161 | 162 | @path("/sitemap.xml") 163 | void getSitemap(HTTPServerResponse res) 164 | { 165 | res.contentType = "application/xml"; 166 | res.bodyWriter.write("\n"); 167 | res.bodyWriter.write("\n"); 168 | void writeEntry(string[] parts...){ 169 | res.bodyWriter.write(""); 170 | res.bodyWriter.write(m_settings.siteURL.toString()); 171 | foreach( p; parts ) 172 | res.bodyWriter.write(p); 173 | res.bodyWriter.write("\n"); 174 | } 175 | 176 | // home page 177 | writeEntry(); 178 | 179 | m_ctrl.db.getPostsForCategory(m_ctrl.config.categories, 0, (size_t i, Post p){ 180 | if( p.isPublic ) writeEntry("posts/", p.name); 181 | return true; 182 | }); 183 | 184 | res.bodyWriter.write("\n"); 185 | res.bodyWriter.flush(); 186 | } 187 | 188 | @errorDisplay!get 189 | void postLogin(string username, string password, string redirect = null) 190 | { 191 | import vibelog.internal.passwordhash : validatePasswordHash; 192 | 193 | auto usr = m_ctrl.db.getUserByName(username); 194 | enforce(usr && validatePasswordHash(usr.password, password), 195 | "Invalid user name or password."); 196 | m_loggedInUser = username; 197 | .redirect(redirect.length ? redirect : m_ctrl.settings.rootDir); 198 | } 199 | 200 | void getLogout() 201 | { 202 | m_loggedInUser = null; 203 | redirect(m_ctrl.settings.rootDir); 204 | } 205 | } 206 | 207 | import vibelog.info : VibeLogInfo; 208 | struct PageInfo 209 | { 210 | import vibelog.controller : PostListInfo; 211 | PostListInfo pli; 212 | alias pli this; 213 | string rootPath; 214 | string refPath; 215 | string loginError; 216 | 217 | import vibelog.settings : VibeLogSettings; 218 | this(VibeLogSettings settings, PostListInfo pli) 219 | { 220 | this.pli = pli; 221 | this.rootPath = settings.siteURL.path.toString(); 222 | } 223 | } 224 | 225 | struct PostInfo 226 | { 227 | string loginError; 228 | 229 | import vibelog.info : VibeLogInfo; 230 | VibeLogInfo vli; 231 | alias vli this; 232 | 233 | import vibelog.user : User; 234 | User[string] users; 235 | 236 | import vibelog.settings : VibeLogSettings; 237 | VibeLogSettings settings; 238 | 239 | import vibelog.post : Post; 240 | Post post; 241 | 242 | int postPage; 243 | 244 | DiskutoWeb diskuto; 245 | 246 | Post[] recentPosts; 247 | string rootPath; 248 | string refPath; 249 | string error; 250 | 251 | this(VibeLogSettings settings) 252 | { 253 | vli = VibeLogInfo(settings); 254 | this.settings = settings; 255 | this.rootPath = settings.siteURL.path.toString; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /source/vibelog/webadmin.d: -------------------------------------------------------------------------------- 1 | module vibelog.webadmin; 2 | 3 | public import vibelog.controller; 4 | 5 | import vibelog.config; 6 | import vibelog.post; 7 | import vibelog.user; 8 | 9 | import vibe.http.router; 10 | import vibe.web.web; 11 | import std.exception : enforce; 12 | import std.typecons : Nullable; 13 | 14 | 15 | void registerVibeLogWebAdmin(URLRouter router, VibeLogController controller) 16 | { 17 | auto websettings = new WebInterfaceSettings; 18 | websettings.urlPrefix = (controller.settings.siteURL.path ~ controller.settings.adminPrefix).toString(); 19 | router.registerWebInterface(new VibeLogWebAdmin(controller), websettings); 20 | } 21 | 22 | /// private 23 | /*private*/ final class VibeLogWebAdmin { 24 | private { 25 | VibeLogController m_ctrl; 26 | VibeLogSettings m_settings; 27 | string m_subPath; 28 | string m_config; 29 | } 30 | 31 | this(VibeLogController controller) 32 | { 33 | m_ctrl = controller; 34 | m_settings = controller.settings; 35 | m_subPath = (m_settings.siteURL.path ~ m_settings.adminPrefix).toString(); 36 | } 37 | 38 | void getLogin(string redirect = "") 39 | { 40 | auto info = AdminInfo(AuthInfo.init, m_settings); 41 | render!("vibelog.admin.login.dt", info, redirect); 42 | } 43 | 44 | // the whole admin interface needs authentication 45 | @auth: 46 | 47 | void get(AuthInfo _auth) 48 | { 49 | auto info = AdminInfo(_auth, m_settings); 50 | 51 | render!("vibelog.admin.home.dt", info); 52 | } 53 | 54 | // 55 | // Configs 56 | // 57 | 58 | @path("configs/") 59 | void getConfigs(AuthInfo _auth) 60 | { 61 | enforceAuth(_auth.loginUser.isConfigAdmin()); 62 | 63 | auto info = ConfigsInfo(_auth, m_settings); 64 | info.configs = m_ctrl.db.getAllConfigs(); 65 | info.activeConfigName = m_settings.configName; 66 | 67 | render!("vibelog.admin.editconfiglist.dt", info); 68 | } 69 | 70 | @path("configs/:configname/") 71 | void getConfigEdit(string _configname, AuthInfo _auth) 72 | { 73 | enforceAuth(_auth.loginUser.isConfigAdmin()); 74 | 75 | auto info = ConfigEditInfo(_auth, m_settings); 76 | info.config = m_ctrl.db.getConfig(_configname); 77 | info.globalConfig = m_ctrl.db.getConfig("global", true); 78 | 79 | render!("vibelog.admin.editconfig.dt", info); 80 | } 81 | 82 | @path("configs/:configname/") 83 | void postPutConfig(HTTPServerRequest req, string language, string copyrightString, string feedTitle, string feedLink, string feedDescription, string feedImageTitle, string feedImageUrl, string _configname, AuthInfo _auth, string categories = null) 84 | { 85 | import std.string; 86 | 87 | enforceAuth(_auth.loginUser.isConfigAdmin()); 88 | Config cfg = m_ctrl.db.getConfig(_configname); 89 | if( cfg.name == "global" ) 90 | cfg.categories = categories.splitLines(); 91 | else { 92 | cfg.categories = null; 93 | foreach (k, v; req.form.byKeyValue) { 94 | if (k.startsWith("category_")) 95 | cfg.categories ~= k[9 .. $]; 96 | } 97 | } 98 | cfg.language = language; 99 | cfg.copyrightString = copyrightString; 100 | cfg.feedTitle = feedTitle; 101 | cfg.feedLink = feedLink; 102 | cfg.feedDescription = feedDescription; 103 | cfg.feedImageTitle = feedImageTitle; 104 | cfg.feedImageUrl = feedImageUrl; 105 | 106 | m_ctrl.db.setConfig(cfg); 107 | 108 | redirect(m_subPath ~ "configs/"); 109 | } 110 | 111 | @path("configs/:configname/delete") 112 | void postDeleteConfig(string _configname, AuthInfo _auth) 113 | { 114 | enforceAuth(_auth.loginUser.isConfigAdmin()); 115 | m_ctrl.db.deleteConfig(_configname); 116 | redirect(m_subPath ~ "configs/"); 117 | } 118 | 119 | 120 | // 121 | // Users 122 | // 123 | 124 | @path("users/") 125 | void getUsers(AuthInfo _auth) 126 | { 127 | auto info = AdminInfo(_auth, m_settings); 128 | 129 | render!("vibelog.admin.edituserlist.dt", info); 130 | } 131 | 132 | @path("users/:username/") 133 | void getUserEdit(string _username, AuthInfo _auth) 134 | { 135 | auto info = UserEditInfo(_auth, m_settings); 136 | 137 | info.globalConfig = m_ctrl.db.getConfig("global", true); 138 | info.user = m_ctrl.db.getUserByName(_username); 139 | 140 | render!("vibelog.admin.edituser.dt", info); 141 | } 142 | 143 | @path("users/:username/") 144 | void postPutUser(string id, string username, string password, string name, string email, string passwordConfirmation, Nullable!string oldPassword, string _username, HTTPServerRequest req, AuthInfo _auth) 145 | { 146 | import vibelog.internal.passwordhash : generatePasswordHash, validatePasswordHash; 147 | import vibe.data.bson : BsonObjectID; 148 | import std.algorithm.searching : startsWith; 149 | 150 | User usr; 151 | if( id.length > 0 ){ 152 | enforce(_auth.loginUser.isUserAdmin() || username == _auth.loginUser.username, 153 | "You can only change your own account."); 154 | usr = m_ctrl.db.getUser(BsonObjectID.fromHexString(id)); 155 | enforce(usr.username == username, "Cannot change the user name!"); 156 | } else { 157 | enforce(_auth.loginUser.isUserAdmin(), "You are not allowed to add users."); 158 | usr = new User; 159 | usr.username = username; 160 | foreach (u; _auth.users) 161 | enforce(u.username != usr.username, "A user with the specified user name already exists!"); 162 | } 163 | enforce(password == passwordConfirmation, "Passwords do not match!"); 164 | 165 | usr.name = name; 166 | usr.email = email; 167 | 168 | if (password.length) { 169 | enforce((_auth.loginUser.isUserAdmin() && _auth.loginUser._id != usr._id) 170 | || validatePasswordHash(usr.password, oldPassword.get), 171 | "Old password does not match."); 172 | usr.password = generatePasswordHash(password); 173 | } 174 | 175 | if (_auth.loginUser.isUserAdmin()) { 176 | usr.groups = null; 177 | foreach (k, v; req.form.byKeyValue) { 178 | if (k.startsWith("group_")) 179 | usr.groups ~= k[6 .. $]; 180 | } 181 | 182 | usr.allowedCategories = null; 183 | foreach (k, v; req.form.byKeyValue) { 184 | if (k.startsWith("category_")) 185 | usr.allowedCategories ~= k[9 .. $]; 186 | } 187 | } 188 | 189 | if( id.length > 0 ){ 190 | m_ctrl.db.modifyUser(usr); 191 | } else { 192 | usr._id = m_ctrl.db.addUser(usr); 193 | } 194 | 195 | if (_auth.loginUser.isUserAdmin()) redirect(m_subPath~"users/"); 196 | else redirect(m_subPath); 197 | } 198 | 199 | @path("users/:username/delete") 200 | void postDeleteUser(string _username, AuthInfo _auth) 201 | { 202 | enforceAuth(_auth.loginUser.isUserAdmin(), "You are not authorized to delete users!"); 203 | enforce(_auth.loginUser.username != _username, "Cannot delete the own user account!"); 204 | foreach (usr; _auth.users) 205 | if (usr.username == _username) { 206 | m_ctrl.db.deleteUser(usr._id); 207 | redirect(m_subPath ~ "users/"); 208 | return; 209 | } 210 | 211 | // fall-through (404) 212 | } 213 | 214 | @path("users/") 215 | void postAddUser(string username, AuthInfo _auth) 216 | { 217 | enforceAuth(_auth.loginUser.isUserAdmin(), "You are not authorized to add users!"); 218 | if (username !in _auth.users) { 219 | auto u = new User; 220 | u.username = username; 221 | m_ctrl.db.addUser(u); 222 | } 223 | redirect(m_subPath ~ "users/" ~ username ~ "/"); 224 | } 225 | 226 | // 227 | // Posts 228 | // 229 | 230 | @path("posts/") 231 | void getPosts(AuthInfo _auth) 232 | { 233 | auto info = PostsInfo(_auth, m_settings); 234 | m_ctrl.db.getAllPosts(0, (size_t idx, Post post){ 235 | if (_auth.loginUser.isPostAdmin() || post.author == _auth.loginUser.username 236 | || _auth.loginUser.mayPostInCategory(post.category)) 237 | { 238 | info.posts ~= post; 239 | } 240 | return true; 241 | }); 242 | 243 | render!("vibelog.admin.editpostslist.dt", info); 244 | } 245 | 246 | @path("make_post") 247 | void getMakePost(AuthInfo _auth, string _error = null) 248 | { 249 | auto info = PostEditInfo(_auth, m_settings); 250 | info.globalConfig = m_ctrl.db.getConfig("global", true); 251 | info.error = _error; 252 | 253 | render!("vibelog.admin.editpost.dt", info); 254 | } 255 | 256 | @auth @errorDisplay!getMakePost 257 | void postMakePost(bool isPublic, bool commentsAllowed, string author, 258 | string date, string category, string slug, string headerImage, string header, string subHeader, 259 | string summaryImage, string summary, string summaryTitle, string content, string filters, AuthInfo _auth) 260 | { 261 | postPutPost(null, isPublic, commentsAllowed, author, date, category, slug, headerImage, 262 | header, subHeader, summaryImage, summary, summaryTitle, content, filters, null, _auth); 263 | } 264 | 265 | @path("posts/:postname/") 266 | void getEditPost(string _postname, AuthInfo _auth, string _error = null) 267 | { 268 | auto info = PostEditInfo(_auth, m_settings); 269 | info.globalConfig = m_ctrl.db.getConfig("global", true); 270 | info.post = m_ctrl.db.getPost(_postname); 271 | info.files = m_ctrl.db.getFiles(_postname); 272 | info.error = _error; 273 | render!("vibelog.admin.editpost.dt", info); 274 | } 275 | 276 | @path("posts/:postname/delete") 277 | void postDeletePost(string id, string _postname, AuthInfo _auth) 278 | { 279 | import vibe.data.bson : BsonObjectID; 280 | // FIXME: check permissons! 281 | auto bid = BsonObjectID.fromHexString(id); 282 | m_ctrl.db.deletePost(bid); 283 | redirect(m_subPath ~ "posts/"); 284 | } 285 | 286 | @path("posts/:postname/") @errorDisplay!getEditPost 287 | void postPutPost(string id, bool isPublic, bool commentsAllowed, string author, 288 | string date, string category, string slug, string headerImage, string header, string subHeader, 289 | string summaryImage, string summary, string summaryTitle, string content, 290 | string filters, string _postname, AuthInfo _auth) 291 | { 292 | import vibe.data.bson : BsonObjectID; 293 | 294 | Post p; 295 | if( id.length > 0 ){ 296 | p = m_ctrl.db.getPost(BsonObjectID.fromHexString(id)); 297 | enforce(_postname == p.name, "URL does not match the edited post!"); 298 | } else { 299 | p = new Post; 300 | p.category = "general"; 301 | p.date = Clock.currTime().toUTC(); 302 | } 303 | enforce(_auth.loginUser.mayPostInCategory(category), "You are now allowed to post in the '"~category~"' category."); 304 | 305 | p.isPublic = isPublic; 306 | p.commentsAllowed = commentsAllowed; 307 | p.author = author; 308 | p.date = SysTime.fromSimpleString(date); 309 | p.category = category; 310 | p.slug = slug.length ? slug : header.length ? makeSlugFromHeader(header) : id; 311 | p.headerImage = headerImage; 312 | p.header = header; 313 | p.subHeader = subHeader; 314 | p.summaryImage = summaryImage; 315 | p.summary = summary; 316 | p.summaryTitle = summaryTitle; 317 | p.content = content; 318 | import std.array : split; 319 | p.filters = filters.split(); 320 | 321 | enforce(!m_ctrl.db.hasPost(p.slug) || m_ctrl.db.getPost(p.slug).id == p.id, "Post slug is already used for another article."); 322 | 323 | if( id.length > 0 ) 324 | { 325 | m_ctrl.db.modifyPost(p); 326 | _postname = p.name; 327 | } 328 | else 329 | { 330 | p.id = m_ctrl.db.addPost(p); 331 | } 332 | redirect(m_subPath~"posts/"); 333 | } 334 | 335 | @path("posts/:postname/files/:filename/delete") @errorDisplay!getEditPost 336 | void postDeleteFile(string _postname, string _filename, AuthInfo _auth) 337 | { 338 | m_ctrl.db.removeFile(_postname, _filename); 339 | redirect("../../"); 340 | } 341 | 342 | @path("posts/:postname/files/") @errorDisplay!getEditPost 343 | void postUploadFile(string _postname, HTTPServerRequest req, AuthInfo _auth) 344 | { 345 | import vibe.core.file; 346 | import vibe.stream.operations : readAll; 347 | 348 | import vibe.core.log; 349 | logInfo("FILES %s %s", req.files.length, req.files.getAll("files")); 350 | foreach (f; req.files.byValue) { 351 | logInfo("FILE %s", f.filename.name); 352 | auto fil = openFile(f.tempPath, FileMode.read); 353 | scope (exit) fil.close(); 354 | m_ctrl.db.addFile(_postname, f.filename.name, fil.readAll()); 355 | } 356 | redirect("../"); 357 | } 358 | 359 | private enum auth = before!performAuth("_auth"); 360 | 361 | private AuthInfo performAuth(HTTPServerRequest req, HTTPServerResponse res) 362 | { 363 | import vibe.inet.webform : formEncode; 364 | 365 | string uname = req.session ? req.session.get("vibelog.loggedInUser", "") : ""; 366 | User[string] users = m_ctrl.db.getAllUsers(); 367 | auto pu = uname in users; 368 | if (pu is null) { 369 | redirect(m_subPath ~ "login?"~formEncode(["redirect": (cast(PosixPath)req.requestPath).toString()])); 370 | return AuthInfo.init; 371 | } 372 | enforceHTTP(pu !is null, HTTPStatus.forbidden, "Not authorized to access this page."); 373 | return AuthInfo(*pu, users); 374 | } 375 | 376 | mixin PrivateAccessProxy; 377 | } 378 | 379 | struct AdminInfo 380 | { 381 | import vibelog.info : VibeLogInfo; 382 | VibeLogInfo vli; 383 | alias vli this; 384 | 385 | User loginUser; 386 | User[string] users; 387 | InetPath rootPath, managePath; 388 | string loginError; 389 | 390 | import vibelog.settings : VibeLogSettings; 391 | this(AuthInfo auth, VibeLogSettings settings) 392 | { 393 | vli = VibeLogInfo(settings); 394 | loginUser = auth.loginUser; 395 | users = auth.users; 396 | this.settings = settings; 397 | rootPath = settings.siteURL.path; 398 | managePath = rootPath ~ settings.adminPrefix; 399 | } 400 | } 401 | 402 | enum string mixAdminInfo = q{AdminInfo ai; alias ai this;}; 403 | enum string mixInitAdminInfo = q{ai = AdminInfo(auth, settings);}; 404 | 405 | struct PostEditInfo 406 | { 407 | mixin(mixAdminInfo); 408 | 409 | import vibelog.config : Config; 410 | Config globalConfig; 411 | 412 | import vibelog.post : Post; 413 | Post post; 414 | 415 | string[] files; 416 | string error; 417 | 418 | import vibelog.settings : VibeLogSettings; 419 | this(AuthInfo auth, VibeLogSettings settings) 420 | { 421 | mixin(mixInitAdminInfo); 422 | } 423 | } 424 | 425 | struct ConfigEditInfo 426 | { 427 | mixin(mixAdminInfo); 428 | 429 | Config config; 430 | Config globalConfig; 431 | 432 | import vibelog.settings : VibeLogSettings; 433 | this(AuthInfo auth, VibeLogSettings settings, Config config, Config globalConfig) 434 | { 435 | this(auth, settings); 436 | this.config = config; 437 | this.globalConfig = globalConfig; 438 | } 439 | 440 | this(AuthInfo auth, VibeLogSettings settings) 441 | { 442 | mixin(mixInitAdminInfo); 443 | } 444 | } 445 | 446 | struct ConfigsInfo 447 | { 448 | mixin(mixAdminInfo); 449 | 450 | import vibelog.config : Config; 451 | Config[] configs; 452 | string activeConfigName; 453 | 454 | import vibelog.settings : VibeLogSettings; 455 | this(AuthInfo auth, VibeLogSettings settings, Config[] configs, string activeConfigName) 456 | { 457 | this(auth, settings); 458 | this.configs = configs; 459 | this.activeConfigName = activeConfigName; 460 | } 461 | this(AuthInfo auth, VibeLogSettings settings) 462 | { 463 | mixin(mixInitAdminInfo); 464 | } 465 | } 466 | 467 | struct UserEditInfo 468 | { 469 | mixin(mixAdminInfo); 470 | 471 | import vibelog.config : Config; 472 | Config globalConfig; 473 | 474 | import vibelog.user : User; 475 | User user; 476 | 477 | import vibelog.settings : VibeLogSettings; 478 | this(AuthInfo auth, VibeLogSettings settings) 479 | { 480 | mixin(mixInitAdminInfo); 481 | } 482 | } 483 | 484 | struct PostsInfo 485 | { 486 | mixin(mixAdminInfo); 487 | 488 | import vibelog.post : Post; 489 | Post[] posts; 490 | 491 | import vibelog.settings : VibeLogSettings; 492 | this(AuthInfo auth, VibeLogSettings settings) 493 | { 494 | mixin(mixInitAdminInfo); 495 | } 496 | } 497 | 498 | private struct AuthInfo { 499 | User loginUser; 500 | User[string] users; 501 | } 502 | 503 | private void enforceAuth(bool cond, lazy string message = "Not authorized to perform this action!") 504 | { 505 | if (!cond) throw new HTTPStatusException(HTTPStatus.forbidden, message); 506 | } -------------------------------------------------------------------------------- /views/layout.dt: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | - string title; 5 | - string sep = " - "; 6 | block title 7 | title= title ~ sep ~ info.settings.blogName 8 | link(rel="stylesheet", type="text/css", href="#{info.settings.rootDir}styles/common.css") 9 | block layout.head 10 | body 11 | header 12 | block layout.header 13 | block layout.content 14 | -------------------------------------------------------------------------------- /views/mail.new_comment.dt: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html 3 | head 4 | title New comment 5 | body 6 | p A new comment has been made on 7 | a(href="#{post.slug}") "#{post.header}" 8 | | by 9 | a(href="mailto:#{comment.authorMail}")= comment.authorName 10 | |. 11 | 12 | p Homepage: #{comment.authorHomepage} 13 | 14 | p IP: #{comment.authorIP} 15 | 16 | p Comment: #{comment.content} 17 | -------------------------------------------------------------------------------- /views/vibelog.admin.editconfig.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.admin.layout 2 | 3 | block vibelog.title 4 | - title = "Edit configuration '"~info.config.name~"'"; 5 | 6 | block vibelog-localnav 7 | ul.admin-local-nav 8 | li 9 | a(href="../") Back to config list 10 | 11 | block vibelog-content 12 | form(action="./", method="POST") 13 | - import std.array; 14 | - if( info.config.name == "global" ) 15 | p 16 | label(for="categories") Categories 17 | br 18 | textarea(name="categories", rows="10", cols="40")= join(info.config.categories, "\n") 19 | - else 20 | p Categories 21 | p 22 | - foreach( grp; info.globalConfig.categories ) 23 | - if( info.config.hasCategory(grp) ) 24 | input(type="checkbox", name="category_#{grp}", value="1", checked) 25 | - else 26 | input(type="checkbox", name="category_#{grp}", value="1") 27 | label(for="category_#{grp}")= grp 28 | br 29 | 30 | p 31 | label(for="language") Language 32 | input(type="text", name="language", value="#{info.config.language}") 33 | 34 | p 35 | label(for="copyrightString") Copyright String 36 | input(type="text", name="copyrightString", value="#{info.config.copyrightString}") 37 | 38 | p 39 | label(for="feedTitle") Feed title 40 | input(type="text", name="feedTitle", value="#{info.config.feedTitle}") 41 | 42 | p 43 | label(for="feedLink") Feed link 44 | input(type="url", name="feedLink", value="#{info.config.feedLink}") 45 | 46 | p 47 | label(for="feedDescription") Feed description 48 | input(type="text", name="feedDescription", value="#{info.config.feedDescription}") 49 | 50 | p 51 | label(for="feedImageTitle") Feed image title 52 | input(type="text", name="feedImageTitle", value="#{info.config.feedImageTitle}") 53 | 54 | p 55 | label(for="feedImageUrl") Feed image URL 56 | input(type="url", name="feedImageUrl", value="#{info.config.feedImageUrl}") 57 | 58 | input(type="submit", value="Apply changes") 59 | -------------------------------------------------------------------------------- /views/vibelog.admin.editconfiglist.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.admin.layout 2 | 3 | block vibelog.title 4 | - title = "Configuration overview"; 5 | 6 | block vibelog-localnav 7 | ul.admin-local-nav 8 | li 9 | a(href="../") Back to admin panel 10 | 11 | block vibelog-content 12 | - import std.datetime; 13 | 14 | table(width="100%") 15 | tr 16 | th Name 17 | th Edit 18 | th Delete 19 | - foreach (cfg; info.configs) 20 | tr 21 | td= cfg.name 22 | - if (cfg.name == info.activeConfigName) 23 | | (active) 24 | td 25 | form(action="#{cfg.name}/", method="GET") 26 | input(type="submit", value="edit") 27 | td 28 | form(action="#{cfg.name}/delete", method="POST") 29 | input(type="submit", value="delete") 30 | -------------------------------------------------------------------------------- /views/vibelog.admin.editpost.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.admin.layout 2 | 3 | block vibelog.title 4 | - import std.datetime : Clock; 5 | - import core.time : usecs; 6 | - title = (info.post ? "Modify" : "Add new") ~ " blog post"; 7 | script(type="text/javascript", src="#{info.settings.rootDir}scripts/jquery.js") 8 | script(type="text/javascript", src="#{info.settings.rootDir}scripts/vibelog-edit.js") 9 | script(type="text/javascript"). 10 | window.rootDir = "#{info.settings.rootDir}"; 11 | 12 | block vibelog-localnav 13 | ul.admin-local-nav 14 | li 15 | a(href='#{info.post ? "../" : "./"}') Cancel 16 | 17 | block vibelog-content 18 | - if (info.error.length) 19 | p.error= info.error 20 | 21 | - if (!info.post || info.post.author == info.loginUser.username || info.loginUser.isPostAdmin()) 22 | form(action='#{info.post ? "./" : "make_post"}', method="POST") 23 | input(type="hidden", name="id", value="#{info.post ? info.post.id.toString() : null}") 24 | table.blind 25 | col.caption 26 | tr 27 | td 28 | label(for="isPublic") Public 29 | td 30 | - if( info.post && info.post.isPublic ) 31 | input#isPublic.checkbox(type="checkbox", name="isPublic", value="yes", checked) 32 | - else 33 | input#isPublic.checkbox(type="checkbox", name="isPublic", value="yes") 34 | tr 35 | td 36 | label(for="commentsAllowed") Comments allowed 37 | td 38 | - if( !info.post || info.post.commentsAllowed ) 39 | input#commentsAllowed.checkbox(type="checkbox", name="commentsAllowed", value="yes", checked) 40 | - else 41 | input#commentsAllowed.checkbox(type="checkbox", name="commentsAllowed", value="yes") 42 | tr 43 | - if (info.loginUser.isPostAdmin()) 44 | td 45 | label(for="author") Author 46 | td 47 | select#author(name="author", size="1") 48 | - foreach (usr; info.users) 49 | - if (usr.username == (info.post ? info.post.author : info.loginUser.username)) 50 | option(value="#{usr.username}", selected) #{usr.name} (#{usr.username}) 51 | - else 52 | option(value="#{usr.username}") #{usr.name} (#{usr.username}) 53 | - else 54 | td Author 55 | td #{info.loginUser.name} (#{info.loginUser.username}) 56 | input(type="hidden", name="author", value="#{info.loginUser.username}") 57 | tr 58 | td 59 | label(for="category") Category 60 | td 61 | select#category(name="category", size="1") 62 | - foreach (cat; info.globalConfig.categories) 63 | - if (info.loginUser.isPostAdmin() || info.loginUser.mayPostInCategory(cat)) 64 | - if (info.post && cat == info.post.category) 65 | option(value="#{cat}", selected)= cat 66 | - else 67 | option(value="#{cat}")= cat 68 | tr 69 | td 70 | label(for="date") Date 71 | td 72 | input#date(type="text", name="date", value="#{info.post ? info.post.date.toSimpleString() : Clock.currTime().toSimpleString()}") 73 | tr 74 | td 75 | label(for="slug") Post slug 76 | td 77 | input#slug(type="text", name="slug", value="#{info.post ? info.post.slug : null}") 78 | tr 79 | td 80 | label(for="headerImage-field") Header image 81 | td 82 | input#headerImage-field(type="text", name="headerImage", value="#{info.post ? info.post.headerImage : null}") 83 | tr 84 | td(colspan=2) 85 | hr 86 | tr 87 | td 88 | label(for="summaryImage-field") Summary image 89 | td 90 | input#summaryImage-field(type="text", name="summaryImage", value="#{info.post ? info.post.summaryImage : null}") 91 | tr 92 | td 93 | label(for="summary-title-field") Summary title 94 | td 95 | input#summary-title-field(type="text", name="summaryTitle", value="#{info.post ? info.post.summaryTitle : null}", maxlength=70) 96 | tr 97 | td 98 | label(for="summary-field") Summary 99 | td 100 | input#summary-field(type="text", name="summary", value="#{info.post ? info.post.summary : null}", maxlength=240) 101 | tr 102 | td(colspan=2) Note: Summary fields are used to generate the meta tags used for link previews 103 | tr 104 | td(colspan=2) 105 | hr 106 | tr 107 | td 108 | label(for="header-field") Heading 109 | td 110 | input#header-field(type="text", name="header", value="#{info.post ? info.post.header : null}") 111 | tr 112 | td 113 | label(for="subHeader") Sub-Heading 114 | td 115 | textarea#subHeader(cols="80", rows="5", name="subHeader")= info.post ? info.post.subHeader : null 116 | tr 117 | td Article text 118 | p 119 | input#preview-checkbox.checkbox(type="checkbox", onchange="previewUpdate();", style="width: auto;", autocomplete="off") 120 | label(for="preview-checkbox") Preview 121 | p 122 | label(for="filters-field") Filters 123 | - import std.array; 124 | input#filters-field(type="text", name="filters", onchange="previewUpdate();", style="width: auto;", autocomplete="off", value='#{info.post ? info.post.filters.join(" ") : "markdown"}') 125 | td 126 | #message-area 127 | textarea#message(name="content", cols="80", rows="40")= info.post ? info.post.content : null 128 | #message-preview(style="display: none;") 129 | input(type="submit", value='#{info.post ? "Apply changes" : "Create post!"}') 130 | 131 | - if (info.post) 132 | h2 Files 133 | 134 | - if (info.files.length) 135 | table 136 | tr 137 | th Name 138 | th Action 139 | - foreach (f; info.files) 140 | tr 141 | td= f 142 | td 143 | form(action="files/#{f}/delete", method="POST") 144 | button(type="submit") Delete 145 | - else 146 | p No files uploaded for this post. 147 | 148 | h3 Upload files 149 | form(action="files/", method="POST", enctype="multipart/form-data") 150 | input(type="file", name="files", multiple) 151 | button(type="submit") Upload 152 | 153 | - else 154 | p.error You are not the author of this post and/or are not authorized to change it. 155 | 156 | p Public: #{info.post.isPublic ? "yes" : "no"} 157 | p Author: #{info.post.author} 158 | p Heading: #{info.post.header} 159 | p Sub-Heading: #{info.post.subHeader} 160 | p Content: 161 | pre= info.post.content 162 | -------------------------------------------------------------------------------- /views/vibelog.admin.editpostslist.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.admin.layout 2 | 3 | block vibelog.title 4 | - title = "Post overview"; 5 | 6 | block vibelog-localnav 7 | ul.admin-local-nav 8 | li 9 | a(href="../") Back to admin panel 10 | li 11 | a(href="../make_post") Make new post 12 | 13 | block vibelog-content 14 | - import std.datetime; 15 | 16 | table(width="100%") 17 | tr 18 | th Date 19 | th Pub 20 | th Comment 21 | th Header 22 | th Author 23 | th Category 24 | th View 25 | th Edit 26 | th Delete 27 | - foreach (post; info.posts) 28 | - if (info.loginUser.isPostAdmin() || info.loginUser.username == post.author) 29 | tr 30 | td= (cast(Date)post.date).toSimpleString() 31 | td= post.isPublic ? "yes" : "" 32 | td= post.commentsAllowed ? "yes" : "" 33 | td= post.header 34 | td= post.author 35 | td= post.category 36 | td 37 | form(action="#{info.settings.siteURL}posts/#{post.name}", method="GET") 38 | input(type="submit", value="view") 39 | td 40 | form(action="#{post.name}/", method="GET") 41 | input(type="submit", value="edit") 42 | td 43 | form(action="#{post.name}/delete", method="POST") 44 | input(type="hidden", name="id", value="#{post.id.toString()}") 45 | input(type="submit", value="delete") 46 | -------------------------------------------------------------------------------- /views/vibelog.admin.edituser.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.admin.layout 2 | 3 | block vibelog.title 4 | - title = "Edit user '"~info.user.username~"'"; 5 | 6 | block vibelog-localnav 7 | ul.admin-local-nav 8 | li 9 | a(href="../") Back to user list 10 | 11 | block vibelog-content 12 | - import std.datetime; 13 | 14 | form(action="./", method="POST") 15 | input(type="hidden", name="id", value="#{info.user._id.toString()}") 16 | p 17 | label(for="username") Username 18 | input(type="text", name="username", value="#{info.user.username}") 19 | p 20 | label(for="name") Full name 21 | input(type="text", name="name", value="#{info.user.name}") 22 | p 23 | label(for="email") Email 24 | input(type="email", name="email", value="#{info.user.email}") 25 | - if (!info.loginUser.isUserAdmin()) 26 | p 27 | label(for="oldPassword") Current Password 28 | input(type="password", name="oldPassword", value="") 29 | p 30 | label(for="password") New Password 31 | input(type="password", name="password", value="") 32 | p 33 | label(for="passwordConfirmation") Confim new password 34 | input(type="password", name="passwordConfirmation", value="") 35 | - if (info.loginUser.isUserAdmin()) 36 | p Groups: 37 | p 38 | - foreach( grp; info.globalConfig.groups ) 39 | - if( info.user.inGroup(grp) ) 40 | input.checkbox(type="checkbox", name="group_#{grp}", value="1", checked) 41 | - else 42 | input.checkbox(type="checkbox", name="group_#{grp}", value="1") 43 | label(for="group_#{grp}")= grp 44 | br 45 | p Allowed categories: 46 | p 47 | - foreach( grp; info.globalConfig.categories ) 48 | - if( info.user.mayPostInCategory(grp) ) 49 | input.checkbox(type="checkbox", name="category_#{grp}", value="1", checked) 50 | - else 51 | input.checkbox(type="checkbox", name="category_#{grp}", value="1") 52 | label(for="category_#{grp}")= grp 53 | br 54 | input(type="submit", value="Apply changes") 55 | -------------------------------------------------------------------------------- /views/vibelog.admin.edituserlist.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.admin.layout 2 | 3 | block vibelog.title 4 | - title = "User overview"; 5 | 6 | block vibelog-localnav 7 | ul.admin-local-nav 8 | li 9 | a(href="../") Back to admin panel 10 | 11 | block vibelog-content 12 | - import std.datetime; 13 | 14 | form(action="./", method="POST") 15 | p Add user: 16 | input(type="text", name="username") 17 | input(type="submit", value="create") 18 | 19 | - if (info.loginUser.isUserAdmin()) 20 | table(width="100%") 21 | tr 22 | th Username 23 | th Full name 24 | th Edit 25 | th Delete 26 | - foreach (usr; info.users) 27 | tr 28 | td= usr.username 29 | td= usr.name 30 | td 31 | form(action="#{usr.username}/", method="GET") 32 | input(type="submit", value="edit") 33 | td 34 | form(action="#{usr.username}/delete", method="POST") 35 | input(type="submit", value="delete") 36 | - else 37 | p Sorry, you are not authorized to edit users. You can, however, 38 | a(href="#{info.loginUser.username}/") edit your own account 39 | |. 40 | -------------------------------------------------------------------------------- /views/vibelog.admin.home.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.admin.layout 2 | 3 | block vibelog.title 4 | - title = "VibeLog Admin Panel"; 5 | 6 | block vibelog-content 7 | section#actions.vibelogAdminSection 8 | h2 Dashboard 9 | -------------------------------------------------------------------------------- /views/vibelog.admin.inc.nav.dt: -------------------------------------------------------------------------------- 1 | - import vibe.core.path : relativeTo; 2 | - auto tmp = req.requestPath; 3 | - if (!tmp.endsWithSlash) tmp = tmp.parentPath; 4 | - tmp = info.managePath.relativeTo(tmp); 5 | - auto root_dir = tmp.toString(); 6 | - if (!root_dir.length) root_dir = "./"; 7 | 8 | ul.headerMenu.admin-global-nav 9 | li 10 | a(href=root_dir) Dashboard 11 | li 12 | a(href="#{root_dir}make_post") New post 13 | li 14 | a(href="#{root_dir}posts/") Manage posts 15 | - if (info.loginUser.isConfigAdmin()) 16 | li 17 | a(href="#{root_dir}configs/") Manage configurations 18 | - if (info.loginUser.isUserAdmin()) 19 | li 20 | a(href="#{root_dir}users/") Manage users 21 | - else 22 | li 23 | a(href="#{root_dir}users/#{info.loginUser.username}/") Manage account 24 | -------------------------------------------------------------------------------- /views/vibelog.admin.layout.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.layout 2 | block vibelog.layout.head 3 | link(rel="stylesheet", type="text/css", href="#{info.settings.rootDir}styles/menu.css") 4 | block vibelog.admin.layout.head 5 | block vibelog.layout.header 6 | div#menuContainer 7 | include vibelog.admin.inc.nav 8 | block vibelog.admin.layout.header 9 | block vibelog.layout.content 10 | #vibelogContent 11 | block vibelog-localnav 12 | block vibelog-content 13 | block vibelog.admin.layout.content 14 | -------------------------------------------------------------------------------- /views/vibelog.admin.login.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.blocks 2 | 3 | block vibelog-content 4 | form(method="POST", action="#{info.rootPath}login") 5 | input(type="hidden", name="redirect", value="#{info.managePath}") 6 | label(for="username") User name 7 | input#username(type="text", name="username") 8 | label(for="password") Password 9 | input#password(type="password", name="password") 10 | button(type="submit") Log in -------------------------------------------------------------------------------- /views/vibelog.blocks.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.layout 2 | 3 | block vibelog.layout.content 4 | #vibelogRecentList 5 | block vibelog-recent-list 6 | 7 | block vibelog-content 8 | -------------------------------------------------------------------------------- /views/vibelog.inc.header.dt: -------------------------------------------------------------------------------- 1 | - if (req.session && req.session.get("vibelog.loggedInUser", "").length) 2 | .vibelog-user-area 3 | p: a(href="#{info.rootPath}manage") Manage 4 | p: a(href="#{req.rootDir}logout") Log out 5 | - else 6 | form.vibelog-login-form(method="POST", action="#{req.rootDir}login") 7 | input(type="hidden", name="redirect", value=req.path) 8 | input(type="text", name="username", placeholder="User name") 9 | input(type="password", name="password", placeholder="Password") 10 | button(type="submit") Log in 11 | .error= info.loginError 12 | h1= info.settings.blogName 13 | h2= info.settings.blogDescription 14 | -------------------------------------------------------------------------------- /views/vibelog.inc.headlinelist.dt: -------------------------------------------------------------------------------- 1 | - import vibelog.controller; 2 | - void insertVibelogHeadlineList(VibelogHeadlineListConfig config) 3 | - import vibe.inet.message; 4 | - import std.algorithm : max, min; 5 | 6 | .blogHeadlineList 7 | - foreach (i, post; info.posts[0 .. min($, config.maxPosts)]) 8 | .blogHeadlineEntry 9 | - if (config.dateFirst) 10 | .blogPostDate= toRFC822DateString(post.date) 11 | - auto hlvl = config.headerLevel; 12 | - if (config.headerLinks) 13 | |#{post.header} 14 | - else 15 | |#{post.header} 16 | - if (!config.dateFirst) 17 | .blogPostDate= toRFC822DateString(post.date) 18 | - if (config.showSummaries) 19 | .blogPostHeaderContent 20 | - if (post.headerImage.length) 21 | img.headerImage(alt="Header image", src="#{post.headerImage}") 22 | .blogPostHeaderText(class='#{post.headerImage.length ? "with-image" : "without-image"}') 23 | |#{post.subHeader} 24 | - if (config.footerLinks) 25 | a(href="#{info.settings.rootDir}posts/#{post.name}")& Read more… 26 | -------------------------------------------------------------------------------- /views/vibelog.inc.postlist.dt: -------------------------------------------------------------------------------- 1 | - import vibelog.post; 2 | 3 | - void insertVibelogPostList() 4 | - int year(Post p){ return p.date.year; } 5 | - Post[][] splitYears(Post[] posts) 6 | - Post[][] ret; 7 | - while( posts.length ) 8 | - if( ret.length == 0 || year(posts[0]) != year(ret[$-1][0]) ) 9 | - ret ~= [posts[0]]; 10 | - else 11 | - ret[$-1] ~= posts[0]; 12 | - posts = posts[1 ..$]; 13 | - return ret; 14 | 15 | ul 16 | - foreach( yr; splitYears(info.recentPosts) ) 17 | li 18 | | #{year(yr[0])} 19 | ul 20 | - foreach( p; yr ) 21 | li 22 | a(href="#{info.settings.rootDir}posts/#{p.name}")= p.header 23 | -------------------------------------------------------------------------------- /views/vibelog.layout.dt: -------------------------------------------------------------------------------- 1 | extends layout 2 | block title 3 | block vibelog.title 4 | block layout.head 5 | block vibelog.layout.head 6 | block layout.header 7 | include vibelog.inc.header 8 | block vibelog.layout.header 9 | block layout.content 10 | block vibelog.layout.content 11 | -------------------------------------------------------------------------------- /views/vibelog.post.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.blocks 2 | 3 | block vibelog.title 4 | - title = info.post.header; 5 | 6 | block vibelog.layout.head 7 | meta(property="og:type", content="article") 8 | meta(property="og:title", content=info.post.summaryTitle.length ? info.post.summaryTitle : info.post.header) 9 | - if (info.post.summary.length) 10 | meta(property="og:description", content=info.post.summary) 11 | meta(property="og:image", content=info.post.summaryImage.length ? info.post.summaryImage : info.post.headerImage) 12 | meta(property="twitter:card", content="summary_large_image") 13 | meta(property="twitter:site", content="@bildhuus") 14 | 15 | block vibelog-content 16 | - import std.datetime; 17 | - import vibe.inet.message; 18 | 19 | #vibelogContent 20 | - if (info.settings.enableBackButton) 21 | a.backButton(href="../?page=#{info.postPage}") Back 22 | 23 | .blogSinglePost 24 | .blogPost 25 | header 26 | .blogPostHeaderContent 27 | - if (info.post.headerImage.length && info.settings.placePostHeaderImageFirst) 28 | img.headerImage(alt="Header image", src="#{info.post.headerImage}") 29 | h1= info.post.header 30 | - if (info.post.headerImage.length && !info.settings.placePostHeaderImageFirst) 31 | img.headerImage(alt="Header image", src="#{info.post.headerImage}") 32 | .blogPostHeaderText(class='#{info.post.headerImage.length ? "with-image" : "without-image"}') 33 | span.blogPostDate= toRFC822DateString(info.post.date) 34 | | #{info.post.subHeader} 35 | section.blogPostContent(class='#{info.post.headerImage.length ? "with-image" : "without-image"}') 36 | != info.post.renderContentAsHtml(info.settings, info.refPath, 1) 37 | 38 | footer 39 | p Posted at #{toRFC822TimeString(info.post.date)} by #{info.post.author in info.users ? info.users[info.post.author].name : info.post.author} 40 | 41 | section.comments 42 | - if (!info.post.commentsAllowed) 43 | p Comments for the post are currently disabled. 44 | 45 | include diskuto.inc.comments 46 | - includeDiskuto(info.diskuto, "vibelog-"~info.post.id.toString()); 47 | 48 | block vibelog-recent-list 49 | include vibelog.inc.postlist 50 | - insertVibelogPostList(); 51 | -------------------------------------------------------------------------------- /views/vibelog.postlist.dt: -------------------------------------------------------------------------------- 1 | extends vibelog.blocks 2 | 3 | block vibelog.title 4 | - title = "All posts"; 5 | 6 | block vibelog-content 7 | - import vibe.inet.message; 8 | #vibelogContent 9 | - if (info.loginError.length) 10 | .error= info.loginError 11 | 12 | #vibelogPostList 13 | - foreach( i, post; info.posts ) 14 | .blogMultiPost 15 | .blogPost 16 | header 17 | - if (post.headerImage.length && info.settings.placePostHeaderImageFirst) 18 | img.headerImage(alt="Header image", src="#{post.headerImage}") 19 | h2 20 | a(href="posts/#{post.name}")= post.header 21 | .blogPostHeaderContent 22 | - if (post.headerImage.length && !info.settings.placePostHeaderImageFirst) 23 | img.headerImage(alt="Header image", src="#{post.headerImage}") 24 | .blogPostHeaderText(class='#{post.headerImage.length ? "with-image" : "without-image"}') 25 | - import vibe.inet.message : toRFC822DateString; 26 | span.blogPostDate= toRFC822DateString(post.date) 27 | | #{post.subHeader} 28 | - if (!info.settings.showFullPostsInPostList && info.settings.inlineReadMoreButton) 29 | a(href="posts/#{post.name}#trackback") Read more… 30 | - if (info.settings.showFullPostsInPostList) 31 | .blogPostContent(class='#{post.headerImage.length ? "with-image" : "without-image"}') 32 | != post.renderContentAsHtml(info.settings, info.refPath, 2) 33 | footer 34 | - import vibe.inet.message : toRFC822TimeString; 35 | | Posted at #{toRFC822TimeString(post.date)} by #{post.author in info.users ? info.users[post.author].name : post.author} 36 | | #[a(href="posts/#{post.name}#comments") #{info.commentCount[i]} comments] 37 | - if (post.trackbacks.length > 0) 38 | | #[a(href="posts/#{post.name}#trackback") #{post.trackbacks.length} trackbacks] 39 | - else if (!info.settings.inlineReadMoreButton) 40 | a(href="posts/#{post.name}#trackback") Read more… 41 | 42 | .pageNumbers 43 | p Pages: 44 | - foreach( p; 0 .. info.pageCount ) 45 | - if( p == info.pageNumber ) 46 | em #{p+1} 47 | - else 48 | | #[a(href="?page=#{p+1}") #{p+1}] 49 | 50 | block vibelog-recent-list 51 | include vibelog.inc.postlist 52 | - insertVibelogPostList(); 53 | --------------------------------------------------------------------------------