diff --git a/News/AUTHORS b/News/AUTHORS new file mode 100644 index 0000000..00dc043 --- /dev/null +++ b/News/AUTHORS @@ -0,0 +1,9 @@ +Chyrp is written and maintained by the Chyrp Team: + +Lead Developer: + +- Arian Xhezairi + +Project Founder: + +- Alex Suraci diff --git a/News/COPYING b/News/COPYING new file mode 100755 index 0000000..77a1b6e --- /dev/null +++ b/News/COPYING @@ -0,0 +1,27 @@ +Copyright (c) 2011 Chyrp Team (see AUTHORS) and individual contributors. + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +Except as contained in this notice, the name(s) of the above +copyright holders shall not be used in advertising or otherwise +to promote the sale, use or other dealings in this Software +without prior written authorization. diff --git a/News/README.markdown b/News/README.markdown new file mode 100644 index 0000000..10bf69d --- /dev/null +++ b/News/README.markdown @@ -0,0 +1,97 @@ +Chyrp is a blogging engine designed to be lightweight while retaining functionality. It is driven by PHP and MySQL (or SQLite), and has a great standard theme and robust module engine. You can personalize and modify it any way you want. + +All of your content is powered by a unique Feathers system that allows Chyrp to be whatever you want it to be. You can post anything and everything, or just stick to the default Text feather and run a regular blog. Chyrp destroys the fine line between a blog and a tumblelog. + +Requirements +============ +Chyrp will thrive on virtually any server setup, but we guarantee Chyrp to run on no less than: + +* PHP 5 >= 5.2.0 +* MySQL: + - MySQL 4.1+ +* SQLite: + - SQLite 3+ + - PDO + +These requirements are more of guidelines, as these are the earliest versions of the services that we have tested Chyrp on. If you are successfully running Chyrp on an earlier version of these services, let us know. + +Installation +============ +Installing Chyrp is easier than you expect. You can do it in four steps: + +1. If using MySQL, create a MySQL database with a username and password. +2. Download, unzip, and upload. +3. Open your web browser and navigate to where you uploaded Chyrp. +4. Follow through the installer at [index.php](). + +That's it! Chyrp will be up and running and ready for you to use. + +Upgrading +========= +Keeping Chyrp up to date is important to make sure that your blog is as safe and as awesome as possible. + +1. Download the latest version of Chyrp from [http://chyrp.net/](http://chyrp.net/). +2. Copy your config files1 to somewhere safe. +3. Disable any Modules/Feathers that you downloaded for the release you're upgrading from. +4. Overwrite your current Chyrp installation files with the new ones. +5. Restore your config files1 back to /includes/. +6. Upgrade by navigating to [upgrade.php](), and restore any backups. +7. Re-enable your Modules/Feathers. +8. Run the upgrader again. It will run the Module/Feather upgrade tasks. + +1 The config files vary depending on what you're upgrading from. Any of these in are considered "config files": + +* `/includes/config.yaml.php` +* `/includes/database.yaml.php` +* `/includes/config.yml.php` +* `/includes/database.yml.php` +* `/includes/config.php` +* `/includes/database.php` + +Extensions +========== +Chyrp isn't complete without activating a few extensions. Extensions add functionality (ex. audio clips, video, photos) to Chyrp. You can find extensions for Chyrp made by the Chyrp community at [http://chyrp.net/extend](http://chyrp.net/extend). + +Installing Extensions +===================== +To install extensions, you have to determine what type of extension it is. It can be a *module*, a *feather*, a *theme*, or a *localization*. There's a different setup process for each type. + +## Feathers +Feathers add new *post types* to Chyrp. Post types determine what kind of media you can display in your blog. + +1. Download and unzip the feather +2. Upload the feather to the `feathers/` folder. +3. Open your web browser and navigate to your Chyrp administration panel. +4. Click on the *Extend* tab, and then the *Feathers* sub tab. +5. Drag it from the Disabled pane to the Enabled pane. + +You can now use the feather by navigating to the Write tab and choosing the feather you uploaded. + +## Modules +Installing modules is quick, easy, and painless with Chyrp. They add extra functionality to Chyrp. + +1. Download and unzip the module. +2. Upload the module to the `modules/` folder. +3. Open your web browser and navigate to your Chyrp administration panel. +4. Click on the *Extend* tab and drag it from the Disabled pane to the Enabled pane. + +The module is now installed and is ready for action. Keep in mind that some modules may conflict with each other if they do similar tasks. They are marked with red lines between them on the Modules page. + +## Themes +Chyrp makes applying themes to your blog easy. With a single click you can change the look of your blog. + +1. Download and unzip the theme. +2. Upload the theme to the `themes/` folder. Make sure that it is contained in it's own folder. +3. Open your web browser and navigate to your Chyrp administration panel. +4. Click on the *Extend* tab, and then the *Themes* sub tab. +5. Click on the screenshot of the theme you just uploaded to apply it to your blog. + +Chyrp can even show you what the theme will look like before anyone else sees it. In the Themes sub tab, click on the Preview button below the theme screenshot to see the theme. + +## Localization +Chyrp is multilingual! If your first language isn't English, you can apply a new localization to Chyrp to make it speak your language. + +1. Download and unzip the localization. +1. Upload the `.mo` file to the `includes/locale/` folder. You don't need anything else for the translation to work. +1. Open your web browser and navigate to your Chyrp administration panel. +1. Click on the *Settings* tab, and change the *Language* option to the language you just uploaded. diff --git a/News/admin/help.php b/News/admin/help.php new file mode 100644 index 0000000..3aaa673 --- /dev/null +++ b/News/admin/help.php @@ -0,0 +1,82 @@ + + + + + Chyrp: <?php echo $title; ?> + + + +
+
+ +
+ + diff --git a/News/admin/index.php b/News/admin/index.php new file mode 100644 index 0000000..cb30857 --- /dev/null +++ b/News/admin/index.php @@ -0,0 +1,27 @@ +group->can("view_site")) + if ($trigger->exists("can_not_view_site")) + $trigger->call("can_not_view_site"); + else + show_403(__("Access Denied"), __("You are not allowed to view this site.")); + + # Execute the appropriate Controller responder. + $route->init(); + + if (!$route->success and !$admin->displayed) + $admin->display($route->action); # Attempt to display it; it'll go through Modules and Feathers. + + $trigger->call("end", $route); + + ob_end_flush(); diff --git a/News/admin/themes/default/images/icons/add.png b/News/admin/themes/default/images/icons/add.png new file mode 100755 index 0000000..6332fef Binary files /dev/null and b/News/admin/themes/default/images/icons/add.png differ diff --git a/News/admin/themes/default/images/icons/appearance.png b/News/admin/themes/default/images/icons/appearance.png new file mode 100755 index 0000000..73c5b3f Binary files /dev/null and b/News/admin/themes/default/images/icons/appearance.png differ diff --git a/News/admin/themes/default/images/icons/cancel.png b/News/admin/themes/default/images/icons/cancel.png new file mode 100755 index 0000000..c149c2b Binary files /dev/null and b/News/admin/themes/default/images/icons/cancel.png differ diff --git a/News/admin/themes/default/images/icons/delete.png b/News/admin/themes/default/images/icons/delete.png new file mode 100755 index 0000000..ebad933 Binary files /dev/null and b/News/admin/themes/default/images/icons/delete.png differ diff --git a/News/admin/themes/default/images/icons/deny.png b/News/admin/themes/default/images/icons/deny.png new file mode 100755 index 0000000..08f2493 Binary files /dev/null and b/News/admin/themes/default/images/icons/deny.png differ diff --git a/News/admin/themes/default/images/icons/edit.png b/News/admin/themes/default/images/icons/edit.png new file mode 100755 index 0000000..0bfecd5 Binary files /dev/null and b/News/admin/themes/default/images/icons/edit.png differ diff --git a/News/admin/themes/default/images/icons/error.png b/News/admin/themes/default/images/icons/error.png new file mode 100755 index 0000000..628cf2d Binary files /dev/null and b/News/admin/themes/default/images/icons/error.png differ diff --git a/News/admin/themes/default/images/icons/failure.png b/News/admin/themes/default/images/icons/failure.png new file mode 100755 index 0000000..c37bd06 Binary files /dev/null and b/News/admin/themes/default/images/icons/failure.png differ diff --git a/News/admin/themes/default/images/icons/help.png b/News/admin/themes/default/images/icons/help.png new file mode 100755 index 0000000..5c87017 Binary files /dev/null and b/News/admin/themes/default/images/icons/help.png differ diff --git a/News/admin/themes/default/images/icons/info.png b/News/admin/themes/default/images/icons/info.png new file mode 100644 index 0000000..9e90193 Binary files /dev/null and b/News/admin/themes/default/images/icons/info.png differ diff --git a/News/admin/themes/default/images/icons/magnifier.png b/News/admin/themes/default/images/icons/magnifier.png new file mode 100755 index 0000000..cf3d97f Binary files /dev/null and b/News/admin/themes/default/images/icons/magnifier.png differ diff --git a/News/admin/themes/default/images/icons/notice.png b/News/admin/themes/default/images/icons/notice.png new file mode 100755 index 0000000..12cd1ae Binary files /dev/null and b/News/admin/themes/default/images/icons/notice.png differ diff --git a/News/admin/themes/default/images/icons/pixel.gif b/News/admin/themes/default/images/icons/pixel.gif new file mode 100755 index 0000000..35d42e8 Binary files /dev/null and b/News/admin/themes/default/images/icons/pixel.gif differ diff --git a/News/admin/themes/default/images/icons/save.png b/News/admin/themes/default/images/icons/save.png new file mode 100755 index 0000000..44c06dd Binary files /dev/null and b/News/admin/themes/default/images/icons/save.png differ diff --git a/News/admin/themes/default/images/icons/success.png b/News/admin/themes/default/images/icons/success.png new file mode 100755 index 0000000..89c8129 Binary files /dev/null and b/News/admin/themes/default/images/icons/success.png differ diff --git a/News/admin/themes/default/info.yaml b/News/admin/themes/default/info.yaml new file mode 100644 index 0000000..eec468a --- /dev/null +++ b/News/admin/themes/default/info.yaml @@ -0,0 +1,7 @@ +name: Default +version: 2.1 +url: http://chyrp.net/ +description: The default theme provided with Chyrp, which all themes fallback onto. +author: + name: Alex Suraci + url: http://toogeneric.com/ \ No newline at end of file diff --git a/News/admin/themes/default/layout.twig b/News/admin/themes/default/layout.twig new file mode 100644 index 0000000..403197a --- /dev/null +++ b/News/admin/themes/default/layout.twig @@ -0,0 +1,67 @@ + + + + + $site.name: {% block title %}$title{% endblock %} + + + + +${ trigger.call("admin_head") } + + + +
+
+ ${ "Log Out ›" | translate } + ${ "Hello, %s!" | translate | format(visitor.full_name | split | first | fallback(visitor.login | fallback("Guest" | translate))) } + ${ "View Site ›" | translate } +
+
+ +
+
+ {% for notice in flash.notices %} +

$notice

+ {% endfor %} + {% for warning in flash.warnings %} +

$warning

+ {% endfor %} + {% for message in flash.messages %} +

$message

+ {% endfor %} +{% block content %}{% endblock %} +
+
+ + + diff --git a/News/admin/themes/default/pages/bookmarklet.twig b/News/admin/themes/default/pages/bookmarklet.twig new file mode 100644 index 0000000..b336dd7 --- /dev/null +++ b/News/admin/themes/default/pages/bookmarklet.twig @@ -0,0 +1,287 @@ +{% if not done %} + + + + + ${ "Chyrp!" | translate } + + + + + + + {% if site.enabled_feathers | length > 1 %} + +
+ {% endif %} +
+ {% for feather in feathers %} + + {% endfor %} +
+ + +{% else %} {# This one is 100% credited to Tumblr. They did it perfectly, didn't want to muck it up. #} + + + + + ${ "Chyrp!" | translate } + + + + +
+
${ "Done!" | translate }
+ + ${ "Close this window" | translate } + + + ${ "or wait 3 seconds." | translate } + +
+ + + +{% endif %} diff --git a/News/admin/themes/default/pages/content_settings.twig b/News/admin/themes/default/pages/content_settings.twig new file mode 100644 index 0000000..01accbc --- /dev/null +++ b/News/admin/themes/default/pages/content_settings.twig @@ -0,0 +1,69 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Content Settings" | translate }{% endblock %} + +{% block content %} +
+
+

+ + +

+

+ + +

+

+ + + + {% if site.clean_urls %} + ${ "Allows you to set an alternative URL for /feed/, e.g. your feed on FeedBurner." | translate } + {% else %} + ${ "Allows you to set an alternative URL for /?feed, e.g. your feed on FeedBurner." | translate } + {% endif %} + +

+

+ + + + ${ "The directory, relative to your Chyrp install, to upload files to. You can use /../ to go up one directory." | translate } + +

+

+ + + + ${ "Trackbacking allows sites to notify you when they write a new entry, usually because they link to or reference yours." | translate } + +

+

+ + + + ${ "Attempts to notify sites linked to from your posts. It'll slow down things a bit when you submit them, depending on how many links you've got in it." | translate } + +

+

+ + + + ${ "XML-RPC support allows for remote access to your site. This allows you to use remote clients (e.g., MarsEdit or Flickr) to create/edit content on your site." | translate } + +

+

+ + +

+ +

+ +

+ + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/delete_group.twig b/News/admin/themes/default/pages/delete_group.twig new file mode 100644 index 0000000..93ab940 --- /dev/null +++ b/News/admin/themes/default/pages/delete_group.twig @@ -0,0 +1,60 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Delete Group “%s”?" | translate | format(group.name | escape) }{% endblock %} + +{% block content %} +

${ "Are you sure you want to delete the “%s” group?" | translate | format(group.name | escape) }

+
+
+
+ {% if group.members %} +

${ "Members:" | translate }

+
    + {% for member in group.members %} +
  • ${ member.full_name | fallback(member.login) }
  • + {% endfor %} +
+
+ {% if groups %} +

${ "Move members to:" | translate }

+ +
+ {% endif %} +
+ {% endif %} + {% if group.id == site.default_group and groups %} +

${ "New default group:" | translate }

+ +
+
+ {% endif %} + {% if group.id == site.guest_group and groups %} +

${ "New “guest” group:" | translate }

+ +
+
+ {% endif %} +
+ +
+ + +
+ + + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/delete_page.twig b/News/admin/themes/default/pages/delete_page.twig new file mode 100644 index 0000000..6f1d905 --- /dev/null +++ b/News/admin/themes/default/pages/delete_page.twig @@ -0,0 +1,33 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Delete Page “%s”?" | translate | format(page.title | escape) }{% endblock %} + +{% block content %} +

${ "Are you sure you want to delete “%s”?" | translate | format(page.title) }

+
+
+
+

${ "Excerpt" | translate }

+ ${ page.body | truncate(500) } + {% if page.children %} +
+

${ "Delete children?" | translate }

+
    + {% for child in page.children %} +
  • $child.title
  • + {% endfor %} +
+ {% endif %} +
+
+ +
+ + +
+ + + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/delete_post.twig b/News/admin/themes/default/pages/delete_post.twig new file mode 100644 index 0000000..fc8acbc --- /dev/null +++ b/News/admin/themes/default/pages/delete_post.twig @@ -0,0 +1,24 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Delete “%s”?" | translate | format(post.title | escape) }{% endblock %} + +{% block content %} +

${ "Are you sure you want to delete “%s”?" | translate | format(post.title) }

+
+
+
+

${ "Excerpt" | translate }

+ $post.excerpt +
+
+ +
+ + +
+ + + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/delete_user.twig b/News/admin/themes/default/pages/delete_user.twig new file mode 100644 index 0000000..c56da15 --- /dev/null +++ b/News/admin/themes/default/pages/delete_user.twig @@ -0,0 +1,89 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Delete User “%s”?" | translate | format(user.login | escape) }{% endblock %} + +{% block content %} +

${ "Are you sure you want to delete user “%s”?" | translate | format(user.login | escape) }

+
+
+
+

${ "Information" | translate }

+
    + {% if user.full_name %} +
  • ${ "Full Name:" | translate } $user.full_name
  • + {% endif %} +
  • ${ "E-Mail:" | translate } $user.email
  • + {% if user.website %} +
  • ${ "Website:" | translate } $user.website
  • + {% endif %} +
  • ${ "Group:" | translate } $user.group.name
  • +
+
+ {% if user.posts %} +

${ "Posts:" | translate }

+
    + {% for post in user.posts %} +
  • $post.title
  • + {% endfor %} +
+
+ {% if users %} +

+ + ${ "Attribute posts to:" | translate } +

+ +
+ {% endif %} +

+ + ${ "Delete posts." | translate } +

+
+
+ {% endif %} + {% if user.pages %} +

${ "Pages:" | translate }

+
    + {% for page in user.pages %} +
  • $page.title
  • + {% endfor %} +
+
+ {% if users %} +

+ + ${ "Attribute pages to:" | translate } +

+ +
+ {% endif %} +

+ + ${ "Delete pages." | translate } +

+
+
+ {% endif %} +${ trigger.call("delete_user_form") } +
+
+ +
+ + +
+ + + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/edit_group.twig b/News/admin/themes/default/pages/edit_group.twig new file mode 100644 index 0000000..d3a6e1b --- /dev/null +++ b/News/admin/themes/default/pages/edit_group.twig @@ -0,0 +1,36 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Edit Group “%s”" | translate | format(group.name | escape) }{% endblock %} + +{% block content %} + ${ group.delete_link('delete'~ ("Delete" | translate), null, null, "button boo right") } +

${ "Editing Group “%s”" | translate | format(group.name | escape) }

+
+
+

+ + +

+ +

${ "Permissions" | translate }

+

+ +

+
+ {% for permission in permissions %} +

+ + +

+ {% endfor %} +
+ +

+ +

+ + + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/edit_page.twig b/News/admin/themes/default/pages/edit_page.twig new file mode 100644 index 0000000..7432b71 --- /dev/null +++ b/News/admin/themes/default/pages/edit_page.twig @@ -0,0 +1,16 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Edit “%s”" | translate | format(page.title | escape) }{% endblock %} + +{% block content %} + ${ page.delete_link('delete'~ ("Delete" | translate), null, null, "button boo right") } +

${ "Editing “%s”" | translate | format(page.title) }

+
+
+{% include "partials/page_fields.twig" %} + + + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/edit_post.twig b/News/admin/themes/default/pages/edit_post.twig new file mode 100644 index 0000000..a889323 --- /dev/null +++ b/News/admin/themes/default/pages/edit_post.twig @@ -0,0 +1,17 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Edit “%s”" | translate | format(post.title | escape) }{% endblock %} + +{% block content %} + ${ post.delete_link('delete'~ ("Delete" | translate), null, null, "button boo right") } +

${ "Editing “%s”" | translate | format(post.title | escape) }

+
+
+{% include "partials/post_fields.twig" %} + + + + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/edit_user.twig b/News/admin/themes/default/pages/edit_user.twig new file mode 100644 index 0000000..9bab373 --- /dev/null +++ b/News/admin/themes/default/pages/edit_user.twig @@ -0,0 +1,57 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Edit User “%s”" | translate | format(user.login | escape) }{% endblock %} + +{% block content %} + ${ user.delete_link('delete'~ ("Delete" | translate), null, null, "button boo right") } +

${ "Editing User “%s”" | translate | format(user.login | escape) }

+
+
+

${ "Information" | translate }

+

+ + +

+

+ + +

+

+ + +

+ +

${ "Settings" | translate }

+

+ + +

+

+ + +

+

+ + +

+

+ + +

+ ${ trigger.call("edit_user_fields", user) } + +
+ +

+ +

+ + + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/export.twig b/News/admin/themes/default/pages/export.twig new file mode 100644 index 0000000..678d349 --- /dev/null +++ b/News/admin/themes/default/pages/export.twig @@ -0,0 +1,62 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Export" | translate }{% endblock %} + +{% block content %} +

${ "What would you like to export?" | translate }

+
+
+

+ + + + ${ "filter:" | translate } + + help + ${ "(optional)" | translate } + +

+

+ + + + ${ "filter:" | translate } + + help + ${ "(optional)" | translate } + +

+

+ + + + ${ "filter:" | translate } + + help + ${ "(optional)" | translate } + +

+

+ + + + ${ "(warning: this also exports the hashed passwords, keep it safe)" | translate } + + + ${ "filter:" | translate } + + help + ${ "(optional)" | translate } + +

+ + ${ trigger.call("export_choose") } + +

+ +

+ + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/feathers.twig b/News/admin/themes/default/pages/feathers.twig new file mode 100644 index 0000000..52a8250 --- /dev/null +++ b/News/admin/themes/default/pages/feathers.twig @@ -0,0 +1,49 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Feathers" | translate }{% endblock %} + +{% block content %} +
+

${ "Enabled" | translate }

+
    + {% for safename, feather in enabled_feathers | items %} +
  • + + {% if feather.help %} + help + {% endif %} + ${ "%s v%s by %s" | translate | format(feather.url, feather.name | translate(safename), feather.version, feather.author.link) } +
    +
    + $feather.description {# translation is done in the controller #} +
    + +
    +
  • + {% endfor %} +
+
+
+

${ "Disabled" | translate }

+
    + {% for safename, feather in disabled_feathers | items %} +
  • + + {% if feather.help %} + help + {% endif %} + ${ "%s v%s by %s" | translate | format(feather.url, feather.name | translate(safename), feather.version, feather.author.link) } +
    +
    + $feather.description {# translation is done in the controller #} +
    + +
    +
  • + {% endfor %} +
+
+
+
+ ${ "Get More Feathers ›" | translate } +{% endblock %} diff --git a/News/admin/themes/default/pages/general_settings.twig b/News/admin/themes/default/pages/general_settings.twig new file mode 100644 index 0000000..1b76668 --- /dev/null +++ b/News/admin/themes/default/pages/general_settings.twig @@ -0,0 +1,60 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Site Configuration" | translate }{% endblock %} + +{% block content %} +
+
+

+ + +

+

+ + +

+

+ + +

+

+ + + help + + ${ "Enter an alternate address here if you want your homepage URL to be different from the URL where Chyrp is normally available." | translate } + +

+

+ + +

+

+ + +

+

+ + +

+ +

+ +

+ + +
+
+{% endblock %} diff --git a/News/admin/themes/default/pages/import.twig b/News/admin/themes/default/pages/import.twig new file mode 100644 index 0000000..4345129 --- /dev/null +++ b/News/admin/themes/default/pages/import.twig @@ -0,0 +1,176 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Import" | translate }{% endblock %} + +{% block content %} +

Chyrp

+
+
+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + + + ${ "Usually something like http://example.com/uploads/." | translate } + +

+ + ${ trigger.call("import_choose") } + +

+ +

+ + +
+
+
+
+
+

WordPress

+
+
+

+ + +

+

+ + + + ${ "Usually something like http://example.com/wp-content/uploads/." | translate } + +

+ +

+ +

+ + +
+
+
+
+
+

Tumblr

+
+
+

+ + + ${ "Note: Audio tumbles cannot be imported." | translate } +

+ +

+ +

+ + +
+
+
+
+
+

TextPattern

+
+
+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + + ${ "(if any)" | translate } +

+

+ + + + ${ "Usually something like http://example.com/images/." | translate } + +

+

+ +

+ + +
+
+
+
+
+

MovableType

+
+
+

+ + +

+

+ + +

+

+ + +

+

+ + +

+

+ + + + ${ "Usually something like http://example.com/images/." | translate } + +

+

+ +

+ + +
+
+ ${ trigger.call("import_choose") } +{% endblock %} diff --git a/News/admin/themes/default/pages/manage_groups.twig b/News/admin/themes/default/pages/manage_groups.twig new file mode 100644 index 0000000..65f18d1 --- /dev/null +++ b/News/admin/themes/default/pages/manage_groups.twig @@ -0,0 +1,47 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Manage Groups" | translate }{% endblock %} + +{% block content %} +

${ "Need more detail?" | translate }

+
+
+ + {% if visitor.group.can("add_group") %} + + add ${ "New Group" | translate } + + {% endif %} +
+

${ "Search all groups for user…" | translate }

+ +
+
+
+
+

${ "Groups" | translate }

+ {% for group in groups.paginated %} +
+

+ + ${ group.edit_link('edit '~("edit" | translate)) } + ${ group.delete_link('delete '~("delete" | translate)) } + + {% if group.id == site.guest_group %} + ${ "“%s” is the group for guests." | translate | format(group.name) } + {% elseif group.id == site.default_group %} + ${ "“%s” is the default group and has %d member." | translate_plural("“%s” is the default group and has %d members.", group.size) | format(group.name, group.size, route.url("/admin/?action=manage_users&query=group%3A"~group.name)) } + {% else %} + ${ "“%s” has %d member." | translate_plural("“%s” has %d members.", group.size) | format(group.name, group.size, route.url("/admin/?action=manage_users&query=group%3A"~group.name)) } + {% endif %} +

+
+ {% endfor %} + {% if groups.paginated and groups.pages > 1 %} + + {% endif %} +{% endblock %} diff --git a/News/admin/themes/default/pages/manage_pages.twig b/News/admin/themes/default/pages/manage_pages.twig new file mode 100644 index 0000000..2f5a7cd --- /dev/null +++ b/News/admin/themes/default/pages/manage_pages.twig @@ -0,0 +1,91 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Manage Pages" | translate }{% endblock %} + +{% block content %} +

${ "Need more detail?" | translate }

+
+
+ +
+

+ ${ "Search…" | translate } + help +

+ +
+
+
+
+

{% if GET.query %}${ "Search Results" | translate }{% else %}${ "Last 25 Pages" | translate }{% endif %}

+ + + + + + + + ${ trigger.call("manage_pages_column_header") } + + + + + {% for page in pages.paginated %} + + + + + + ${ trigger.call("manage_pages_column", page) } + {% if page.editable and page.deletable %} + + + {% else %} + ${ page.edit_link('edit '~("edit" | translate), '') } + ${ page.delete_link('delete '~("delete" | translate), '') } + {% endif %} + + {% else %} + + + + {% endfor %} + +
${ "Title" | translate }${ "Created" | translate }${ "Last Updated" | translate }${ "Author" | translate }${ "Controls" | translate }
${ page.title | truncate }${ page.created_at | strftime }{% if page.updated %}${ page.updated_at | strftime }{% else %}${ "never" | translate }{% endif %}$page.user.login${ page.edit_link('edit '~("edit" | translate)) }${ page.delete_link('delete '~("delete" | translate)) }', '', '
${ "(none)" | translate }
+ {% if pages.paginated and pages.pages > 1 %} +
+ + {% endif %} + {% if pages.total %} +
+

${ "Reorder Pages" | translate }

+
+
    + {% for item in theme.pages_list %} +
  • + +
    $item.page.title
    + + {% if item.has_children %}
      {% endif %} + {% if not item.has_children %}{% endif %} + + {% for ul, li in item.end_tags %} + $ul + $li + {% endfor %} + {% endfor %} +
    + +
  • + {% endif %} +{% endblock %} diff --git a/News/admin/themes/default/pages/manage_posts.twig b/News/admin/themes/default/pages/manage_posts.twig new file mode 100644 index 0000000..c65269c --- /dev/null +++ b/News/admin/themes/default/pages/manage_posts.twig @@ -0,0 +1,74 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Manage Posts" | translate }{% endblock %} + +{% block content %} +

    ${ "Need more detail?" | translate }

    +
    +
    + +
    +

    + ${ "Search…" | translate } + help +

    + +
    +
    +

    ${ "Browse by month:" | translate }

    + + +
    +
    +
    +
    +
    +

    {% if GET.query %}${ "Search Results" | translate }{% else %}${ "Last 25 Posts" | translate }{% endif %}

    + + + + + + + + ${ trigger.call("manage_posts_column_header") } + + + + + {% for post in posts.paginated %} + + + + + + ${ trigger.call("manage_posts_column", post) } + {% if post.editable and post.deletable %} + + + {% elseif post.editable or post.deletable %} + ${ post.edit_link('edit '~("edit" | translate), '') } + ${ post.delete_link('delete '~("delete" | translate), '') } + {% endif %} + + {% else %} + + + + {% endfor %} + +
    ${ "Title" | translate }${ "Posted" | translate }${ "Status" | translate }${ "Author" | translate }${ "Controls" | translate }
    ${ post.title | truncate }${ post.created_at | strftime }$post.status_name$post.user.login${ post.edit_link('edit '~("edit" | translate)) }${ post.delete_link('delete '~("delete" | translate)) }', '', '
    ${ "(none)" | translate }
    + {% if posts.paginated and posts.pages > 1 %} +
    + + {% endif %} +{% endblock %} diff --git a/News/admin/themes/default/pages/manage_users.twig b/News/admin/themes/default/pages/manage_users.twig new file mode 100644 index 0000000..e63e2a9 --- /dev/null +++ b/News/admin/themes/default/pages/manage_users.twig @@ -0,0 +1,74 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Manage Users" | translate }{% endblock %} + +{% block content %} +

    ${ "Need more detail?" | translate }

    +
    +
    + + {% if visitor.group.can("add_user") %} + + add ${ "New User" | translate } + + {% endif %} +
    +

    + ${ "Search…" | translate } + help +

    + +
    +
    +
    +
    +

    ${ "Users" | translate }

    + + + + + + + + ${ trigger.call("manage_users_column_header") } + + + + + {% for user in users.paginated %} + + + + + + ${ trigger.call("manage_users_column", user) } + {% if user.editable and user.deletable %} + + + {% else %} + ${ user.edit_link('edit '~("edit" | translate), '') } + ${ user.delete_link('delete '~("delete" | translate), '') } + {% endif %} + + {% else %} + + + + {% endfor %} + +
    ${ "Name" | translate }${ "Group" | translate }${ "Joined" | translate }${ "Website" | translate }${ "Controls" | translate }
    + {% if user.full_name != "" %} + $user.full_name ($user.login) + {% else %} + $user.login + {% endif %} + ${ user.group.name | escape }${ user.joined_at | strftime }{% if user.website != "" %}$user.website{% endif %}${ user.edit_link('edit '~("edit" | translate)) }${ user.delete_link('delete '~("delete" | translate)) }', '', '
    ${ "(none)" | translate }
    + {% if users.paginated and users.pages > 1 %} +
    + + {% endif %} +{% endblock %} diff --git a/News/admin/themes/default/pages/modules.twig b/News/admin/themes/default/pages/modules.twig new file mode 100644 index 0000000..ab087de --- /dev/null +++ b/News/admin/themes/default/pages/modules.twig @@ -0,0 +1,71 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Modules" | translate }{% endblock %} + +{% block content %} +
    +

    ${ "Enabled" | translate }

    +
      + {% for safename, module in enabled_modules | items %} +
    • + + {% if module.help %} + help + {% endif %} + ${ "%s v%s by %s" | translate | format(module.url, module.name | translate(safename), module.version, module.author.link) } +
      +
      + $module.description {# translation is done in the controller #} + + + +
      + {% if not module.dependencies_needed %} + + {% endif %} +
      +
    • + {% endfor %} +
    +
    +
    +

    ${ "Disabled" | translate }

    +
      + {% for safename, module in disabled_modules | items %} +
    • + + {% if module.help %} + help + {% endif %} + ${ "%s v%s by %s" | translate | format(module.url, module.name | translate(safename), module.version, module.author.link) } +
      +
      + $module.description {# translation is done in the controller #} + + + +
      + {% if not module.dependencies_needed %} + + {% endif %} +
      +
    • + {% endfor %} +
    +
    +
    +
    + ${ "Get More Modules ›" | translate } +{% endblock %} diff --git a/News/admin/themes/default/pages/new_group.twig b/News/admin/themes/default/pages/new_group.twig new file mode 100644 index 0000000..ba1916b --- /dev/null +++ b/News/admin/themes/default/pages/new_group.twig @@ -0,0 +1,34 @@ +{% extends "layout.twig" %} + +{% block title %}${ "New Group" | translate }{% endblock %} + +{% block content %} +

    ${ "New Group" | translare }

    +
    +
    +

    + + +

    + +

    ${ "Permissions" | translate }

    +

    + +

    +
    + {% for permission in permissions %} +

    + + +

    + {% endfor %} +
    + +

    + +

    + + +
    +
    +{% endblock %} diff --git a/News/admin/themes/default/pages/new_user.twig b/News/admin/themes/default/pages/new_user.twig new file mode 100644 index 0000000..0f7052d --- /dev/null +++ b/News/admin/themes/default/pages/new_user.twig @@ -0,0 +1,58 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Add User" | translate }{% endblock %} + +{% block content %} +

    ${ "New User" | translare }

    +
    +
    +

    ${ "Settings" | translate }

    +

    + + +

    +

    + + +

    +

    + + +

    +

    + + +

    + +

    ${ "Information" | translate }

    +

    + + +

    +

    + + +

    +

    + + +

    + + + ${ trigger.call("new_user_fields") } + +
    + +

    + +

    + + +
    +
    +{% endblock %} diff --git a/News/admin/themes/default/pages/route_settings.twig b/News/admin/themes/default/pages/route_settings.twig new file mode 100644 index 0000000..12f64bc --- /dev/null +++ b/News/admin/themes/default/pages/route_settings.twig @@ -0,0 +1,50 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Route Settings" | translate }{% endblock %} + +{% block content %} +
    +
    +

    + + + ${ "(recommended)" | translate } + + ${ "Gives your site prettier urls." | translate }
    + ${ "Requires .htaccess support (pretty common). If you're unsure, it's safe to test and find out. Just come back and disable it if it breaks your site." | translate } +
    +

    +

    + + +

    +
    + ${ "Syntax:" | translate } +
      +
    • (year): ${ "Year submitted" | translate } ${ "(ex. 2007)" | translate }
    • +
    • (month): ${ "Month submitted" | translate } ${ "(ex. 12)" | translate }
    • +
    • (day): ${ "Day submitted" | translate } ${ "(ex. 25)" | translate }
    • +
    • (hour): ${ "Hour submitted" | translate } ${ "(ex. 03)" | translate }
    • +
    • (minute): ${ "Minute submitted" | translate } ${ "(ex. 59)" | translate }
    • +
    • (second): ${ "Second submitted" | translate } ${ "(ex. 30)" | translate }
    • +
    • (id): ${ "Post ID" | translate }
    • +
    • (author): ${ "Post author (username)" | translate } ${ "(ex. Alex)" | translate }
    • +
    • (clean): ${ "The non-unique sanitized name" | translate } ${ "(ex. this_is_clean)" | translate }
    • +
    • (url): ${ "The unique form of (clean)" | translate } ${ "(ex. this_one_is_taken_2)" | translate }
    • +
    • (feather): ${ "The post's feather" | translate } ${ "(ex. text)" | translate }
    • +
    • (feathers): ${ "The plural form of the post's feather" | translate } ${ "(ex. links)" | translate }
    • + ${ trigger.call("post_view_url_settings") } +
    +
    + +

    + +

    + + +
    +
    +{% endblock %} diff --git a/News/admin/themes/default/pages/themes.twig b/News/admin/themes/default/pages/themes.twig new file mode 100644 index 0000000..4a2006b --- /dev/null +++ b/News/admin/themes/default/pages/themes.twig @@ -0,0 +1,55 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Themes" | translate }{% endblock %} + +{% block content %} + {% for theme in themes %} +
    + + ${ theme.info.name | translate(theme.name) | escape } + {% if theme.screenshot %} + ${ (' + {% endif %} + + {% if theme.screenshot %} +
    + {% endif %} + ${ "v%s by %s" | translate | format(theme.info.version, theme.info.author.link) } +
    +
    + {% if theme.name == site.theme %} + current theme${ "Current Theme" | translate } + {% else %} + Select${ "Select" | translate } + Preview${ "Preview" | translate } + {% endif %} +
    + {% endfor %} +
    +
    +

    ${ "Admin Themes" | translate }

    + {% for theme in admin_themes %} +
    + + ${ theme.info.name | translate(theme.name) | escape } + {% if theme.screenshot %} + ${ (' + {% endif %} + + {% if theme.screenshot %} +
    + {% endif %} + ${ "v%s by %s" | translate | format(theme.info.version, theme.info.author.link) } +
    +
    + {% if theme.name == admin_theme %} + current theme${ "Current Theme" | translate } + {% else %} + Select${ "Select" | translate } + {% endif %} +
    + {% endfor %} +
    +
    + ${ "Get More Themes ›" | translate } +{% endblock %} diff --git a/News/admin/themes/default/pages/user_settings.twig b/News/admin/themes/default/pages/user_settings.twig new file mode 100644 index 0000000..adf7bd3 --- /dev/null +++ b/News/admin/themes/default/pages/user_settings.twig @@ -0,0 +1,37 @@ +{% extends "layout.twig" %} + +{% block title %}${ "User Settings" | translate }{% endblock %} + +{% block content %} +
    +
    +

    + + + ${ "Allow people to register" | translate } +

    +

    + + +

    +

    + + +

    + +

    + +

    + + +
    +
    +{% endblock %} diff --git a/News/admin/themes/default/pages/write_page.twig b/News/admin/themes/default/pages/write_page.twig new file mode 100644 index 0000000..63658c3 --- /dev/null +++ b/News/admin/themes/default/pages/write_page.twig @@ -0,0 +1,13 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Add Page" | translate }{% endblock %} + +{% block content %} +
    +
    +{% include "partials/page_fields.twig" %} + + +
    +
    +{% endblock %} diff --git a/News/admin/themes/default/pages/write_post.twig b/News/admin/themes/default/pages/write_post.twig new file mode 100644 index 0000000..c6e8ef7 --- /dev/null +++ b/News/admin/themes/default/pages/write_post.twig @@ -0,0 +1,14 @@ +{% extends "layout.twig" %} + +{% block title %}${ "Write" | translate }{% endblock %} + +{% block content %} +
    +
    +{% include "partials/post_fields.twig" %} + + + +
    +
    +{% endblock %} diff --git a/News/admin/themes/default/partials/page_fields.twig b/News/admin/themes/default/partials/page_fields.twig new file mode 100644 index 0000000..bd2faa1 --- /dev/null +++ b/News/admin/themes/default/partials/page_fields.twig @@ -0,0 +1,49 @@ + ${ trigger.call("before_page_fields") } +

    + + +

    +

    + + +

    + ${ trigger.call("after_page_fields") } + +
    + +
    + +
    + +
    +

    + +   +

    +

    + + +

    +

    + + +

    + {% if route.action == "write_page" %} + ${ trigger.call("new_page_options") } + {% else %} + ${ trigger.call("edit_page_options", page) } + {% endif %} +
    +
    + diff --git a/News/admin/themes/default/partials/post_fields.twig b/News/admin/themes/default/partials/post_fields.twig new file mode 100644 index 0000000..cce32b2 --- /dev/null +++ b/News/admin/themes/default/partials/post_fields.twig @@ -0,0 +1,133 @@ + ${ trigger.call("before_post_fields", feather) } + {% for field in feather.fields %} +

    + + {% if field.type == "text" or field.type == "file" %} + + {% if post.filename and route.action == "edit_post" %} + Current file name: ${ post.filename | escape('') } + {% endif %} + {% elseif field.type == "text_block" %} + + {% elseif field.type == "checkbox" %} + + {% elseif field.type == "select" %} + + {% endif %} + $field.extra +

    + {% endfor %} + ${ trigger.call("after_post_fields", feather) } + +
    + {% if route.action == "edit_post" %} + + {% else %} + {% if visitor.group.can("add_post") %} + + {% endif %} + + {% endif %} +
    + +
    + +
    + {% if visitor.group.can("add_post") %} +

    + + +

    + {% endif %} +

    + +   + ${ "(shows this post above all others)" | translate } +

    +

    + + +

    +

    + + + +

    +

    + + + +

    + {% for field in options %} +

    + + {% if field.type == "text" or field.type == "file" %} + + {% elseif field.type == "text_block" %} + + {% elseif field.type == "select" %} + + {% endif %} + $field.extra +

    + {% endfor %} +
    +
    + \ No newline at end of file diff --git a/News/admin/themes/default/style.css b/News/admin/themes/default/style.css new file mode 100644 index 0000000..870289a --- /dev/null +++ b/News/admin/themes/default/style.css @@ -0,0 +1,948 @@ +/* @group Reset */ +body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,p,blockquote,th,td { margin: 0; padding: 0; } +table { border-collapse: collapse; border-spacing: 0; } +fieldset,img { border: 0; } +address,caption,cite,code,dfn,em,strong,th,var { font-style: normal; font-weight: normal; } +li { list-style: none; } +caption,th { text-align: left; } +h1,h2,h3,h4,h5,h6 { font-size: 100%; font-weight: normal; } +abbr,acronym { border: 0; font-variant: normal; } +input,textarea,select { font-family: inherit; font-size: inherit; font-weight: inherit; } +a:link, a:visited { text-decoration: none; color: inherit; } +/* @end */ + +/* @group Core */ +html { + font-size: 62.5%; +} +body { + font: 1.25em/1.5em normal Lucida Grande, Helvetica, Arial, sans-serif; + color: #333; + background: #efefef; +} + +h1, h2, h3, h4, h5, h6{ + font-weight: normal !important; + font-family: "Palatino Linotype", Chaparral Pro, Georgia, serif; +} + +em { + font-style: italic; +} +strong { + font-weight: bold; +} +small, +.small { + font-size: .9em; + color: #999; +} +p { + margin: 0 0 1em; +} +a:link, +a:visited { + text-decoration: underline; +} +/* @end */ + +/* @group Header */ +#header { + background: #353535; + padding: 2em 0; + color: #fff; + position: relative; +} +#header h1 { + font-size: 2em; + line-height: 1.25em; + text-shadow: #000 0 1px 1px; +} +#header h1 a { + text-decoration: none; +} +#header #navigation { + float: right; + position: absolute; + bottom: 0; + right: 5em; +} +#header #navigation li { + display: inline; +} +#header #navigation li a:link, +#header #navigation li a:visited { + float: left; + margin-left: 1em; + text-shadow: #000 0 0 0; + background: #444; + color: #ebeeee; + height: 4em; + line-height: 4em; + text-transform: uppercase; + width: 8em; + text-align: center; + text-decoration: none; + font-family: "Palatino Linotype", Chaparral Pro, Georgia, serif; + letter-spacing: 0.2em; + font-size: 0.8em; +} +#header #navigation li.selected a:link, +#header #navigation li.selected a:visited { + background: #fff; + color: #333; +} + +#header #navigation li a:hover { + background: #4c4c4c; +} + +#welcome { + background: #fff; + padding: .9em 0; + border-bottom: 1px solid #e1e1e1; +} +#welcome a:link, +#welcome a:visited { + text-decoration: none; + background: #fcfcfc; + padding: 1em; + margin: -1em 0 -.8em; +} +#welcome .right { + padding: .9em 1em !important; + margin-top: -.9em !important; +} +#welcome a:hover { + background: #f5f5f5; +} +#welcome a:active { + background: #f0f0f0; +} +/* @end */ + +/* @group Footer */ +#footer { + width: 425px; + margin: 2.5em auto !important; + font-size: 1.25em; + text-align: center; +} +#footer .sub { + color: #acacac; + font-size: .7em; +} +/* @end */ + +/* @group Sub-Navigation */ +#sub-nav { + margin-top: 2em; +} +#sub-nav li { + float: left; + position: relative; + top: 1px; + z-index: 1; +} +#sub-nav li a:link, +#sub-nav li a:visited { + float: left; + padding: .4em .75em; + background: #dfdfdf; + border-bottom: 0; + color: #737373; + text-decoration: none; + margin: 0 0.7em 0 0; +} +#sub-nav li a:hover { + background: #e5e5e5; +} +#sub-nav li.selected a:link, +#sub-nav li.selected a:visited { + background: #fff; + border: 1px solid #e1e1e1; + border-bottom: none; +} +#sub-nav li.right { + margin: .75em 0 0; +} +#sub-nav li.right a:link, +#sub-nav li.right a:visited { + float: none; + background: transparent; + padding: 0; + font-size: .95em; + color: #444; +} +.feathers_sort { + background: #eee; + margin-top: .2em; + height: 2.3em !important; + border-left: .1em dashed #ccc; + border-right: .1em dashed #ccc; +} +/* @end */ + +/* @group Content */ +#content { + background: #fff; + padding: 1em; + border: 1px solid #e1e1e1; + position: relative; +} +#content > form { + /* margin-top: .5em; */ +} +#content h1 { + font-size: 2em; + margin: .2em 0 1em; + color: #aaa; + font-weight: bold; +} +#content h2 { + font-size: 1.5em; + margin: 0 0 .5em; + color: #666; + font-weight: normal; +} +#content h3 { + font-weight: bold; + font-size: 1.25em; + margin-bottom: 1.5em; +} +#content h4 { + font-weight: bold; + font-size: 1.2em; +} +/* @end */ + +/* @group Extend */ +.enable, +.disable { + width: 49%; +} +.enable h2, +.disable h2 { + font-weight: normal !important; + font-size: 1.1em !important; +} +.enable h2 .sub, +.disable h2 .sub { + font-size: .8em; + color: #bcbcbc; +} +.enable ul, +.disable ul { + padding: 1em 1em .5em; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; +} +.enable ul li, +.disable ul li { + padding: .1em .25em .1em .7em; + margin: 0 0 .5em; + list-style: disc inside !important; + position: relative; +} +.enable ul li img, +.disable ul li img { + position: absolute; + top: 4px; + right: 4px; +} +.enable ul li a.help img, +.disable ul li a.help img { + top: 3px; + right: 20px; + width: 15px; + height: 15px; +} +.enable ul li .sub, +.disable ul li .sub { + font-size: .9em; +} +.enable ul li .enable_button, +.disable ul li .disable_button { + display: block; + padding: .25em .5em; + text-align: right; +} +ul.extend .expand { + margin: 0 -.25em 0 -.7em; +} +.enable h2 { + color: #4f8f62 !important; +} +.enable ul { + background: #dbffe6; + border: 1px solid #dbffe6; +} +.enable ul li { + background: #e6ffee; + color: #386345; +} +.enable ul.hover { + border: 1px solid #b3d1bb; +} +.enable ul li a:link, +.enable ul li a:visited, +.enable ul li .sub a:hover { + text-decoration: none; + color: #386345; +} +.enable ul li .sub, +.enable ul li .sub a:link, +.enable ul li .sub a:visited { + color: #74a683; +} +.enable ul li .description { + padding: .5em; + background: #F7FFF9; +} +.enable ul li .enable_button { + color: #189100; +} +.enable ul li .enable_button:hover { + background: #189100; + color: #fff; +} +.enable ul li .enable_button:active { + background: #067000; +} +.disable h2 { + color: #ff7070 !important; +} +.disable ul { + background: #ffdbdb; + border: 1px solid #ffdbdb; +} +.disable h2 .sub { + color: #bcbcbc; +} +.disable ul li { + background: #ffe6e6; + color: #653b3d; +} +.disable ul.hover { + border: 1px solid #cfb2b2; +} +.disable ul li a:link, +.disable ul li a:visited, +.disable ul li .sub a:hover { + text-decoration: none; + color: #7b4d4d; +} +.disable ul li .sub, +.disable ul li .sub a:link, +.disable ul li .sub a:visited { + color: #a97f7f; +} +.disable ul li .description { + padding: .5em; + background: #FFF1F1; +} +.disable ul li .disable_button { + color: #d51800; +} +.disable ul li .disable_button:hover { + background: #d51800; + color: #fff; +} +.disable ul li .disable_button:active { + background: #b30600; +} +/* @end */ + +/* @group Theme Changing */ +.theme { + text-align: center; + margin-right: 15px !important; + border: 0 !important; + padding-bottom: 20px !important; + position: relative; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; +} +.theme.last { + margin-right: 0 !important; +} +.theme .link { + font-weight: bold; + text-align: center; + text-decoration: none; +} +.theme .link span { + display: block; +} +.theme.current { + background: #EBFAE4; + color: #65845e; +} +.theme.current small, +.theme.current small a:link, +.theme.current small a:visited { + color: #789f72; +} +.theme.current .button { + background: #189100; + color: #FFF; + border: 0; +} +.theme img.screenshot { + padding: 5px; + background: #FFF; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; +} +.theme a:hover img.screenshot { + background: #CCC; +} +.theme small { + font-weight: normal; +} +/* @end */ + +/* @group Forms */ +form.delete blockquote { + width: 75%; + margin: 0 auto; + font-style: italic; +} +form.delete blockquote h2, +form.delete .noitalic { + font-style: normal; +} +form.delete blockquote h2 input[type="checkbox"] { + position: relative; + top: -.125em; +} +form.delete h2.inline { + display: inline; +} +form.delete select { + position: relative; + top: -2px; +} +form.split label { + width: 41%; + float: left; + text-align: right; + font-weight: bold; + padding: 4px 2% 5px; + color: #999; +} +form.split label .sub { + display: block; +} +form.split label input { + margin: -.5em 0; +} +form.split select { + margin: 3px 0 .5em; +} +form.split small, +form.split .small { + display: block; + margin-left: 45%; +} +form.split button { + margin-left: 45%; +} +form .buttons { + margin-top: 2em; +} +label { + display: block; + font-size: 1.1em; +} +form p .sub, +label .sub, +tr .sub { + font-size: .8em; + color: #999; + font-weight: normal; +} +input.text, +input[type="text"], +input[type="password"], +input.file, +textarea { + font-size: 1.25em; + padding: 3px; + border: 1px solid #ddd; + background: #fff; +} +input.file { + border: none; +} +input.text, +input.file, +select, +textarea { + margin-bottom: .25em; +} +input.text { + /* width: 15em; */ +} +input.checkbox { + margin: .6em .5em .75em 0; +} +span.checkbox { + position: relative; + top: -3px; +} +textarea { + font-size: 1.1em; + font-family: "Monaco", "Consolas", monospace; +} +textarea.wide, +input.text.wide { + padding: .5%; + width: 99%; + _width: 100%; /* Hooray for IE6's randomized box model. */ +} +input.code, +textarea.code, +code { + font-family: "Consolas", "Monaco", monospace; +} +/* @end */ + +/* @group Tables */ +table thead tr { + background: #555; + color: #fff; + border: 0 !important; + border-bottom: 2px solid #444; + text-shadow: #333 0px 1px 0px; +} +table thead tr th { + padding: .25em .5em; + font-weight: bold; +} +table thead tr th input { + vertical-align: middle; + margin: -5px 0 0 !important; +} +table thead tr th.header { + cursor: pointer; +} +table thead tr th.headerSortDown, +table thead tr th.headerSortUp { + background: #444; +} +table tbody tr { + +} +table tbody tr.last { + +} +table tbody tr td { + padding: .5em .7em; + border-right: 1px solid #eee; + border-bottom: 1px solid #eee; + vertical-align: middle; +} +table tbody tr td.main { + background: #f9f9f9; +} +table tbody tr td.center { + text-align: center; + vertical-align: middle; +} +table tbody tr td.controls { + text-align: center; + font-size: .95em; + color: #888; +} +table tbody tr td.controls img { + margin-bottom: -3px; + margin-right: 1px; +} +table tbody tr td.main a:link, +table tbody tr td.main a:visited, +table tbody tr td.controls a:link, +table tbody tr td.controls a:visited { + text-decoration: none; +} +tr.draft, +tr.excerpt, +tr.draft td, +tr.excerpt td, +tr.draft td.main { + background-color: #dff4ff; + border-color: #c2e1ef !important; + color: #336699; +} +tr.excerpt p { + margin: 0; +} +/* @end */ + +/* @group Preview */ +h1.preview-header { + margin: 0 0 .5em !important; +} +.preview-content { + margin: 0 0 2em; + padding-bottom: .75em; + border-bottom: 1px dotted #ddd; +} +.preview-content ul, +.preview-content ol { + margin: 0 0 1em 2em; + list-style: square; +} +.preview-content ol li { + list-style: decimal; +} +.preview-content a:link, +.preview-content a:visited { + text-decoration: underline; + color: #222; +} +.preview-content hr { + margin: 2em 0; +} +.preview-content blockquote { + margin: 0 2em; + color: #999; +} +/* @end */ + +/* @group Messages */ +p.message { + font-size: 1.1em; + padding: .6em; + margin-bottom: 1.75em; + background: #f1f1f1; + border: 1px solid #ccc; +} +p.message.yay { + background: #189100; + color: #fff; + border-color: #105f00; +} +p.message.boo { + background: #d51800; + color: #fff; + border-color: #9f1000; +} +/* @end */ + +/* @group Buttons */ +/* Inspiration from Chawlk. */ +button, +.button, +a.next_page, +a.prev_page { + display: inline-block; + font-family: Verdana, Helvetica, Arial, sans-serif; + font-weight: bold; + font-size: 1.125em; + margin: 0 .5em 0 0; + color: #333; + padding: .5em .75em; + background: #f1f1f1; + border: 1px solid #ccc; + border-top-color: #f1f1f1; + border-left-color: #f1f1f1; + border-bottom: 1px solid #ccc !important; + cursor: pointer; + text-decoration: none !important; +} +.button { + padding: .6em 1em .5em .9em; +} +button.lite { + padding: .15em .375em .25em .275em; +} +.button.lite, +a.next_page, +a.prev_page { + padding: .1em .4em .2em; +} +button.inline { + position: relative; + top: -1px; + padding: 4px 5px 4px 4px; +} +button.right, +.button.right, +a.next_page { + margin: 0 0 0 .5em; + float: right; +} +button:hover, +.button:hover, +a.next_page:hover, +a.prev_page:hover { + background: #f7f7f7; + border-color: #f7f7f7; + border-bottom-color: #d1d1d1 !important; + border-right-color: #d1d1d1; +} +button:active, +.button:active, +.button.selected, +a.next_page:active, +a.prev_page:active { + background-color: #d7d7d7; + border-color: #d7d7d7; + border-left-color: #b1b1b1; + border-top-color: #b1b1b1; + border-bottom-color: #d7d7d7 !important; +} +button.boo, +.button.boo { + background: #faebe4; + color: #3e3634; + border-color: #faebe4; + border-bottom-color: #d6bdb5; + border-right-color: #d6bdb5; +} +button.boo:hover, +.button.boo:hover { + background: #d51800; + color: #fff; + border-color: #d51800; + border-bottom-color: #9f1000 !important; + border-right-color: #9f1000; +} +button.boo:active, +.button.boo:active { + background: #b30600; + border-color: #b30600; + border-left-color: #7d0000; + border-top-color: #7d0000; + border-bottom-color: #b30600 !important; +} +button.yay, +.button.yay { + background: #ebfae4; + color: #363e34; + border-color: #ebfae4; + border-bottom-color: #bdd6b5; + border-right-color: #bdd6b5; +} +button.yay:hover, +.button.yay:hover { + background: #189100; + color: #fff; + border-color: #189100; + border-bottom-color: #105f00 !important; + border-right-color: #105f00; +} +button.yay:active, +.button.yay:active { + background: #067000; + border-color: #067000; + border-left-color: #003d00; + border-top-color: #003d00; + border-bottom-color: #067000 !important; +} +button[disabled="disabled"], +button.disabled { + background: #f0f0f0; + border-color: #f0f0f0 !important; + color: #999; +} +button[disabled="disabled"]:hover, +button.disabled:hover { + background: #f0f0f0; + border-color: #f0f0f0 !important; + color: #999; + cursor: default; +} +button img, +.button img { + margin: 0 6px -3px -4px; +} +button.lite img, +.button.lite img { + margin: 0 -1px -3px 0; +} +/* @end */ + +/* @group Options */ +.more_options_link { + font-weight: bold; + color: #999; + border-bottom: 1px solid #ddd; + display: block; + text-decoration: none !important; +} +.more_options_link:hover { + color: #666; + border-bottom-color: #aaa; +} +.more_options { + background: #f9f9f9; + padding: 2% 0 0 2%; +} +.more_options p { + width: 48%; + margin: 0 2% 0 0; + min-height: 5.5em; + _height: 5.5em; + float: left; +} +.more_options p input.text { + width: 96.5%; + margin: 0; +} +/* @end */ + +/* @group Page Reordering */ +ul.sort_pages, +ul.sort_pages ul { + padding: 0; + margin: 0 0 0 2em; +} +ul.sort_pages li { + list-style: square; +} +ul.sort_pages li a:link, +ul.sort_pages li a:visited { + text-decoration: none !important; +} +ul.sort_pages input { + font-size: 1em; + margin: .25em .25em .25em 0; +} +ul.sort_pages .sort-placeholder { + border: 1px dashed #777; + display: list-item !important; + margin-bottom: .5em; + height: 1.8em; +} +ul.sort_pages .sort-hover { + border: 1px dashed #777 !important; +} +/* @end */ + +/* @group Helpers & General */ +.breakword { + word-wrap: break-word !important; +} +div.controls.right { + margin-top: -2px; +} +div.controls h4 { + display: inline; +} +div.controls button { + position: relative; + top: -2px; + right: -1px; + margin: 0 0 0 .5em; +} +hr { + border: 0; + border-top: 1px solid #ddd; +} +#content ul li { + list-style: square; +} +.box { + background: #f5f5f5; + border: 1px solid #ddd; + padding: 1em; + margin: 0 0 1em; +} +.box h1 { + font-size: 1.25em !important; + margin: 0 !important; +} +.box h1 .sub { + font-weight: normal; + font-size: .75em; +} +.box h1 span.right { + font-size: .75em; + color: #666; +} +.box h1 span.right a:link, +.box h1 span.right a:visited { + margin-left: 1em; + text-decoration: none; +} +.box h1 span.right a img { + margin-bottom: -3px; +} +.edit_link:hover { + color: #333; +} +.delete_link { + color: #b30600; +} +table tbody td .delete_link { + margin: 0 -.4em 0 0; +} +.delete_link:after { + content: "!"; + opacity: 0; +} +.delete_link:hover { + color: #f00; +} +.delete_link:hover:after { + color: #f00; + + opacity: 1; +} +a.emblem { + border: none; +} +a.emblem img { + margin-bottom: -2px; +} +.pad { + padding: .75em; +} +.margin-right { + margin-right: 1em; +} +.margin-left { + margin-left: 1em; +} +.detail .button.right { + margin-top: 2.25em; +} +.detail h3 { + margin-bottom: .5em !important; +} +.detail.inline { + position: relative; + bottom: -.75em; +} +.wide { + width: 100%; +} +.clear { + clear: both; +} +.left { + float: left !important; +} +.right { + float: right !important; +} +.center { + margin: 0 auto; + text-align: center; +} +.inline { + display: inline; +} +.column { + margin: 0 5em; +} +br#after_options { + display: none; +} +.js_enabled { + display: none; +} +/* @end */ + diff --git a/News/feathers/audio/audio.php b/News/feathers/audio/audio.php new file mode 100755 index 0000000..4298ecb --- /dev/null +++ b/News/feathers/audio/audio.php @@ -0,0 +1,167 @@ +setField(array("attr" => "audio", + "type" => "file", + "label" => __("MP3 File", "audio"), + "note" => "(Max. file size: ".ini_get('upload_max_filesize').")")); + if (isset($_GET['action']) and $_GET['action'] == "bookmarklet") + $this->setField(array("attr" => "from_url", + "type" => "text", + "label" => __("From URL?", "audio"), + "optional" => true, + "no_value" => true)); + $this->setField(array("attr" => "description", + "type" => "text_block", + "label" => __("Description", "audio"), + "optional" => true, + "preview" => true, + "bookmarklet" => "selection")); + + $this->setFilter("description", array("markup_text", "markup_post_text")); + + $this->respondTo("delete_post", "delete_file"); + $this->respondTo("javascript", "player_js"); + $this->respondTo("feed_item", "enclose_mp3"); + $this->respondTo("filter_post", "filter_post"); + $this->respondTo("admin_write_post", "swfupload"); + $this->respondTo("admin_edit_post", "swfupload"); + $this->respondTo("post_options", "add_option"); + } + + public function swfupload($admin, $post = null) { + if (isset($post) and $post->feather != "audio" or + isset($_GET['feather']) and $_GET['feather'] != "audio") + return; + + Trigger::current()->call("prepare_swfupload", "audio", "*.mp3"); + } + + public function submit() { + if (!isset($_POST['filename'])) { + if (isset($_FILES['audio']) and $_FILES['audio']['error'] == 0) + $filename = upload($_FILES['audio'], "mp3"); + elseif (!empty($_POST['from_url'])) + $filename = upload_from_url($_POST['from_url'], "mp3"); + else + error(__("Error"), __("Couldn't upload audio file.")); + } else + $filename = $_POST['filename']; + + return Post::add(array("filename" => $filename, + "description" => $_POST['description']), + $_POST['slug'], + Post::check_url($_POST['slug'])); + } + + public function update($post) { + if (!isset($_POST['filename'])) + if (isset($_FILES['audio']) and $_FILES['audio']['error'] == 0) { + $this->delete_file($post); + $filename = upload($_FILES['audio'], "mp3"); + } elseif (!empty($_POST['from_url'])) { + $this->delete_file($post); + $filename = upload_from_url($_POST['from_url'], "mp3"); + } else + $filename = $post->filename; + else { + $this->delete_file($post); + $filename = $_POST['filename']; + } + + $post->update(array("filename" => $filename, + "description" => $_POST['description'])); + } + + public function title($post) { + return oneof($post->title, $post->title_from_excerpt()); + } + + public function excerpt($post) { + return $post->description; + } + + public function feed_content($post) { + return $post->description; + } + + public function delete_file($post) { + if ($post->feather != "audio") return; + unlink(MAIN_DIR.Config::current()->uploads_path.$post->filename); + } + + public function filter_post($post) { + if ($post->feather != "audio") return; + $post->audio_player = $this->flash_player_for($post->filename, array(), $post); + } + + public function player_js() { +?>//'."\n"; + $player.= ''."\n\t"; + $player.= ''."\n\t"; + $player.= ''."\n\t"; + $player.= ''."\n\t"; + $player.= ''."\n\t"; + $player.= ''."\n"; + $player.= ''."\n"; + + return $player; + } + + public function add_option($options, $post = null) { + if (isset($post) and $post->feather != "audio") return; + if (!isset($_GET['feather']) and Config::current()->enabled_feathers[0] != "audio" or + isset($_GET['feather']) and $_GET['feather'] != "audio") return; + + $options[] = array("attr" => "from_url", + "label" => __("From URL?", "audio"), + "type" => "text"); + + return $options; + } + } diff --git a/News/feathers/audio/info.yaml b/News/feathers/audio/info.yaml new file mode 100755 index 0000000..fb365bf --- /dev/null +++ b/News/feathers/audio/info.yaml @@ -0,0 +1,8 @@ +name: Audio +url: http://chyrp.net/ +version: 2.0 +description: An audio feather, including a flash player. +author: + name: Alex Suraci + url: http://ecks.tc/ +uploader: true \ No newline at end of file diff --git a/News/feathers/audio/lib/audio-player.js b/News/feathers/audio/lib/audio-player.js new file mode 100755 index 0000000..66b35a1 --- /dev/null +++ b/News/feathers/audio/lib/audio-player.js @@ -0,0 +1,25 @@ +var ap_instances = new Array(); + +function ap_stopAll(playerID) { + for(var i = 0;i, YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Chyrp v2.1\n" +"Report-Msgid-Bugs-To: email@chyrp.net\n" +"POT-Creation-Date: 2011-01-10 22:16+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FIRST LAST \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: feathers/audio/audio.php:6 +msgid "MP3 File" +msgstr "" + +#: feathers/audio/info.yaml:1 +msgid "Audio" +msgstr "" + +#: feathers/audio/info.yaml:6 +msgid "An audio feather, including a flash player." +msgstr "" + +#: feathers/audio/audio.php:17 +msgid "Description" +msgstr "" + +#: feathers/audio/audio.php:11 +#: feathers/audio/audio.php:163 +msgid "From URL?" +msgstr "" + diff --git a/News/feathers/chat/chat.php b/News/feathers/chat/chat.php new file mode 100755 index 0000000..4c96705 --- /dev/null +++ b/News/feathers/chat/chat.php @@ -0,0 +1,132 @@ +setField(array("attr" => "title", + "type" => "text", + "label" => __("Title", "chat"), + "optional" => true)); + $this->setField(array("attr" => "dialogue", + "type" => "text_block", + "label" => __("Dialogue", "chat"), + "preview" => true, + "help" => "chat_dialogue", + "bookmarklet" => "selection")); + + $this->customFilter("dialogue", "format_dialogue"); + + $this->setFilter("title", array("markup_title", "markup_post_title")); + $this->setFilter("dialogue", array("markup_text", "markup_post_text")); + + $this->respondTo("preview_chat", "format_dialogue"); + $this->respondTo("help_chat_dialogue", "help"); + } + + public function submit() { + if (empty($_POST['dialogue'])) + error(__("Error"), __("Dialogue can't be blank.")); + + fallback($_POST['slug'], sanitize($_POST['title'])); + + return Post::add(array("title" => $_POST['title'], + "dialogue" => $_POST['dialogue']), + $_POST['slug'], + Post::check_url($_POST['slug'])); + } + + public function update($post) { + if (empty($_POST['dialogue'])) + error(__("Error"), __("Dialogue can't be blank.")); + + $post->update(array("title" => $_POST['title'], + "dialogue" => $_POST['dialogue'])); + } + + public function title($post) { + $dialogue = oneof($post->dialogue_unformatted, $post->dialogue); + + $dialogue = explode("\n", $dialogue); + $line = preg_replace("/^\s*[\[\(]?[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?\s*(pm|am)?[\]|\)]?\s*/i", "", $dialogue[0]); + $first_line = preg_replace("/([<]?)([^:|>]+)( \(me\)?)(:|>) (.+)/i", "\\1\\2\\4 \\5", $dialogue[0]); + + return oneof($post->title, $first_line); + } + + public function excerpt($post) { + return $post->dialogue; + } + + public function feed_content($post) { + return $post->dialogue; + } + + public function format_dialogue($text, $post = null) { + if (isset($post)) + $post->dialogue_unformatted = $text; + + $split = explode("\n", $text); + $return = '
      '; + $count = 0; + $my_name = ""; + $links = array(); + foreach ($split as $line) { + # Remove the timstamps + $line = preg_replace("/^\s*[\[\(]?[0-9]{1,2}:[0-9]{2}(:[0-9]{2})?\s*(pm|am)?[\]|\)]?\s*/i", "", $line); + + preg_match("/()\s*(.+)/i", $line, $matches); + + if (empty($matches)) + continue; + + if (preg_match("/\s*\(([^\)]+)\)$/", $matches[2], $attribution)) + if ($attribution[1] == "me") { + $my_name = $matches[2] = str_replace($attribution[0], "", $matches[2]); + } else { + $matches[2] = str_replace($attribution[0], "", $matches[2]); + $links[$matches[2]] = $attribution[1]; + } + + $link = oneof(@$links[$matches[2]], ""); + + $me = ($my_name == $matches[2] ? " me" : ""); + + $username = $matches[1].$matches[2].$matches[3]; + $class = ($count % 2 ? "even" : "odd"); + $return.= '
    • '; + + if (!empty($link)) + $return.= ''.$matches[1].''.fix($matches[2], false).''.$matches[3].' '.$matches[4]."\n"; + else + $return.= ''.fix($username, false).' '.$matches[4]."\n"; + + $return.= '
    • '; + $count++; + } + $return.= "
    "; + + # If they're previewing. + if (!isset($post)) + $return = preg_replace("/(
  • )(.+)(<\/span> (.+)\n<\/li>)/", "\\1\\3\\4", $return); + + return $return; + } + + public function help() { + $title = __("Dialogue Formatting", "chat"); + + $body = "

    ".__("To give yourself a special CSS class, append \" (me)\" to your username, like so:", "chat")."

    \n"; + $body.= "
      \n"; + $body.= "\t
    • "<Alex>" → "<Alex (me)>"
    • \n"; + $body.= "\t
    • "Alex:" → "Alex (me):"
    • \n"; + $body.= "
    \n"; + $body.= "

    ".__("This only has to be done to the first occurrence of the username.", "chat")."

    "; + + $body.= "

    ".__("To attribute a name to a URL, append the URL in parentheses, preceded by a space, to the username:", "chat")."

    \n"; + $body.= "
      \n"; + $body.= "\t
    • "<John>" → "<John (http://example.com/)>"
    • \n"; + $body.= "\t
    • "John:" → "John (http://example.com/):"
    • \n"; + $body.= "
    \n"; + $body.= "

    ".__("This also only has to be done to the first occurrence of the username. It cannot be combined with attributing someone as yourself (because they're already at your site anyway).", "chat")."

    "; + + return array($title, $body); + } + } diff --git a/News/feathers/chat/info.yaml b/News/feathers/chat/info.yaml new file mode 100755 index 0000000..21e053f --- /dev/null +++ b/News/feathers/chat/info.yaml @@ -0,0 +1,7 @@ +name: Chat +url: http://chyrp.net/ +version: 2.0 +description: Post logs of IRC or other chat formats. +author: + name: Alex Suraci + url: http://ecks.tc/ diff --git a/News/feathers/chat/locale/en_US.pot b/News/feathers/chat/locale/en_US.pot new file mode 100644 index 0000000..1704c42 --- /dev/null +++ b/News/feathers/chat/locale/en_US.pot @@ -0,0 +1,55 @@ +# Chyrp v2.1 Translation File. +# Copyright (C) 2011 Chyrp Team +# This file is distributed under the same license as the Chyrp v2.1 package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Chyrp v2.1\n" +"Report-Msgid-Bugs-To: email@chyrp.net\n" +"POT-Creation-Date: 2011-01-10 22:16+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FIRST LAST \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: feathers/chat/chat.php:123 +msgid "To attribute a name to a URL, append the URL in parentheses, preceded by a space, to the username:" +msgstr "" + +#: feathers/chat/info.yaml:1 +msgid "Chat" +msgstr "" + +#: feathers/chat/chat.php:121 +msgid "This only has to be done to the first occurrence of the username." +msgstr "" + +#: feathers/chat/chat.php:114 +msgid "Dialogue Formatting" +msgstr "" + +#: feathers/chat/chat.php:128 +msgid "This also only has to be done to the first occurrence of the username. It cannot be combined with attributing someone as yourself (because they're already at your site anyway)." +msgstr "" + +#: feathers/chat/chat.php:6 +msgid "Title" +msgstr "" + +#: feathers/chat/info.yaml:5 +msgid "Post logs of IRC or other chat formats." +msgstr "" + +#: feathers/chat/chat.php:116 +msgid "To give yourself a special CSS class, append \" (me)\" to your username, like so:" +msgstr "" + +#: feathers/chat/chat.php:10 +msgid "Dialogue" +msgstr "" + diff --git a/News/feathers/link/info.yaml b/News/feathers/link/info.yaml new file mode 100755 index 0000000..9091b3b --- /dev/null +++ b/News/feathers/link/info.yaml @@ -0,0 +1,7 @@ +name: Link +url: http://chyrp.net/ +version: 2.0 +description: Link to other sites, name it, and add an optional description. +author: + name: Alex Suraci + url: http://ecks.tc/ diff --git a/News/feathers/link/link.php b/News/feathers/link/link.php new file mode 100755 index 0000000..e4f5c20 --- /dev/null +++ b/News/feathers/link/link.php @@ -0,0 +1,72 @@ +setField(array("attr" => "source", + "type" => "text", + "label" => __("URL", "link"), + "bookmarklet" => "url")); + $this->setField(array("attr" => "name", + "type" => "text", + "label" => __("Name", "link"), + "bookmarklet" => "title")); + $this->setField(array("attr" => "description", + "type" => "text_block", + "label" => __("Description", "link"), + "optional" => true, + "preview" => true, + "bookmarklet" => "selection")); + + $this->setFilter("name", array("markup_title", "markup_post_title")); + $this->setFilter("description", array("markup_text", "markup_post_text")); + + $this->respondTo("feed_url", "set_feed_url"); + } + + public function submit() { + if (empty($_POST['source'])) + error(__("Error"), __("URL can't be empty.")); + + if (!@parse_url($_POST['source'], PHP_URL_SCHEME)) + $_POST['source'] = "http://".$_POST['source']; + + fallback($_POST['slug'], sanitize($_POST['name'])); + + return Post::add(array("name" => $_POST['name'], + "source" => $_POST['source'], + "description" => $_POST['description']), + $_POST['slug'], + Post::check_url($_POST['slug'])); + } + + public function update($post) { + if (empty($_POST['source'])) + error(__("Error"), __("URL can't be empty.")); + + if (!@parse_url($_POST['source'], PHP_URL_SCHEME)) + $_POST['source'] = "http://".$_POST['source']; + + $post->update(array("name" => $_POST['name'], + "source" => $_POST['source'], + "description" => $_POST['description'])); + } + + public function title($post) { + $return = $post->name; + fallback($return, $post->title_from_excerpt()); + fallback($return, $post->source); + return $return; + } + + public function excerpt($post) { + return $post->description; + } + + public function feed_content($post) { + return $post->description; + } + + public function set_feed_url($url, $post) { + if ($post->feather != "link") return; + return $url = $post->source; + } + } diff --git a/News/feathers/link/locale/en_US.pot b/News/feathers/link/locale/en_US.pot new file mode 100644 index 0000000..5dcf171 --- /dev/null +++ b/News/feathers/link/locale/en_US.pot @@ -0,0 +1,39 @@ +# Chyrp v2.1 Translation File. +# Copyright (C) 2011 Chyrp Team +# This file is distributed under the same license as the Chyrp v2.1 package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Chyrp v2.1\n" +"Report-Msgid-Bugs-To: email@chyrp.net\n" +"POT-Creation-Date: 2011-01-10 22:16+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FIRST LAST \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: feathers/link/link.php:10 +msgid "Name" +msgstr "" + +#: feathers/link/link.php:6 +msgid "URL" +msgstr "" + +#: feathers/link/info.yaml:5 +msgid "Link to other sites, name it, and add an optional description." +msgstr "" + +#: feathers/link/info.yaml:1 +msgid "Link" +msgstr "" + +#: feathers/link/link.php:14 +msgid "Description" +msgstr "" + diff --git a/News/feathers/photo/info.yaml b/News/feathers/photo/info.yaml new file mode 100755 index 0000000..f3772de --- /dev/null +++ b/News/feathers/photo/info.yaml @@ -0,0 +1,8 @@ +name: Photo +url: http://chyrp.net/ +version: 2.0 +description: A photo feather, allowing you to upload any type of picture, with a caption. +author: + name: Alex Suraci + url: http://ecks.tc/ +uploader: true diff --git a/News/feathers/photo/locale/en_US.pot b/News/feathers/photo/locale/en_US.pot new file mode 100644 index 0000000..68649e7 --- /dev/null +++ b/News/feathers/photo/locale/en_US.pot @@ -0,0 +1,47 @@ +# Chyrp v2.1 Translation File. +# Copyright (C) 2011 Chyrp Team +# This file is distributed under the same license as the Chyrp v2.1 package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Chyrp v2.1\n" +"Report-Msgid-Bugs-To: email@chyrp.net\n" +"POT-Creation-Date: 2011-01-10 22:16+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FIRST LAST \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: feathers/photo/info.yaml:6 +msgid "A photo feather, allowing you to upload any type of picture, with a caption." +msgstr "" + +#: feathers/photo/photo.php:143 +msgid "Source" +msgstr "" + +#: feathers/photo/photo.php:138 +msgid "Alt-Text" +msgstr "" + +#: feathers/photo/photo.php:6 +#: feathers/photo/info.yaml:1 +msgid "Photo" +msgstr "" + +#: feathers/photo/photo.php:17 +msgid "Caption" +msgstr "" + +#: feathers/photo/photo.php:11 +#: feathers/photo/photo.php:39 +#: feathers/photo/photo.php:49 +#: feathers/photo/photo.php:148 +msgid "From URL?" +msgstr "" + diff --git a/News/feathers/photo/photo.php b/News/feathers/photo/photo.php new file mode 100755 index 0000000..58f03f1 --- /dev/null +++ b/News/feathers/photo/photo.php @@ -0,0 +1,153 @@ +setField(array("attr" => "photo", + "type" => "file", + "label" => __("Photo", "photo"), + "note" => "(Max. file size: ".ini_get('upload_max_filesize').")")); + if (isset($_GET['action']) and $_GET['action'] == "bookmarklet") + $this->setField(array("attr" => "from_url", + "type" => "text", + "label" => __("From URL?", "photo"), + "optional" => true, + "no_value" => true)); + $this->setField(array("attr" => "caption", + "type" => "text_block", + "label" => __("Caption", "photo"), + "optional" => true, + "preview" => true, + "bookmarklet" => "page_link")); + + $this->setFilter("caption", array("markup_text", "markup_post_text")); + + $this->respondTo("delete_post", "delete_file"); + $this->respondTo("filter_post", "filter_post"); + $this->respondTo("post_options", "add_option"); + $this->respondTo("admin_write_post", "swfupload"); + $this->respondTo("admin_edit_post", "swfupload"); + + if (isset($_GET['url']) and + preg_match("/http:\/\/(www\.)?flickr\.com\/photos\/([^\/]+)\/([0-9]+)/", $_GET['url'])) { + $this->bookmarkletSelected(); + + $page = get_remote($_GET['url']); + preg_match("/class=\"photoImgDiv\">\nsetField(array("attr" => "from_url", + "type" => "text", + "label" => __("From URL?", "photo"), + "optional" => true, + "value" => $image[1])); + } + + if (isset($_GET['url']) and preg_match("/\.(jpg|jpeg|png|gif|bmp)$/", $_GET['url'])) { + $this->bookmarkletSelected(); + + $this->setField(array("attr" => "from_url", + "type" => "text", + "label" => __("From URL?", "photo"), + "optional" => true, + "value" => $_GET['url'])); + } + } + + public function swfupload($admin, $post = null) { + if (isset($post) and $post->feather != "photo" or + isset($_GET['feather']) and $_GET['feather'] != "photo") + return; + + Trigger::current()->call("prepare_swfupload", "photo", "*.jpg;*.jpeg;*.png;*.gif;*.bmp"); + } + + public function submit() { + if (!isset($_POST['filename'])) { + if (isset($_FILES['photo']) and $_FILES['photo']['error'] == 0) + $filename = upload($_FILES['photo'], array("jpg", "jpeg", "png", "gif", "bmp")); + elseif (!empty($_POST['from_url'])) + $filename = upload_from_url($_POST['from_url'], array("jpg", "jpeg", "png", "gif", "bmp")); + else + error(__("Error"), __("Couldn't upload photo.")); + } else + $filename = $_POST['filename']; + + return Post::add(array("filename" => $filename, + "caption" => $_POST['caption']), + $_POST['slug'], + Post::check_url($_POST['slug'])); + } + + public function update($post) { + if (!isset($_POST['filename'])) + if (isset($_FILES['photo']) and $_FILES['photo']['error'] == 0) { + $this->delete_file($post); + $filename = upload($_FILES['photo'], array("jpg", "jpeg", "png", "gif", "tiff", "bmp")); + } elseif (!empty($_POST['from_url'])) { + $this->delete_file($post); + $filename = upload_from_url($_POST['from_url'], array("jpg", "jpeg", "png", "gif", "tiff", "bmp")); + } else + $filename = $post->filename; + else { + $this->delete_file($post); + $filename = $_POST['filename']; + } + + $post->update(array("filename" => $filename, + "caption" => $_POST['caption'])); + } + + public function title($post) { + return oneof($post->title_from_excerpt(), $post->filename); + } + public function excerpt($post) { + return $post->caption; + } + + public function feed_content($post) { + return self::image_tag($post, 500, 500)."

    ".$post->caption; + } + + public function delete_file($post) { + if ($post->feather != "photo") return; + unlink(MAIN_DIR.Config::current()->uploads_path.$post->filename); + } + + public function filter_post($post) { + if ($post->feather != "photo") return; + $post->image = $this->image_tag($post); + } + + public function image_tag($post, $max_width = 500, $max_height = null, $more_args = "quality=100") { + $filename = $post->filename; + $config = Config::current(); + $alt = !empty($post->alt_text) ? fix($post->alt_text, true) : $filename ; + return ''.$alt.''; + } + + public function image_link($post, $max_width = 500, $max_height = null, $more_args="quality=100") { + $source = !empty($post->source) ? $post->source : uploaded($post->filename) ; + return ''.$this->image_tag($post, $max_width, $max_height, $more_args).''; + } + + public function add_option($options, $post = null) { + if (isset($post) and $post->feather != "photo") return; + if (!isset($_GET['feather']) and Config::current()->enabled_feathers[0] != "photo" or + isset($_GET['feather']) and $_GET['feather'] != "photo") return; + + $options[] = array("attr" => "option[alt_text]", + "label" => __("Alt-Text", "photo"), + "type" => "text", + "value" => oneof(@$post->alt_text, "")); + + $options[] = array("attr" => "option[source]", + "label" => __("Source", "photo"), + "type" => "text", + "value" => oneof(@$post->source, "")); + + $options[] = array("attr" => "from_url", + "label" => __("From URL?", "photo"), + "type" => "text"); + + return $options; + } + } + diff --git a/News/feathers/quote/info.yaml b/News/feathers/quote/info.yaml new file mode 100755 index 0000000..1882914 --- /dev/null +++ b/News/feathers/quote/info.yaml @@ -0,0 +1,7 @@ +name: Quote +url: http://chyrp.net/ +version: 2.0 +description: Post quotes and cite sources. +author: + name: Alex Suraci + url: http://ecks.tc/ diff --git a/News/feathers/quote/locale/en_US.pot b/News/feathers/quote/locale/en_US.pot new file mode 100644 index 0000000..67208cb --- /dev/null +++ b/News/feathers/quote/locale/en_US.pot @@ -0,0 +1,36 @@ +# Chyrp v2.1 Translation File. +# Copyright (C) 2011 Chyrp Team +# This file is distributed under the same license as the Chyrp v2.1 package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Chyrp v2.1\n" +"Report-Msgid-Bugs-To: email@chyrp.net\n" +"POT-Creation-Date: 2011-01-10 22:16+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FIRST LAST \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: feathers/quote/quote.php:7 +#: feathers/quote/info.yaml:1 +msgid "Quote" +msgstr "" + +#: feathers/quote/info.yaml:5 +msgid "Post quotes and cite sources." +msgstr "" + +#: feathers/quote/quote.php:24 +msgid "Quote can't be empty." +msgstr "" + +#: feathers/quote/quote.php:13 +msgid "Source" +msgstr "" + diff --git a/News/feathers/quote/quote.php b/News/feathers/quote/quote.php new file mode 100755 index 0000000..21fcf5b --- /dev/null +++ b/News/feathers/quote/quote.php @@ -0,0 +1,59 @@ +setField(array("attr" => "quote", + "type" => "text_block", + "rows" => 5, + "label" => __("Quote", "quote"), + "preview" => true, + "bookmarklet" => "selection")); + $this->setField(array("attr" => "source", + "type" => "text_block", + "rows" => 5, + "label" => __("Source", "quote"), + "optional" => true, + "preview" => true, + "bookmarklet" => "page_link")); + + $this->setFilter("quote", array("markup_text", "markup_post_text")); + $this->setFilter("source", array("markup_text", "markup_post_text")); + } + + public function submit() { + if (empty($_POST['quote'])) + error(__("Error"), __("Quote can't be empty.", "quote")); + + return Post::add(array("quote" => $_POST['quote'], + "source" => $_POST['source']), + $_POST['slug'], + Post::check_url($_POST['slug'])); + } + + public function update($post) { + if (empty($_POST['quote'])) + error(__("Error"), __("Quote can't be empty.")); + + $post->update(array("quote" => $_POST['quote'], + "source" => $_POST['source'])); + } + + public function title($post) { + return $post->title_from_excerpt(); + } + + public function excerpt($post) { + return $post->quote; + } + + public function add_dash($text) { + return preg_replace("/(]+)?>|^)/si", "\\1— ", $text, 1); + } + + public function feed_content($post) { + $body = "
    \n\t"; + $body.= $post->quote; + $body.= "\n
    \n"; + $body.= $post->source; + return $body; + } + } diff --git a/News/feathers/text/info.yaml b/News/feathers/text/info.yaml new file mode 100755 index 0000000..e94e6d5 --- /dev/null +++ b/News/feathers/text/info.yaml @@ -0,0 +1,7 @@ +name: Text +url: http://chyrp.net/ +version: 2.0 +description: A basic text feather. +author: + name: Alex Suraci + url: http://ecks.tc/ diff --git a/News/feathers/text/locale/en_US.pot b/News/feathers/text/locale/en_US.pot new file mode 100644 index 0000000..1942e7f --- /dev/null +++ b/News/feathers/text/locale/en_US.pot @@ -0,0 +1,35 @@ +# Chyrp v2.1 Translation File. +# Copyright (C) 2011 Chyrp Team +# This file is distributed under the same license as the Chyrp v2.1 package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Chyrp v2.1\n" +"Report-Msgid-Bugs-To: email@chyrp.net\n" +"POT-Creation-Date: 2011-01-10 22:17+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FIRST LAST \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: feathers/text/info.yaml:1 +msgid "Text" +msgstr "" + +#: feathers/text/text.php:11 +msgid "Body" +msgstr "" + +#: feathers/text/info.yaml:5 +msgid "A basic text feather." +msgstr "" + +#: feathers/text/text.php:6 +msgid "Title" +msgstr "" + diff --git a/News/feathers/text/text.php b/News/feathers/text/text.php new file mode 100755 index 0000000..4d1ba24 --- /dev/null +++ b/News/feathers/text/text.php @@ -0,0 +1,50 @@ +setField(array("attr" => "title", + "type" => "text", + "label" => __("Title", "text"), + "optional" => true, + "bookmarklet" => "title")); + $this->setField(array("attr" => "body", + "type" => "text_block", + "label" => __("Body", "text"), + "preview" => true, + "bookmarklet" => "selection")); + + $this->setFilter("title", array("markup_title", "markup_post_title")); + $this->setFilter("body", array("markup_text", "markup_post_text")); + } + + public function submit() { + if (empty($_POST['body'])) + error(__("Error"), __("Body can't be blank.")); + + fallback($_POST['slug'], sanitize($_POST['title'])); + + return Post::add(array("title" => $_POST['title'], + "body" => $_POST['body']), + $_POST['slug'], + Post::check_url($_POST['slug'])); + } + + public function update($post) { + if (empty($_POST['body'])) + error(__("Error"), __("Body can't be blank.")); + + $post->update(array("title" => $_POST['title'], + "body" => $_POST['body'])); + } + + public function title($post) { + return oneof($post->title, $post->title_from_excerpt()); + } + + public function excerpt($post) { + return $post->body; + } + + public function feed_content($post) { + return $post->body; + } + } diff --git a/News/feathers/video/info.yaml b/News/feathers/video/info.yaml new file mode 100755 index 0000000..a6c9740 --- /dev/null +++ b/News/feathers/video/info.yaml @@ -0,0 +1,7 @@ +name: Video +url: http://chyrp.net/ +version: 2.0 +description: Lets you post videos to your site. +author: + name: Alex Suraci + url: http://ecks.tc/ diff --git a/News/feathers/video/locale/en_US.pot b/News/feathers/video/locale/en_US.pot new file mode 100644 index 0000000..39cda94 --- /dev/null +++ b/News/feathers/video/locale/en_US.pot @@ -0,0 +1,32 @@ +# Chyrp v2.1 Translation File. +# Copyright (C) 2011 Chyrp Team +# This file is distributed under the same license as the Chyrp v2.1 package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: Chyrp v2.1\n" +"Report-Msgid-Bugs-To: email@chyrp.net\n" +"POT-Creation-Date: 2011-01-10 22:17+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FIRST LAST \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n" + +#: feathers/video/info.yaml:5 +msgid "Lets you post videos to your site." +msgstr "" + +#: feathers/video/video.php:7 +#: feathers/video/info.yaml:1 +msgid "Video" +msgstr "" + +#: feathers/video/video.php:15 +msgid "Caption" +msgstr "" + diff --git a/News/feathers/video/video.php b/News/feathers/video/video.php new file mode 100755 index 0000000..7671096 --- /dev/null +++ b/News/feathers/video/video.php @@ -0,0 +1,125 @@ +setField(array("attr" => "video", + "type" => "text_block", + "rows" => 4, + "label" => __("Video", "video"), + "preview" => true, + "bookmarklet" => $this->isVideo() ? + "url" : + "")); + $this->setField(array("attr" => "caption", + "type" => "text_block", + "rows" => 4, + "label" => __("Caption", "video"), + "optional" => true, + "preview" => true, + "bookmarklet" => "selection")); + + if ($this->isVideo()) + $this->bookmarkletSelected(); + + $this->setFilter("caption", array("markup_text", "markup_post_text")); + + $this->respondTo("preview_video", "embed_tag"); + } + + public function submit() { + if (empty($_POST['video'])) + error(__("Error"), __("Video can't be blank.")); + + return Post::add(array("embed" => $this->embed_tag($_POST['video']), + "video" => $_POST['video'], + "caption" => $_POST['caption']), + $_POST['slug'], + Post::check_url($_POST['slug'])); + } + + public function update($post) { + if (empty($_POST['video'])) + error(__("Error"), __("Video can't be blank.")); + + $post->update(array("embed" => $this->embed_tag($_POST['video']), + "video" => $_POST['video'], + "caption" => $_POST['caption'])); + } + + public function title($post) { + return $post->title_from_excerpt(); + } + + public function excerpt($post) { + return $post->caption; + } + + public function feed_content($post) { + return $post->embed."

    ".$post->caption; + } + + public function embed_tag($video, $field = null) { # We use this for previewing too + if (isset($field) and $field != "embed") + return $video; # If they're previewing and the field argument isn't the embed, return the original. + + if (preg_match("/http:\/\/(www\.|[a-z]{2}\.)?youtube\.com\/watch\?v=([^&]+)/", $video, $matches)) { + return ''; + } else if (preg_match("/^http:\/\/(www\.)?vimeo.com\/([0-9]+)/", $video, $matches)) { + $site = get_remote("http://vimeo.com/".$matches[2]); + preg_match('/
    /', + $site, + $scale); + return ''; + } else if (preg_match("/http:\/\/(www\.)?metacafe.com\/watch\/([0-9]+)\/([^\/&\?]+)/", $video, $matches)) { + return ''; + } else if (preg_match("/http:\/\/(www\.)?revver.com\/video\/([0-9]+)/", $video, $matches)) { + return ''; + } else if (preg_match("/http:\/\/(www\.)viddler\.com\/.+/", $video)) { + $viddler_page = get_remote($video); + + if (preg_match("/'; + } + + return $video; + } + + return $video; + } + + public function embed_tag_for($post, $max_width = 500) { + $post->embed = preg_replace("/&([[:alnum:]_]+)=/", "&\\1=", $post->embed); + + if (preg_match("/width(=\"|='|:\s*)([0-9]+)/", $post->embed, $width)) { + $sep_w = $width[1]; + $original_width = $width[2]; + } else + return $post->embed; + + if (preg_match("/height(=\"|='|:\s*)([0-9]+)/", $post->embed, $height)) { + $sep_h = $height[1]; + $original_height = $height[2]; + + $new_height = (int) (($max_width / $original_width) * $original_height); + } + + $post->embed = str_replace(array($width[0], $height[0]), array("width".$sep_w.$max_width, "height".$sep_h.$new_height), $post->embed); + + return $post->embed; + } + + public function isVideo() { + if (!isset($_GET['url'])) + return false; + + if (preg_match("/http:\/\/(www\.|[a-z]{2}\.)?youtube\.com\/watch\?v=([^&]+)/", $_GET['url']) or + preg_match("/http:\/\/(www\.)?vimeo.com\/([0-9]+)/", $_GET['url']) or + preg_match('/http:\/\/(www\.)?metacafe.com\/watch\/([0-9]+)\/([^\/&\?]+)/', $_GET['url']) or + preg_match("/http:\/\/(www\.)?revver.com\/video\/([0-9]+)/", $_GET['url']) or + preg_match("/http:\/\/(www\.)viddler\.com\/.+/", $_GET['url'])) + return true; + + return false; + } + } diff --git a/News/includes/admin.js.php b/News/includes/admin.js.php new file mode 100644 index 0000000..9037a4a --- /dev/null +++ b/News/includes/admin.js.php @@ -0,0 +1,741 @@ + + diff --git a/News/includes/ajax.php b/News/includes/ajax.php new file mode 100755 index 0000000..bcfe753 --- /dev/null +++ b/News/includes/ajax.php @@ -0,0 +1,207 @@ +group->can("view_site")) + if ($trigger->exists("can_not_view_site")) + $trigger->call("can_not_view_site"); + else + show_403(__("Access Denied"), __("You are not allowed to view this site.")); + + switch($_POST['action']) { + case "edit_post": + if (!isset($_POST['id'])) + error(__("No ID Specified"), __("Please specify an ID of the post you would like to edit.")); + + $post = new Post($_POST['id'], array("filter" => false, "drafts" => true)); + + if ($post->no_results) { + header("HTTP/1.1 404 Not Found"); + $trigger->call("not_found"); + exit; + } + + if (!$post->editable()) + show_403(__("Access Denied"), __("You do not have sufficient privileges to edit posts.")); + + $title = $post->title(); + $theme_file = THEME_DIR."/forms/feathers/".$post->feather.".php"; + $default_file = FEATHERS_DIR."/".$post->feather."/fields.php"; + + $options = array(); + Trigger::current()->filter($options, array("edit_post_options", "post_options"), $post); + + $main->display("forms/post/edit", array("post" => $post, + "feather" => Feathers::$instances[$post->feather], + "options" => $options, + "groups" => Group::find(array("order" => "id ASC")))); + break; + + case "delete_post": + $post = new Post($_POST['id'], array("drafts" => true)); + + if ($post->no_results) { + header("HTTP/1.1 404 Not Found"); + $trigger->call("not_found"); + exit; + } + + if (!$post->deletable()) + show_403(__("Access Denied"), __("You do not have sufficient privileges to delete this post.")); + + Post::delete($_POST['id']); + break; + + case "view_post": + fallback($_POST['offset'], 0); + fallback($_POST['context']); + + $reason = (isset($_POST['reason'])) ? $_POST['reason'] : "" ; + + if (isset($_POST['id'])) + $post = new Post($_POST['id'], array("drafts" => true)); + + if ($post->no_results) { + header("HTTP/1.1 404 Not Found"); + $trigger->call("not_found"); + exit; + } + + $main->display("feathers/".$post->feather, array("post" => $post, "ajax_reason" => $reason)); + break; + + case "preview": + if (empty($_POST['content'])) + break; + + $trigger->filter($_POST['content'], + array("preview_".$_POST['feather'], "preview"), + $_POST['field'], + $_POST['feather']); + + echo "

    ".__("Preview")."

    \n". + "
    ".fix($_POST['content'])."
    "; + break; + + case "check_confirm": + if (!$visitor->group->can("toggle_extensions")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to enable/disable extensions.")); + + $dir = ($_POST['type'] == "module") ? MODULES_DIR : FEATHERS_DIR ; + $info = YAML::load($dir."/".$_POST['check']."/info.yaml"); + fallback($info["confirm"], ""); + + if (!empty($info["confirm"])) + echo __($info["confirm"], $_POST['check']); + + break; + + case "organize_pages": + foreach ($_POST['parent'] as $id => $parent) + $sql->update("pages", array("id" => $id), array("parent_id" => $parent)); + + foreach ($_POST['page_list'] as $index => $page) + $sql->update("pages", array("id" => $page), array("list_order" => $index)); + + break; + + case "enable_module": case "enable_feather": + $type = ($_POST['action'] == "enable_module") ? "module" : "feather" ; + + if (!$visitor->group->can("change_settings")) + if ($type == "module") + exit("{ \"notifications\": [\"".__("You do not have sufficient privileges to enable/disable modules.")."\"] }"); + else + exit("{ \"notifications\": [\"".__("You do not have sufficient privileges to enable/disable feathers.")."\"] }"); + + if (($type == "module" and module_enabled($_POST['extension'])) or + ($type == "feather" and feather_enabled($_POST['extension']))) + exit("{ \"notifications\": [] }"); + + $enabled_array = ($type == "module") ? "enabled_modules" : "enabled_feathers" ; + $folder = ($type == "module") ? MODULES_DIR : FEATHERS_DIR ; + + if (file_exists($folder."/".$_POST["extension"]."/locale/".$config->locale.".mo")) + load_translator($_POST["extension"], $folder."/".$_POST["extension"]."/locale/".$config->locale.".mo"); + + $info = YAML::load($folder."/".$_POST["extension"]."/info.yaml"); + fallback($info["uploader"], false); + fallback($info["notifications"], array()); + + foreach ($info["notifications"] as &$notification) + $notification = addslashes(__($notification, $_POST["extension"])); + + require $folder."/".$_POST["extension"]."/".$_POST["extension"].".php"; + + if ($info["uploader"]) + if (!file_exists(MAIN_DIR.$config->uploads_path)) + $info["notifications"][] = _f("Please create the %s directory at your Chyrp install's root and CHMOD it to 777.", array($config->uploads_path)); + elseif (!is_writable(MAIN_DIR.$config->uploads_path)) + $info["notifications"][] = _f("Please CHMOD %s to 777.", array($config->uploads_path)); + + $class_name = camelize($_POST["extension"]); + + if ($type == "module" and !is_subclass_of($class_name, "Modules")) + error("", __("Item is not a module.")); + + if ($type == "feather" and !is_subclass_of($class_name, "Feathers")) + error("", __("Item is not a feather.")); + + if (method_exists($class_name, "__install")) + call_user_func(array($class_name, "__install")); + + $new = $config->$enabled_array; + array_push($new, $_POST["extension"]); + $config->set($enabled_array, $new); + + exit('{ "notifications": ['. + (!empty($info["notifications"]) ? '"'.implode('", "', $info["notifications"]).'"' : ""). + '] }'); + + break; + + case "disable_module": case "disable_feather": + $type = ($_POST['action'] == "disable_module") ? "module" : "feather" ; + + if (!$visitor->group->can("change_settings")) + if ($type == "module") + exit("{ \"notifications\": [\"".__("You do not have sufficient privileges to enable/disable modules.")."\"] }"); + else + exit("{ \"notifications\": [\"".__("You do not have sufficient privileges to enable/disable feathers.")."\"] }"); + + if (($type == "module" and !module_enabled($_POST['extension'])) or + ($type == "feather" and !feather_enabled($_POST['extension']))) + exit("{ \"notifications\": [] }"); + + $class_name = camelize($_POST["extension"]); + if (method_exists($class_name, "__uninstall")) + call_user_func(array($class_name, "__uninstall"), ($_POST['confirm'] == "1")); + + $enabled_array = ($type == "module") ? "enabled_modules" : "enabled_feathers" ; + $config->set($enabled_array, + array_diff($config->$enabled_array, array($_POST['extension']))); + + exit('{ "notifications": [] }'); + + break; + + case "reorder_feathers": + $reorder = oneof(@$_POST['list'], $config->enabled_feathers); + foreach ($reorder as &$value) + $value = preg_replace("/feathers\[([^\]]+)\]/", "\\1", $value); + + $config->set("enabled_feathers", $reorder); + break; + } + + $trigger->call("ajax"); + + if (!empty($_POST['action'])) + $trigger->call("ajax_".$_POST['action']); diff --git a/News/includes/class/Config.php b/News/includes/class/Config.php new file mode 100755 index 0000000..a7e1cbc --- /dev/null +++ b/News/includes/class/Config.php @@ -0,0 +1,114 @@ + $val array. + private $yaml = array(); + + /** + * Function: __construct + * Loads the configuration YAML file. + */ + private function __construct() { + if (!file_exists(INCLUDES_DIR."/config.yaml.php")) + return false; + + $contents = str_replace("\n", + "", + file_get_contents(INCLUDES_DIR."/config.yaml.php")); + + $this->yaml = YAML::load($contents); + + $arrays = array("enabled_modules", "enabled_feathers", "routes"); + foreach ($this->yaml as $setting => $value) + if (in_array($setting, $arrays) and empty($value)) + $this->$setting = array(); + elseif (!is_int($setting)) + $this->$setting = (is_string($value)) ? stripslashes($value) : $value ; + + fallback($this->url, $this->chyrp_url); + } + + /** + * Function: set + * Adds or replaces a configuration setting with the given value. + * + * Parameters: + * $setting - The setting name. + * $value - The value. + * $overwrite - If the setting exists and is the same value, should it be overwritten? + */ + public function set($setting, $value, $overwrite = true) { + if (isset($this->$setting) and $this->$setting == $value and !$overwrite) + return false; + + if (isset($this->file) and file_exists($this->file)) { + $contents = str_replace("\n", + "", + file_get_contents($this->file)); + + $this->yaml = YAML::load($contents); + } + + # Add the setting + $this->yaml[$setting] = $this->$setting = $value; + + if (class_exists("Trigger")) + Trigger::current()->call("change_setting", $setting, $value, $overwrite); + + # Add the PHP protection! + $contents = "\n"; + + # Generate the new YAML settings + $contents.= YAML::dump($this->yaml); + + if (!@file_put_contents(INCLUDES_DIR."/config.yaml.php", $contents)) { + Flash::warning(_f("Could not set \"%s\" configuration setting because %s is not writable.", + array($setting, "/includes/config.yaml.php"))); + return false; + } else + return true; + } + + /** + * Function: remove + * Removes a configuration setting. + * + * Parameters: + * $setting - The name of the setting to remove. + */ + public function remove($setting) { + if (isset($this->file) and file_exists($this->file)) { + $contents = str_replace("\n", + "", + file_get_contents($this->file)); + + $this->yaml = YAML::load($contents); + } + + # Add the setting + unset($this->yaml[$setting]); + + # Add the PHP protection! + $contents = "\n"; + + # Generate the new YAML settings + $contents.= YAML::dump($this->yaml); + + file_put_contents(INCLUDES_DIR."/config.yaml.php", $contents); + } + + /** + * Function: current + * Returns a singleton reference to the current configuration. + */ + public static function & current() { + static $instance = null; + return $instance = (empty($instance)) ? new self() : $instance ; + } + } diff --git a/News/includes/class/Feathers.php b/News/includes/class/Feathers.php new file mode 100755 index 0000000..e8b72f9 --- /dev/null +++ b/News/includes/class/Feathers.php @@ -0,0 +1,106 @@ + + */ + protected function setFilter($field, $name) { + self::$filters[get_class($this)][] = array("field" => $field, "name" => $name); + } + + /** + * Function: customFilter + * Allows a Feather to apply its own filter to a specified field. + * + * Parameters: + * $field - Attribute of the post to filter. + * $name - Name of the class function to use as the filter. + * $priority - Priority of the filter. + * + * See Also: + * + */ + protected function customFilter($field, $name, $priority = 10) { + self::$custom_filters[get_class($this)][] = array("field" => $field, "name" => $name); + } + + /** + * Function: respondTo + * Allows a Feather to respond to a Trigger as a Module would. + * + * Parameters: + * $name - Name of the trigger to respond to. + * $function - Name of the class function to respond with. + * $priority - Priority of the response. + * + * See Also: + * + */ + protected function respondTo($name, $function = null, $priority = 10) { + fallback($function, $name); + Trigger::current()->priorities[$name][] = array("priority" => $priority, "function" => array($this, $function)); + } + + /** + * Function: setField + * Sets the feather's fields for creating/editing posts with that feather. + * + * Parameters: + * $options - An array of key => val options for the field. + * + * Options: + * attr - The technical name for the field. Think $post->attr. + * type - The field type. (text, file, text_block, or select) + * label - The label for the field. + * preview - Is this field previewable? + * optional - Is this field optional? + * bookmarklet - What to fill this field by in the bookmarklet. + * url or page_url - The URL of the page they're viewing when they open the bookmarklet. + * title or page_title - The title of the page they're viewing when they open the bookmarklet. + * selection - Their selection on the page they're viewing when they open the bookmarklet. + * extra - Stuff to output after the input field. Can be anything. + * note - A minor note to display next to the label text. + */ + protected function setField($options) { + fallback($options["classes"], array()); + + if (isset($options["class"])) + $options["classes"][] = $options["class"]; + + if (isset($options["preview"]) and $options["preview"]) + $options["classes"][] = "preview_me"; + + $this->fields[$options["attr"]] = $options; + } + + /** + * Function: bookmarkletSelected + * The Feather that this function is called from will be selected when they open the Bookmarklet. + */ + protected function bookmarkletSelected() { + AdminController::current()->selected_bookmarklet = $this->safename; + } + } diff --git a/News/includes/class/Flash.php b/News/includes/class/Flash.php new file mode 100644 index 0000000..85be928 --- /dev/null +++ b/News/includes/class/Flash.php @@ -0,0 +1,197 @@ + false, + "notice" => false, + "warning" => false, + null => false); + + /** + * Function: __construct + * Removes empty notification variables from the session. + */ + private function __construct() { + foreach (array("messages", "notices", "warnings") as $type) + if (isset($_SESSION[$type]) and empty($_SESSION[$type])) + unset($_SESSION[$type]); + } + + /** + * Function: prepare + * Prepare the structure of the "flash" session value. + */ + static function prepare($type) { + if (!isset($_SESSION)) + $_SESSION = array(); + + if (!isset($_SESSION[$type])) + $_SESSION[$type] = array(); + } + + /** + * Function: message + * Add a message (neutral) to the session. + * + * Parameters: + * $message - Message to display. + * $redirect_to - URL to redirect to after the message is stored. + */ + static function message($message, $redirect_to = null) { + self::prepare("messages"); + + $_SESSION['messages'][] = Trigger::current()->filter($message, "flash_message", $redirect_to); + + if (isset($redirect_to)) + redirect($redirect_to); + } + + /** + * Function: notice + * Add a notice (positive) message to the session. + * + * Parameters: + * $message - Message to display. + * $redirect_to - URL to redirect to after the message is stored. + */ + static function notice($message, $redirect_to = null) { + self::prepare("notices"); + + $_SESSION['notices'][] = Trigger::current()->filter($message, "flash_notice_message", $redirect_to); + + if (TESTER) + exit("SUCCESS: ".$message); + + if (isset($redirect_to)) + redirect($redirect_to); + } + + /** + * Function: warning + * Add a warning (negative) message to the session. + * + * Parameters: + * $message - Message to display. + * $redirect_to - URL to redirect to after the message is stored. + */ + static function warning($message, $redirect_to = null) { + self::prepare("warnings"); + + $_SESSION['warnings'][] = Trigger::current()->filter($message, "flash_warning_message", $redirect_to); + + if (TESTER) + exit("ERROR: ".$message); + + if (isset($redirect_to)) + redirect($redirect_to); + } + + /** + * Function: messages + * Calls "messages". + */ + public function messages() { + return $this->serve("messages"); + } + + /** + * Function: notices + * Calls "notices". + */ + public function notices() { + return $this->serve("notices"); + } + + /** + * Function: warnings + * Calls "warnings". + */ + public function warnings() { + return $this->serve("warnings"); + } + + /** + * Function: all + * Returns an associative array of all messages and destroys their session values. + * + * Returns: + * An array of every message available, in the form of [type => [messages]]. + */ + public function all() { + return array("messages" => $this->messages(), + "notices" => $this->notices(), + "warnings" => $this->warnings()); + } + + /** + * Function: serve + * Serves a message of type $type and destroys it from the session. + * + * Parameters: + * $type - Type of messages to serve. + * + * Returns: + * An array of messages of the requested type. + */ + public function serve($type) { + if (!empty($_SESSION[$type])) + self::$exists[depluralize($type)] = self::$exists[null] = true; + + if (isset($_SESSION[$type])) { + $this->$type = $_SESSION[$type]; + $_SESSION[$type] = array(); + } + + return $this->$type; + } + + /** + * Function: exists + * Checks for flash messages. + * + * Parameters: + * $type - The type of message to check for. + */ + static function exists($type = null) { + if (self::$exists[$type]) + return self::$exists[$type]; + + if (isset($type)) + return self::$exists[$type] = !empty($_SESSION[pluralize($type)]); + else + foreach (array("messages", "notices", "warnings") as $type) + if (!empty($_SESSION[$type])) + return self::$exists[depluralize($type)] = self::$exists[null] = true; + + return false; + } + + /** + * Function: current + * Returns a singleton reference to the current class. + */ + public static function & current() { + static $instance = null; + return $instance = (empty($instance)) ? new self() : $instance ; + } + } diff --git a/News/includes/class/Model.php b/News/includes/class/Model.php new file mode 100644 index 0000000..7f4cc1c --- /dev/null +++ b/News/includes/class/Model.php @@ -0,0 +1,465 @@ +__placeholders) and $this->__placeholders); + + Trigger::current()->filter($filtered, $model_name."_".$name."_attr", $this); + if ($filtered !== false) + $this->$name = $filtered; + + $this->belongs_to = (array) $this->belongs_to; + $this->has_many = (array) $this->has_many; + $this->has_one = (array) $this->has_one; + + if (in_array($name, $this->belongs_to) or isset($this->belongs_to[$name])) { + $class = (isset($this->belongs_to[$name])) ? $this->belongs_to[$name] : $name ; + if (isset($this->belongs_to[$name])) { + $opts =& $this->belongs_to[$name]; + $model = oneof(@$opts["model"], $name); + + if (preg_match("/^\(([a-z0-9_]+)\)$/", $model, $match)) + $model = $this->$match[1]; + + $match = oneof(@$opts["by"], strtolower($name)); + + fallback($opts["where"], array("id" => $this->{$match."_id"})); + + $opts["where"] = (array) $opts["where"]; + foreach ($opts["where"] as &$val) + if (preg_match("/^\(([a-z0-9_]+)\)$/", $val, $match)) + $val = $this->$match[1]; + + fallback($opts["placeholders"], $placeholders); + } else { + $model = $name; + $opts = array("where" => array("id" => $this->{$name."_id"})); + } + + return $this->$name = new $model(null, $opts); + } elseif (in_array($name, $this->has_many) or isset($this->has_many[$name])) { + if (isset($this->has_many[$name])) { + $opts =& $this->has_many[$name]; + $model = oneof(@$opts["model"], depluralize($name)); + + if (preg_match("/^\(([a-z0-9_]+)\)$/", $model, $match)) + $model = $this->$match[1]; + + $match = oneof(@$opts["by"], strtolower($name)); + + fallback($opts["where"], array($match."_id" => $this->id)); + + $opts["where"] = (array) $opts["where"]; + foreach ($opts["where"] as &$val) + if (preg_match("/^\(([a-z0-9_]+)\)$/", $val, $match)) + $val = $this->$match[1]; + + fallback($opts["placeholders"], $placeholders); + } else { + $model = depluralize($name); + $match = $model_name; + $opts = array("where" => array(strtolower($match)."_id" => $this->id), + "placeholders" => $placeholders); + } + + return $this->$name = call_user_func(array($model, "find"), $opts); + } elseif (in_array($name, $this->has_one) or isset($this->has_one[$name])) { + if (isset($this->has_one[$name])) { + $opts =& $this->has_one[$name]; + $model = oneof(@$opts["model"], depluralize($name)); + + if (preg_match("/^\(([a-z0-9_]+)\)$/", $model, $match)) + $model = $this->$match[1]; + + $match = oneof(@$opts["by"], strtolower($name)); + + fallback($opts["where"], array($match."_id" => $this->id)); + + $opts["where"] = (array) $opts["where"]; + foreach ($opts["where"] as &$val) + if (preg_match("/^\(([a-z0-9_]+)\)$/", $val, $match)) + $val = $this->$match[1]; + } else { + $model = depluralize($name); + $match = $model_name; + $opts = array("where" => array(strtolower($match)."_id" => $this->id)); + } + + return $this->$name = new $model(null, $opts); + } + + if (isset($this->$name)) + return $this->$name; + } + + /** + * Function: __getPlaceholders + * Calls __get with the requested $name, but grabs everything as placeholders. + * + * Parameters: + * $name - Name to call with. + * + * Returns: + * @mixed@ + * + * See Also: + * + */ + public function __getPlaceholders($name) { + $this->__placeholders = true; + $return = $this->__get($name); + unset($this->__placeholders); + return $return; + } + + /** + * Function: grab + * Grabs a single model from the database. + * + * Parameters: + * $model - The instantiated model class to pass the object to (e.g. Post). + * $id - The ID of the model to grab. Can be null. + * $options - An array of options, mostly SQL things. + * + * Options: + * select - What to grab from the table. @(modelname)s@ by default. + * from - Which table(s) to grab from? @(modelname)s.*@ by default. + * left_join - A @LEFT JOIN@ associative array. Example: @array("table" => "foo", "where" => "foo = :bar")@ + * where - A string or array of conditions. @array("__(modelname)s.id = :id")@ by default. + * params - An array of parameters to pass to PDO. @array(":id" => $id)@ by default. + * group - A string or array of "GROUP BY" conditions. + * order - What to order the SQL result by. @__(modelname)s.id DESC@ by default. + * offset - Offset for SQL query. + * read_from - An array to read from instead of performing another query. + */ + protected static function grab($model, $id, $options = array()) { + $model_name = strtolower(get_class($model)); + + if ($model_name == "visitor") + $model_name = "user"; + + if (!isset($id) and isset($options["where"]["id"])) + $id = $options["where"]["id"]; + + $cache = (is_numeric($id) and isset(self::$caches[$model_name][$id])) ? + self::$caches[$model_name][$id] : + ((isset($options["read_from"]["id"]) and isset(self::$caches[$model_name][$options["read_from"]["id"]])) ? + self::$caches[$model_name][$options["read_from"]["id"]] : + (isset(self::$caches[$model_name][serialize($id)]) ? + self::$caches[$model_name][serialize($id)] : + array())) ; + + # Is this model already in the cache? + if (!empty($cache)) { + foreach ($cache as $attr => $val) + $model->$attr = $val; + + return; + } + + fallback($options["select"], "*"); + fallback($options["from"], ($model_name == "visitor" ? "users" : pluralize($model_name))); + fallback($options["left_join"], array()); + fallback($options["where"], array()); + fallback($options["params"], array()); + fallback($options["group"], array()); + fallback($options["order"], "id DESC"); + fallback($options["offset"], null); + fallback($options["read_from"], array()); + fallback($options["ignore_dupes"], array()); + + $options["where"] = (array) $options["where"]; + $options["from"] = (array) $options["from"]; + $options["select"] = (array) $options["select"]; + + if (is_numeric($id)) + $options["where"]["id"] = $id; + elseif (is_array($id)) + $options["where"] = array_merge($options["where"], $id); + + $trigger = Trigger::current(); + $trigger->filter($options, $model_name."_grab"); + + $sql = SQL::current(); + if (!empty($options["read_from"])) + $read = $options["read_from"]; + else { + $query = $sql->select($options["from"], + $options["select"], + $options["where"], + $options["order"], + $options["params"], + null, + $options["offset"], + $options["group"], + $options["left_join"]); + $all = $query->fetchAll(); + + if (count($all) == 1) + $read = $all[0]; + else { + $merged = array(); + + foreach ($all as $index => $row) + foreach ($row as $column => $val) + $merged[$row["id"]][$column][] = $val; + + foreach ($all as $index => &$row) + $row = $merged[$row["id"]]; + + if (count($all)) { + $keys = array_keys($all); + $read = $all[$keys[0]]; + foreach ($read as $name => &$column) { + $column = (!in_array($name, $options["ignore_dupes"]) ? + array_unique($column) : + $column); + $column = (count($column) == 1) ? + $column[0] : + $column ; + } + } else + $read = false; + } + } + + if (!count($read) or !$read) + return $model->no_results = true; + else + $model->no_results = false; + + foreach ($read as $key => $val) + if (!is_int($key)) + $model->$key = $val; + + if (isset($query) and isset($query->queryString)) + $model->queryString = $query->queryString; + + if (isset($model->updated_at)) + $model->updated = (!empty($model->updated_at) and $model->updated_at != "0000-00-00 00:00:00"); + + $clone = clone $model; + + self::$caches[$model_name][$read["id"]] = $clone; + + if (!is_numeric($id) and !isset($options["read_from"]["id"]) and $id !== null) + self::$caches[$model_name][serialize($id)] = $clone; + } + + /** + * Function: search + * Returns an array of model objects that are found by the $options array. + * + * Parameters: + * $options - An array of options, mostly SQL things. + * $options_for_object - An array of options for the instantiation of the model. + * + * Options: + * select - What to grab from the table. @(modelname)s@ by default. + * from - Which table(s) to grab from? @(modelname)s.*@ by default. + * left_join - A @LEFT JOIN@ associative array. Example: @array("table" => "foo", "where" => "foo = :bar")@ + * where - A string or array of conditions. @array("__(modelname)s.id = :id")@ by default. + * params - An array of parameters to pass to PDO. @array(":id" => $id)@ by default. + * group - A string or array of "GROUP BY" conditions. + * order - What to order the SQL result by. @__(modelname)s.id DESC@ by default. + * offset - Offset for SQL query. + * limit - Limit for SQL query. + * + * See Also: + * + */ + protected static function search($model, $options = array(), $options_for_object = array()) { + $model_name = strtolower($model); + + fallback($options["select"], "*"); + fallback($options["from"], pluralize(strtolower($model))); + fallback($options["left_join"], array()); + fallback($options["where"], null); + fallback($options["params"], array()); + fallback($options["group"], array()); + fallback($options["order"], "id DESC"); + fallback($options["offset"], null); + fallback($options["limit"], null); + fallback($options["placeholders"], false); + fallback($options["ignore_dupes"], array()); + + $options["where"] = (array) $options["where"]; + $options["from"] = (array) $options["from"]; + $options["select"] = (array) $options["select"]; + + $trigger = Trigger::current(); + $trigger->filter($options, pluralize(strtolower($model_name))."_get"); + + $grab = SQL::current()->select($options["from"], + $options["select"], + $options["where"], + $options["order"], + $options["params"], + $options["limit"], + $options["offset"], + $options["group"], + $options["left_join"])->fetchAll(); + + $shown_dates = array(); + $results = array(); + + $rows = array(); + + foreach ($grab as $row) + foreach ($row as $column => $val) + $rows[$row["id"]][$column][] = $val; + + foreach ($rows as &$row) + foreach ($row as $name => &$column) { + $column = (!in_array($name, $options["ignore_dupes"]) ? + array_unique($column) : + $column); + $column = (count($column) == 1) ? + $column[0] : + $column ; + } + + foreach ($rows as $result) { + if ($options["placeholders"]) { + $results[] = $result; + continue; + } + + $options_for_object["read_from"] = $result; + $result = new $model(null, $options_for_object); + + if (isset($result->created_at)) { + $pinned = (isset($result->pinned) and $result->pinned); + $shown = in_array(when("m-d-Y", $result->created_at), $shown_dates); + + $result->first_of_day = (!$pinned and !$shown and !AJAX); + + if (!$pinned and !$shown) + $shown_dates[] = when("m-d-Y", $result->created_at); + } + + $results[] = $result; + } + + return ($options["placeholders"]) ? array($results, $model_name) : $results ; + } + + /** + * Function: delete + * Deletes a given object. Calls the @delete_(model)@ trigger with the objects ID. + * + * Parameters: + * $model - The model name. + * $id - The ID of the object to delete. + */ + protected static function destroy($model, $id) { + $model = strtolower($model); + if (Trigger::current()->exists("delete_".$model)) + Trigger::current()->call("delete_".$model, new $model($id)); + + SQL::current()->delete(pluralize($model), array("id" => $id)); + } + + /** + * Function: deletable + * Checks if the can delete the post. + */ + public function deletable($user = null) { + if ($this->no_results) + return false; + + $name = strtolower(get_class($this)); + + fallback($user, Visitor::current()); + return $user->group->can("delete_".$name); + } + + /** + * Function: editable + * Checks if the can edit the post. + */ + public function editable($user = null) { + if ($this->no_results) + return false; + + $name = strtolower(get_class($this)); + + fallback($user, Visitor::current()); + return $user->group->can("edit_".$name); + } + + /** + * Function: edit_link + * Outputs an edit link for the model, if the visitor's edit_[model]. + * + * Parameters: + * $text - The text to show for the link. + * $before - If the link can be shown, show this before it. + * $after - If the link can be shown, show this after it. + * $classes - Extra CSS classes for the link, space-delimited. + */ + public function edit_link($text = null, $before = null, $after = null, $classes = "") { + if (!$this->editable()) + return false; + + fallback($text, __("Edit")); + + $name = strtolower(get_class($this)); + + if (@Feathers::$instances[$this->feather]->disable_ajax_edit) + $classes = empty($classes) ? "no_ajax" : $classes." no_ajax" ; + + echo $before.''.$text.''.$after; + } + + /** + * Function: delete_link + * Outputs a delete link for the post, if the delete_[model]. + * + * Parameters: + * $text - The text to show for the link. + * $before - If the link can be shown, show this before it. + * $after - If the link can be shown, show this after it. + * $classes - Extra CSS classes for the link, space-delimited. + */ + public function delete_link($text = null, $before = null, $after = null, $classes = "") { + if (!$this->deletable()) + return false; + + fallback($text, __("Delete")); + + $name = strtolower(get_class($this)); + + echo $before.''.$text.''.$after; + } + } diff --git a/News/includes/class/Modules.php b/News/includes/class/Modules.php new file mode 100755 index 0000000..7ed2e52 --- /dev/null +++ b/News/includes/class/Modules.php @@ -0,0 +1,39 @@ +priorities[$name][] = array("priority" => $priority, "function" => array($this, $name)); + } + + /** + * Function: addAlias + * Allows a module to respond to a trigger with multiple functions and custom priorities. + * + * Parameters: + * $name - Name of the trigger to respond to. + * $function - Name of the class function to respond with. + * $priority - Priority of the response. + */ + protected function addAlias($name, $function, $priority = 10) { + Trigger::current()->priorities[$name][] = array("priority" => $priority, "function" => array($this, $function)); + } + } diff --git a/News/includes/class/Paginator.php b/News/includes/class/Paginator.php new file mode 100755 index 0000000..25ba399 --- /dev/null +++ b/News/includes/class/Paginator.php @@ -0,0 +1,229 @@ + items be treated as Models? + # In this case, <$array> should be in the form of array(, "ModelName") + public $model; + + # Integer: $total + # Total number of items to paginate. + public $total; + + # Integer: $page + # The current page. + public $page; + + # Integer: $pages + # Total number of pages. + public $pages; + + # Array: $result + # The result of the pagination. + # @paginated@, @paginate@, and @list@ are references to this. + public $result = array(); + + # Array: $names + # An array of the currently-used pagination URL parameters. + static $names = array(); + + /** + * Function: __construct + * Prepares an array for pagination. + * + * Parameters: + * $array - The array to paginate. + * $per_page - Number of items per page. + * $name - The name of the $_GET parameter to use for determining the current page. + * $model - If this is true, each item in $array that gets shown on the page will be + * initialized as a model of whatever is passed as the second argument to $array. + * The first argument of $array is expected to be an array of IDs. + * $page - Page number to start at. + * + * Returns: + * A paginated array of length $per_page or smaller. + */ + public function __construct($array, $per_page = 5, $name = "page", $model = null, $page = null) { + self::$names[] = $name; + + $this->array = (array) $array; + + $this->per_page = $per_page; + $this->name = $name; + $this->model = fallback($model, (count($this->array) == 2 and is_array($this->array[0]) and is_string($this->array[1]) and class_exists($this->array[1]))); + + if ($model) + list($this->array, $model_name) = $this->array; + + $this->total = count($this->array); + $this->page = oneof($page, @$_GET[$name], 1); + $this->pages = ceil($this->total / $this->per_page); + + $offset = ($this->page - 1) * $this->per_page; + + $this->result = array(); + + if ($model) { + for ($i = $offset; $i < ($offset + $this->per_page); $i++) + if (isset($this->array[$i])) + $this->result[] = new $model_name(null, array("read_from" => $this->array[$i])); + } else + $this->result = array_slice($this->array, $offset, $this->per_page); + + $shown_dates = array(); + if ($model) + foreach ($this->result as &$result) + if (isset($result->created_at)) { + $pinned = (isset($result->pinned) and $result->pinned); + $shown = in_array(when("m-d-Y", $result->created_at), $shown_dates); + + $result->first_of_day = (!$pinned and !$shown and !AJAX); + + if (!$pinned and !$shown) + $shown_dates[] = when("m-d-Y", $result->created_at); + } + + $this->paginated = $this->paginate = $this->list =& $this->result; + } + + /** + * Function: next + * Returns the next pagination sequence. + */ + public function next() { + return new self($this->array, $this->per_page, $this->name, $this->model, $this->page + 1); + } + + /** + * Function: prev + * Returns the next pagination sequence. + */ + public function prev() { + return new self($this->array, $this->per_page, $this->name, $this->model, $this->page - 1); + } + + /** + * Function: next_page + * Checks whether or not it makes sense to show the Next Page link. + */ + public function next_page() { + return ($this->page < $this->pages and $this->pages != 1 and $this->pages != 0); + } + + /** + * Function: prev_page + * Checks whether or not it makes sense to show the Previous Page link. + */ + public function prev_page() { + return ($this->page != 1 and $this->page <= $this->pages); + } + + /** + * Function: next_link + * Outputs a link to the next page. + * + * Parameters: + * $text - The text for the link. + * $class - The CSS class for the link. + * $page - Page number to link to. + */ + public function next_link($text = null, $class = "next_page", $page = null) { + if (!$this->next_page()) + return; + + fallback($text, __("Next →")); + echo ''. + $text. + ''; + } + + /** + * Function: prev_link + * Outputs a link to the previous page. + * + * Parameters: + * $text - The text for the link. + * $class - The CSS class for the link. + * $page - Page number to link to. + */ + public function prev_link($text = null, $class = "prev_page", $page = null) { + if (!$this->prev_page()) + return; + + fallback($text, __("← Previous")); + echo ''. + $text. + ''; + } + + /** + * Function: next_page_url + * Returns the URL to the next page. + * + * Parameters: + * $page - Page number to link to. + */ + public function next_page_url($page = null) { + $config = Config::current(); + + $request = "http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; + + # Only used for adding to the end of the URL and clean URLs is off. + $mark = (substr_count($request, "?")) ? "&" : "?" ; + + fallback($page, $this->page + 1); + + # No page is set, add it to the end. + if (!isset($_GET[$this->name])) + return ($config->clean_urls and !ADMIN) ? + rtrim($request, "/")."/".$this->name."/".$page : + $request.$mark.$this->name."=".$page ; + + return ($config->clean_urls and !ADMIN) ? + preg_replace("/(\/{$this->name}\/([0-9]+)|$)/", "/".$this->name."/".$page, $request, 1) : + preg_replace("/((\?|&){$this->name}=([0-9]+)|$)/", "\\2".$this->name."=".$page, $request, 1) ; + } + + /** + * Function: prev_page_url + * Returns the URL to the previous page. + * + * Parameters: + * $page - Page number to link to. + */ + public function prev_page_url($page = null) { + $config = Config::current(); + + $request = "http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']; + + # Only used for adding to the end of the URL and clean URLs is off. + $mark = (substr_count($request, "?")) ? "&" : "?" ; + + fallback($page, $this->page - 1); + + # No page is set, add it to the end. + if (!isset($_GET[$this->name])) + return ($config->clean_urls and !ADMIN) ? + rtrim($request, "/")."/".$this->name."/".$page : + $request.$mark.$this->name."=".$page ; + + return ($config->clean_urls and !ADMIN) ? + preg_replace("/(\/{$this->name}\/([0-9]+)|$)/", "/".$this->name."/".$page, $request, 1) : + preg_replace("/((\?|&){$this->name}=([0-9]+)|$)/", "\\2".$this->name."=".$page, $request, 1) ; + } + } diff --git a/News/includes/class/Query.php b/News/includes/class/Query.php new file mode 100644 index 0000000..1eae6bd --- /dev/null +++ b/News/includes/class/Query.php @@ -0,0 +1,236 @@ +. + */ + class Query { + # Variable: $query + # Holds the current query. + public $query; + + /** + * Function: __construct + * Creates a query based on the . + * + * Parameters: + * $sql - instance. + * $query - Query to execute. + * $params - An associative array of parameters used in the query. + * $throw_exceptions - Throw exceptions on error? + */ + public function __construct($sql, $query, $params = array(), $throw_exceptions = false) { + if (DEBUG) + global $time_start; + + $this->sql = $sql; + + # Don't count config setting queries. + $count = !preg_match("/^SET /", strtoupper($query)); + + if ($count) + ++$this->sql->queries; + + $this->db =& $this->sql->db; + + $this->params = $params; + $this->throw_exceptions = $throw_exceptions; + $this->queryString = $query; + + if ($count and defined('DEBUG') and DEBUG) { + $trace = debug_backtrace(); + $target = $trace[$index = 0]; + + # Getting a traceback from these files doesn't help much. + while (match(array("/SQL\.php/", "/Model\.php/", "/\/model\//"), $target["file"])) + if (isset($trace[$index + 1]["file"])) + $target = $trace[$index++]; + else + break; + + $logQuery = $query; + foreach ($params as $name => $val) + $logQuery = preg_replace("/{$name}([^a-zA-Z0-9_]|$)/", str_replace("\\", "\\\\", $this->sql->escape($val))."\\1", $logQuery); + + $this->sql->debug[] = array("number" => $this->sql->queries, + "file" => str_replace(MAIN_DIR."/", "", $target["file"]), + "line" => $target["line"], + "query" => $logQuery, + "time" => timer_stop()); + } + + switch($this->sql->method) { + case "pdo": + try { + $this->query = $this->db->prepare($query); + $result = $this->query->execute($params); + $this->query->setFetchMode(PDO::FETCH_ASSOC); + + $this->queryString = $query; + foreach ($params as $name => $val) + $this->queryString = preg_replace("/{$name}([^a-zA-Z0-9_]|$)/", + str_replace(array("\\", "\$"), + array("\\\\", "\\\$"), + $this->sql->escape($val))."\\1", + $this->queryString); + + if (!$result) + throw new PDOException; + } catch (PDOException $error) { + if (!empty($error->errorInfo[1]) and $error->errorInfo[1] == 17) + return new self($sql, $query, $params, $throw_exceptions); + + return $this->handle($error); + } + break; + case "mysqli": + foreach ($params as $name => $val) + $query = preg_replace("/{$name}([^a-zA-Z0-9_]|$)/", + str_replace(array("\\", "\$"), + array("\\\\", "\\\$"), + $this->sql->escape($val))."\\1", + $query); + + $this->queryString = $query; + + try { + if (!$this->query = $this->db->query($query)) + throw new Exception($this->db->error); + } catch (Exception $error) { + return $this->handle($error); + } + break; + case "mysql": + foreach ($params as $name => $val) + $query = preg_replace("/{$name}([^a-zA-Z0-9_]|$)/", + str_replace(array("\\", "\$"), + array("\\\\", "\\\$"), + $this->sql->escape($val))."\\1", + $query); + + $this->queryString = $query; + + try { + if (!$this->query = @mysql_query($query)) + throw new Exception(mysql_error()); + } catch (Exception $error) { + return $this->handle($error); + } + + break; + } + } + + /** + * Function: fetchColumn + * Fetches a column of the current row. + * + * Parameters: + * $column - The offset of the column to grab. Default 0. + */ + public function fetchColumn($column = 0) { + switch($this->sql->method) { + case "pdo": + return $this->query->fetchColumn($column); + case "mysqli": + $result = $this->query->fetch_array(); + return $result[$column]; + case "mysql": + $result = mysql_fetch_array($this->query); + return $result[$column]; + } + } + + /** + * Function: fetch + * Returns the current row as an array. + */ + public function fetch() { + switch($this->sql->method) { + case "pdo": + return $this->query->fetch(); + case "mysqli": + return $this->query->fetch_array(); + case "mysql": + return mysql_fetch_array($this->query); + } + } + + /** + * Function: fetchObject + * Returns the current row as an object. + */ + public function fetchObject() { + switch($this->sql->method) { + case "pdo": + return $this->query->fetchObject(); + case "mysqli": + return $this->query->fetch_object(); + case "mysql": + return mysql_fetch_object($this->query); + } + } + + /** + * Function: fetchAll + * Returns an array of every result. + */ + public function fetchAll($style = null) { + switch($this->sql->method) { + case "pdo": + return $this->query->fetchAll($style); + case "mysqli": + $results = array(); + + while ($row = $this->query->fetch_assoc()) + $results[] = $row; + + return $results; + case "mysql": + $results = array(); + + while ($row = mysql_fetch_assoc($this->query)) + $results[] = $row; + + return $results; + } + } + + /** + * Function: grab + * Grabs all of the given column out of the full result of a query. + * + * Parameters: + * $column - Name of the column to grab. + * + * Returns: + * An array of all of the values of that column in the result. + */ + public function grab($column) { + $all = $this->fetchAll(); + + $result = array(); + foreach ($all as $row) + $result[] = $row[$column]; + + return $result; + } + + /** + * Function: handle + * Handles exceptions thrown by failed queries. + */ + public function handle($error) { + $this->sql->error = $error; + + if (UPGRADING or $this->sql->silence_errors) return false; + + $message = $error->getMessage(); + + $message.= "\n\n
    ".print_r($this->queryString, true)."\n\n
    ".print_r($this->params, true)."
    \n\n
    ".$error->getTraceAsString()."
    "; + + if (XML_RPC or $this->throw_exceptions) + throw new Exception($message); + + error(__("Database Error"), $message); + } + } diff --git a/News/includes/class/QueryBuilder.php b/News/includes/class/QueryBuilder.php new file mode 100644 index 0000000..4da0315 --- /dev/null +++ b/News/includes/class/QueryBuilder.php @@ -0,0 +1,449 @@ + $val) + $params[":".str_replace(array("(", ")", "."), "_", $key)] = $val; + + return "INSERT INTO __$table\n". + self::build_insert_header($data)."\n". + "VALUES\n". + "(".implode(", ", array_keys($params)).")\n"; + } + + /** + * Function: build_update + * Creates a full update query. + * + * Parameters: + * $table - Table to update. + * $conds - Conditions to update rows by. + * $data - Data to update. + * &$params - An associative array of parameters used in the query. + * + * Returns: + * An @UPDATE@ query string. + */ + public static function build_update($table, $conds, $data, &$params = array()) { + return "UPDATE __$table\n". + "SET ".self::build_update_values($data, $params)."\n". + ($conds ? "WHERE ".self::build_where($conds, $table, $params) : ""); + } + + /** + * Function: build_delete + * Creates a full delete query. + * + * Parameters: + * $table - Table to delete from. + * $conds - Conditions to delete by. + * &$params - An associative array of parameters used in the query. + * + * Returns: + * A @DELETE@ query string. + */ + public static function build_delete($table, $conds, &$params = array()) { + return "DELETE FROM __$table\n". + ($conds ? "WHERE ".self::build_where($conds, $table, $params) : ""); + } + + /** + * Function: build_update_values + * Creates an update data part. + * + * Parameters: + * $data - Data to update. + * &$params - An associative array of parameters used in the query. + */ + public static function build_update_values($data, &$params = array()) { + $set = self::build_conditions($data, $params, null, true); + return implode(",\n ", $set); + } + + /** + * Function: build_insert_header + * Creates an insert header. + * + * Parameters: + * $data - Data to insert. + */ + public static function build_insert_header($data) { + $set = array(); + + foreach (array_keys($data) as $field) + array_push($set, self::safecol($field)); + + return "(".implode(", ", $set).")"; + } + + /** + * Function: build_limits + * Creates the LIMIT part of a query. + * + * Parameters: + * $offset - Offset of the result. + * $limit - Limit of the result. + */ + public static function build_limits($offset, $limit) { + if ($limit === null) + return ""; + + if ($offset !== null) + return "LIMIT ".$offset.", ".$limit; + + return "LIMIT ".$limit; + } + + /** + * Function: build_from + * Creates a FROM header for select queries. + * + * Parameters: + * $tables - Tables to select from. + */ + public static function build_from($tables) { + if (!is_array($tables)) + $tables = array($tables); + + foreach ($tables as &$table) + if (substr($table, 0, 2) != "__") + $table = "__".$table; + + return implode(",\n ", $tables); + } + + /** + * Function: build_count + * Creates a SELECT COUNT(1) query. + * + * Parameters: + * $tables - Tables to tablefy with. + * $conds - Conditions to select by. + * &$params - An associative array of parameters used in the query. + */ + public static function build_count($tables, $conds, &$params = array()) { + return "SELECT COUNT(1) AS count\n". + "FROM ".self::build_from($tables)."\n". + ($conds ? "WHERE ".self::build_where($conds, $tables, $params) : ""); + } + + /** + * Function: build_select_header + * Creates a SELECT fields header. + * + * Parameters: + * $fields - Columns to select. + * $tables - Tables to tablefy with. + */ + public static function build_select_header($fields, $tables = null) { + if (!is_array($fields)) + $fields = array($fields); + + $tables = (array) $tables; + + foreach ($fields as &$field) { + self::tablefy($field, $tables); + $field = self::safecol($field); + } + + return implode(",\n ", $fields); + } + + /** + * Function: build_where + * Creates a WHERE query. + */ + public static function build_where($conds, $tables = null, &$params = array()) { + $conds = (array) $conds; + $tables = (array) $tables; + + $conditions = self::build_conditions($conds, $params, $tables); + + return (empty($conditions)) ? "" : "(".implode(")\n AND (", array_filter($conditions)).")"; + } + + /** + * Function: build_group + * Creates a GROUP BY argument. + * + * Parameters: + * $order - Columns to group by. + * $tables - Tables to tablefy with. + */ + public static function build_group($by, $tables = null) { + $by = (array) $by; + $tables = (array) $tables; + + foreach ($by as &$column) { + self::tablefy($column, $tables); + $column = self::safecol($column); + } + + return implode(",\n ", array_unique(array_filter($by))); + } + + /** + * Function: build_order + * Creates an ORDER BY argument. + * + * Parameters: + * $order - Columns to order by. + * $tables - Tables to tablefy with. + */ + public static function build_order($order, $tables = null) { + $tables = (array) $tables; + + if (!is_array($order)) + $order = comma_sep($order); + + foreach ($order as &$by) { + self::tablefy($by, $tables); + $by = self::safecol($by); + } + + return implode(",\n ", $order); + } + + /** + * Function: build_list + * Returns ('one', 'two', '', 1, 0) from array("one", "two", null, true, false) + */ + public static function build_list($vals, $params = array()) { + $return = array(); + + foreach ($vals as $val) { + if (is_object($val)) # Useful catch, e.g. empty SimpleXML objects. + $val = ""; + + $return[] = (isset($params[$val])) ? $val : SQL::current()->escape($val) ; + } + + return "(".join(", ", $return).")"; + } + + /** + * Function: safecol + * Wraps a column in proper escaping if it is a SQL keyword. + * + * Doesn't check every keyword, just the common/sensible ones. + * + * ...Okay, it only does two. "order" and "group". + * + * Parameters: + * $name - Name of the column. + */ + public static function safecol($name) { + return preg_replace("/(([^a-zA-Z0-9_]|^)(order|group)([^a-zA-Z0-9_]| +$))/i", + (SQL::current()->adapter == "mysql") ? "\\2`\\3` +\\4" : '\\2"\\3"\\4', + $name); + } + + /** + * Function: build_conditions + * Builds an associative array of SQL values into PDO-esque paramized query strings. + * + * Parameters: + * $conds - Conditions. + * &$params - Parameters array to fill. + * $tables - If specified, conditions will be tablefied with these tables. + * $insert - Is this an insert/update query? + */ + public static function build_conditions($conds, &$params, $tables = null, $insert = false) { + $conditions = array(); + + foreach ($conds as $key => $val) { + if (is_int($key)) # Full expression + $cond = $val; + else { # Key => Val expression + if (is_string($val) and strlen($val) and $val[0] == ":") + $cond = self::safecol($key)." = ".$val; + else { + if (is_bool($val)) + $val = (int) $val; + + if (substr($key, -4) == " not") { # Negation + $key = self::safecol(substr($key, 0, -4)); + $param = str_replace(array("(", ")", "."), "_", $key); + if (is_array($val)) + $cond = $key." NOT IN ".self::build_list($val, $params); + elseif ($val === null) + $cond = $key." IS NOT NULL"; + else { + $cond = $key." != :".$param; + $params[":".$param] = $val; + } + } elseif (substr($key, -5) == " like" and is_array($val)) { # multiple LIKE + $key = self::safecol(substr($key, 0, -5)); + + $likes = array(); + foreach ($val as $index => $match) { + $param = str_replace(array("(", ")", "."), "_", $key)."_".$index; + $likes[] = $key." LIKE :".$param; + $params[":".$param] = $match; + } + + $cond = "(".implode(" OR ", $likes).")"; + } elseif (substr($key, -9) == " like all" and is_array($val)) { # multiple LIKE + $key = self::safecol(substr($key, 0, -9)); + + $likes = array(); + foreach ($val as $index => $match) { + $param = str_replace(array("(", ")", "."), "_", $key)."_".$index; + $likes[] = $key." LIKE :".$param; + $params[":".$param] = $match; + } + + $cond = "(".implode(" AND ", $likes).")"; + } elseif (substr($key, -9) == " not like" and is_array($val)) { # multiple NOT LIKE + $key = self::safecol(substr($key, 0, -9)); + + $likes = array(); + foreach ($val as $index => $match) { + $param = str_replace(array("(", ")", "."), "_", $key)."_".$index; + $likes[] = $key." NOT LIKE :".$param; + $params[":".$param] = $match; + } + + $cond = "(".implode(" AND ", $likes).")"; + } elseif (substr($key, -5) == " like") { # LIKE + $key = self::safecol(substr($key, 0, -5)); + $param = str_replace(array("(", ")", "."), "_", $key); + $cond = $key." LIKE :".$param; + $params[":".$param] = $val; + } elseif (substr($key, -9) == " not like") { # NOT LIKE + $key = self::safecol(substr($key, 0, -9)); + $param = str_replace(array("(", ")", "."), "_", $key); + $cond = $key." NOT LIKE :".$param; + $params[":".$param] = $val; + } elseif (substr_count($key, " ")) { # Custom operation, e.g. array("foo >" => $bar) + list($param,) = explode(" ", $key); + $param = str_replace(array("(", ")", "."), "_", $param); + $cond = self::safecol($key)." :".$param; + $params[":".$param] = $val; + } else { # Equation + if (is_array($val)) + $cond = self::safecol($key)." IN ".self::build_list($val, $params); + elseif ($val === null and $insert) + $cond = self::safecol($key)." = ''"; + elseif ($val === null) + $cond = self::safecol($key)." IS NULL"; + else { + $param = str_replace(array("(", ")", "."), "_", $key); + $cond = self::safecol($key)." = :".$param; + $params[":".$param] = $val; + } + } + } + } + + if ($tables) + self::tablefy($cond, $tables); + + $conditions[] = $cond; + } + + return $conditions; + } + + /** + * Function: tablefy + * Automatically prepends tables and table prefixes to a field if it doesn't already have them. + * + * Parameters: + * &$field - The field to "tablefy". + * $tables - An array of tables. The first one will be used for prepending. + */ + public static function tablefy(&$field, $tables) { + if (!preg_match_all("/(\(|[\s]+|^)(?!__)([a-z0-9_\.\*]+)(\)|[\s]+|$)/", $field, $matches)) + return $field = str_replace("`", "", $field); # Method for bypassing the prefixer. + + foreach ($matches[0] as $index => $full) { + $before = $matches[1][$index]; + $name = $matches[2][$index]; + $after = $matches[3][$index]; + + if (is_numeric($name)) + continue; + + # Does it not already have a table specified? + if (!substr_count($full, ".")) { + # Don't replace things that are already either prefixed or paramized. + $field = preg_replace("/([^\.:'\"_]|^)".preg_quote($full, "/")."/", + "\\1".$before."__".$tables[0].".".$name.$after, + $field, + 1); + } else { + # Okay, it does, but is the table prefixed? + if (substr($full, 0, 2) != "__") { + # Don't replace things that are already either prefixed or paramized. + $field = preg_replace("/([^\.:'\"_]|^)".preg_quote($full, "/")."/", + "\\1".$before."__".$name.$after, + $field, + 1); + } + } + } + + $field = preg_replace("/AS ([^ ]+)\./i", "AS ", $field); + } + } + + diff --git a/News/includes/class/Route.php b/News/includes/class/Route.php new file mode 100755 index 0000000..386558f --- /dev/null +++ b/News/includes/class/Route.php @@ -0,0 +1,235 @@ + call a successful route? + public $success = false; + + # Variable: $controller + # The Route's Controller. + public $controller; + + /** + * Function: __construct + * Parse the URL and to determine what to do. + * + * Parameters: + * $controller - The controller to use. + */ + private function __construct($controller) { + $this->controller = $controller; + + $config = Config::current(); + + if (substr_count($_SERVER['REQUEST_URI'], "..") > 0 ) + exit("GTFO."); + elseif (isset($_GET['action']) and preg_match("/[^(\w+)]/", $_GET['action'])) + exit("Nope!"); + + $this->action =& $_GET['action']; + + if (isset($_GET['feed'])) + $this->feed = true; + + # Parse the current URL and extract information. + $parse = parse_url($config->url); + fallback($parse["path"], "/"); + + if (isset($controller->base)) + $parse["path"] = trim($parse["path"], "/")."/".trim($controller->base, "/")."/"; + + $this->safe_path = str_replace("/", "\\/", $parse["path"]); + $this->request = $parse["path"] == "/" ? + $_SERVER['REQUEST_URI'] : + preg_replace("/{$this->safe_path}?/", "", $_SERVER['REQUEST_URI'], 1) ; + $this->arg = array_map("urldecode", explode("/", trim($this->request, "/"))); + + if (substr_count($this->arg[0], "?") > 0 and !preg_match("/\?\w+/", $this->arg[0])) + exit("No-Go!"); + + if (method_exists($controller, "parse")) + $controller->parse($this); + + Trigger::current()->call("parse_url", $this); + + $this->try[] = isset($this->action) ? + oneof($this->action, "index") : + (!substr_count($this->arg[0], "?") ? + oneof(@$this->arg[0], "index") : + "index") ; + + # Guess the action initially. + # This is only required because of the view_site permission; + # it has to know if they're viewing /login, in which case + # it should allow the page to display. + fallback($this->action, end($this->try)); + } + + /** + * Function: init + * Attempt Controller actions until one of them doesn't return false. + * + * This will also call the @[controllername]_xxxxx@ and @route_xxxxx@ triggers. + */ + public function init() { + $trigger = Trigger::current(); + + $trigger->call("route_init", $this); + + $try = $this->try; + + if (isset($this->action)) + array_unshift($try, $this->action); + + $count = 0; + foreach ($try as $key => $val) { + if (is_numeric($key)) + list($method, $args) = array($val, array()); + else + list($method, $args) = array($key, $val); + + $this->action = $method; + + $name = strtolower(str_replace("Controller", "", get_class($this->controller))); + if ($trigger->exists($name."_".$method) or $trigger->exists("route_".$method)) + $call = $trigger->call(array($name."_".$method, "route_".$method), $this->controller); + else + $call = false; + + if ($call !== true and method_exists($this->controller, $method)) + $response = call_user_func_array(array($this->controller, $method), $args); + else + $response = false; + + if ($response !== false or $call !== false) { + $this->success = true; + break; + } + + if (++$count == count($try) and isset($this->controller->fallback) and method_exists($this->controller, "display")) + call_user_func_array(array($this->controller, "display"), $this->controller->fallback); + } + + if ($this->action != "login" and $this->success) + $_SESSION['redirect_to'] = self_url(); + + $trigger->call("route_done", $this); + + return true; + } + + /** + * Function: url + * Attempts to change the specified clean URL to a dirty URL if clean URLs is disabled. + * Use this for linking to things. The applicable URL conversions are passed through the + * parse_urls trigger. + * + * Parameters: + * $url - The clean URL. + * $use_chyrp_url - Use @Config.chyrp_url@ instead of @Config.url@, when the @$url@ begins with "/"? + * + * Returns: + * A clean or dirty URL, depending on @Config.clean_urls@. + */ + public function url($url, $controller = null) { + $config = Config::current(); + + if ($url[0] == "/") + return (ADMIN ? + $config->chyrp_url.$url : + $config->url.$url); + else + $url = substr($url, -1) == "/" ? $url : $url."/" ; + + fallback($controller, $this->controller); + + $base = !empty($controller->base) ? $config->url."/".$controller->base : $config->url ; + + if ($config->clean_urls) { # If their post URL doesn't have a trailing slash, remove it from these as well. + if (substr($url, 0, 5) == "page/") # Different URL for viewing a page + $url = substr($url, 5); + + return (substr($config->post_url, -1) == "/" or $url == "search/") ? + $base."/".$url : + $base."/".rtrim($url, "/") ; + } + + $urls = fallback($controller->urls, array()); + Trigger::current()->filter($urls, "parse_urls"); + + foreach (array_diff_assoc($urls, $controller->urls) as $key => $value) + $urls[substr($key, 0, -1).preg_quote("feed/", $key[0]).$key[0]] = "/".$value."&feed"; + + $urls["|/([^/]+)/$|"] = "/?action=$1"; + + return $base.fix(preg_replace(array_keys($urls), array_values($urls), "/".$url, 1)); + } + + /** + * Function: add + * Adds a route to Chyrp. Only needed for actions that have more than one parameter. + * For example, for /tags/ you won't need to do this, but you will for /tag/tag-name/. + * + * Parameters: + * $path - The path to add. Wrap variables with parentheses, e.g. "tag/(name)/". + * $action - The action the path points to. + * + * See Also: + * + */ + public function add($path, $action) { + $config = Config::current(); + + $new_routes = $config->routes; + $new_routes[$path] = $action; + + $config->set("routes", $new_routes); + } + + /** + * Function: remove + * Removes a route added by . + * + * Parameters: + * $path - The path to remove. + * + * See Also: + * + */ + public function remove($path) { + $config = Config::current(); + + unset($config->routes[$path]); + + $config->set("routes", $config->routes); + } + + /** + * Function: current + * Returns a singleton reference to the current class. + */ + public static function & current($controller = null) { + static $instance = null; + + if (!isset($controller) and empty($instance)) + error(__("Error"), __("Route was initiated without a Controller."), debug_backtrace()); + + return $instance = (empty($instance)) ? new self($controller) : $instance ; + } + } + diff --git a/News/includes/class/SQL.php b/News/includes/class/SQL.php new file mode 100755 index 0000000..52b42f3 --- /dev/null +++ b/News/includes/class/SQL.php @@ -0,0 +1,473 @@ + + require_once INCLUDES_DIR."/class/Query.php"; + + # File: QueryBuilder + # See Also: + # + require_once INCLUDES_DIR."/class/QueryBuilder.php"; + + class SQL { + # Array: $debug + # Holds debug information for SQL queries. + public $debug = array(); + + # Integer: $queries + # Number of queries it takes to load the page. + public $queries = 0; + + # Variable: $db + # Holds the currently running database. + public $db; + + # Variable: $error + # Holds an error message from the last attempted query. + public $error = ""; + + # Boolean: $silence_errors + # Ignore errors? + public $silence_errors = false; + + /** + * Function: __construct + * The class constructor is private so there is only one connection. + * + * Parameters: + * $settings - Settings to load instead of the config. + */ + private function __construct($settings = array()) { + if (!UPGRADING and !INSTALLING and !isset(Config::current()->sql)) + error(__("Error"), __("Database configuration is not set. Please run the upgrader.")); + + $database = !UPGRADING ? + oneof(@Config::current()->sql, array()) : + Config::get("sql") ; + + if (is_array($settings)) + fallback($database, $settings); + elseif ($settings === true) + $this->silence_errors = true; + + if (!empty($database)) + foreach ($database as $setting => $value) + $this->$setting = $value; + + $this->connected = false; + + # We really don't need PDO anymore, since we have the two we supported with it hardcoded (kinda). + # Keeping this here for when/if we decide to add support for more database engines, like Postgres and MSSQL. + # if (class_exists("PDO") and (in_array("mysql", PDO::getAvailableDrivers()) or in_array("sqlite", PDO::getAvailableDrivers()))) + # return $this->method = "pdo"; + + if (isset($this->adapter)) { + if ($this->adapter == "mysql" and class_exists("MySQLi")) + $this->method = "mysqli"; + elseif ($this->adapter == "mysql" and function_exists("mysql_connect")) + $this->method = "mysql"; + elseif (class_exists("PDO") and + ($this->adapter == "sqlite" and in_array("sqlite", PDO::getAvailableDrivers()) or + $this->adapter == "pgsql" and in_array("pgsql", PDO::getAvailableDrivers()))) + $this->method = "pdo"; + } else + if (class_exists("MySQLi")) + $this->method = "mysqli"; + elseif (function_exists("mysql_connect")) + $this->method = "mysql"; + elseif (class_exists("PDO") and in_array("mysql", PDO::getAvailableDrivers())) + $this->method = "pdo"; + } + + /** + * Function: set + * Sets a variable's value. + * + * Parameters: + * $setting - The setting name. + * $value - The new value. Can be boolean, numeric, an array, a string, etc. + * $overwrite - If the setting exists and is the same value, should it be overwritten? + */ + public function set($setting, $value, $overwrite = true) { + if (isset($this->$setting) and $this->$setting == $value and !$overwrite and !UPGRADING) + return false; + + if (!UPGRADING) + $config = Config::current(); + + $database = (!UPGRADING) ? fallback($config->sql, array()) : Config::get("sql") ; + + # Add the setting + $database[$setting] = $this->$setting = $value; + + return (!UPGRADING) ? $config->set("sql", $database) : Config::set("sql", $database) ; + } + + /** + * Function: connect + * Connects to the SQL database. + * + * Parameters: + * $checking - Return a boolean of whether or not it could connect, instead of showing an error. + */ + public function connect($checking = false) { + if ($this->connected) + return true; + + if (!isset($this->database)) + self::__construct(); + + if (UPGRADING) + $checking = true; + + switch($this->method) { + case "pdo": + try { + if (empty($this->database)) + throw new PDOException("No database specified."); + + if ($this->adapter == "sqlite") { + $this->db = new PDO("sqlite:".$this->database, null, null, array(PDO::ATTR_PERSISTENT => true)); + $this->db->sqliteCreateFunction("YEAR", array($this, "year_from_datetime"), 1); + $this->db->sqliteCreateFunction("MONTH", array($this, "month_from_datetime"), 1); + $this->db->sqliteCreateFunction("DAY", array($this, "day_from_datetime"), 1); + $this->db->sqliteCreateFunction("HOUR", array($this, "hour_from_datetime"), 1); + $this->db->sqliteCreateFunction("MINUTE", array($this, "minute_from_datetime"), 1); + $this->db->sqliteCreateFunction("SECOND", array($this, "second_from_datetime"), 1); + } else + $this->db = new PDO($this->adapter.":host=".$this->host.";".((isset($this->port)) ? "port=".$this->port.";" : "")."dbname=".$this->database, + $this->username, + $this->password, + array(PDO::ATTR_PERSISTENT => true)); + + $this->db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + } catch (PDOException $error) { + $this->error = $error->getMessage(); + return ($checking) ? false : error(__("Database Error"), $this->error) ; + } + break; + case "mysqli": + $this->db = @new MySQLi($this->host, $this->username, $this->password, $this->database); + $this->error = mysqli_connect_error(); + + if (mysqli_connect_errno()) + return ($checking) ? false : error(__("Database Error"), $this->error) ; + + break; + case "mysql": + $this->db = @mysql_connect($this->host, $this->username, $this->password); + $this->error = mysql_error(); + + if (!$this->db or !@mysql_select_db($this->database)) + return ($checking) ? false : error(__("Database Error"), $this->error) ; + + break; + } + + if ($this->adapter == "mysql") + new Query($this, "SET NAMES 'utf8'"); # Note: This doesn't increase the query debug/count. + + return $this->connected = true; + } + + /** + * Function: query + * Executes a query and increases $queries>. + * If the query results in an error, it will die and show the error. + * + * Parameters: + * $query - Query to execute. + * $params - An associative array of parameters used in the query. + * $throw_exceptions - Should an exception be thrown if the query fails? + */ + public function query($query, $params = array(), $throw_exceptions = false) { + if (!$this->connected) + return false; + + # Ensure that every param in $params exists in the query. + # If it doesn't, remove it from $params. + foreach ($params as $name => $val) + if (!strpos($query, $name)) + unset($params[$name]); + + $query = str_replace("__", $this->prefix, $query); + + if ($this->adapter == "sqlite") + $query = str_ireplace(" DEFAULT CHARSET=utf8", "", str_ireplace("AUTO_INCREMENT", "AUTOINCREMENT", $query)); + + if ($this->adapter == "pgsql") + $query = str_ireplace(array("CREATE TABLE IF NOT EXISTS", + "INTEGER PRIMARY KEY AUTO_INCREMENT", + ") DEFAULT CHARSET=utf8", + "TINYINT", + "DATETIME", + "DEFAULT '0000-00-00 00:00:00'", + "LONGTEXT", + "REPLACE INTO"), + array("CREATE TABLE", + "SERIAL PRIMARY KEY", + ")", + "SMALLINT", + "TIMESTAMP", + "", + "TEXT", + "INSERT INTO"), + $query); + + $query = new Query($this, $query, $params, $throw_exceptions); + + return (!$query->query and UPGRADING) ? false : $query ; + } + + /** + * Function: count + * Performs a counting query and returns the number of matching rows. + * + * Parameters: + * $tables - An array (or string) of tables to count results on. + * $conds - An array (or string) of conditions to match. + * $params - An associative array of parameters used in the query. + * $throw_exceptions - Should exceptions be thrown on error? + */ + public function count($tables, $conds = null, $params = array(), $throw_exceptions = false) { + $query = $this->query(QueryBuilder::build_count($tables, $conds, $params), $params, $throw_exceptions); + return ($query->query) ? $query->fetchColumn() : false ; + } + + /** + * Function: select + * Performs a SELECT with given criteria and returns the query result object. + * + * Parameters: + * $tables - An array (or string) of tables to grab results from. + * $fields - Fields to select. + * $conds - An array (or string) of conditions to match. + * $order - ORDER BY statement. Can be an array. + * $params - An associative array of parameters used in the query. + * $limit - Limit for results. + * $offset - Offset for the select statement. + * $group - GROUP BY statement. Can be an array. + * $left_join - An array of additional LEFT JOINs. + * $throw_exceptions - Should exceptions be thrown on error? + */ + public function select($tables, $fields = "*", $conds = null, $order = null, $params = array(), $limit = null, $offset = null, $group = null, $left_join = array(), $throw_exceptions = false) { + return $this->query(QueryBuilder::build_select($tables, $fields, $conds, $order, $limit, $offset, $group, $left_join, $params), $params, $throw_exceptions); + } + + /** + * Function: insert + * Performs an INSERT with given data. + * + * Parameters: + * $table - Table to insert to. + * $data - An associative array of data to insert. + * $params - An associative array of parameters used in the query. + * $throw_exceptions - Should exceptions be thrown on error? + */ + public function insert($table, $data, $params = array(), $throw_exceptions = false) { + return $this->query(QueryBuilder::build_insert($table, $data, $params), $params, $throw_exceptions); + } + + /** + * Function: replace + * Performs either an INSERT or an UPDATE depending on + * whether a row exists with the specified keys matching + * their values in the data. + * + * Parameters: + * $table - Table to update or insert into. + * $keys - Columns to match on. + * $data - Data for the insert and value matches for the keys. + * $params - An associative array of parameters to be used in the query. + * $throw_exceptions - Should exceptions be thrown on error? + */ + public function replace($table, $keys, $data, $params = array(), $throw_exceptions = false) { + $match = array(); + foreach ((array) $keys as $key) + $match[$key] = $data[$key]; + + if ($this->count($table, $match, $params)) + $this->update($table, $match, $data, $params, $throw_exceptions); + else + $this->insert($table, $data, $params, $throw_exceptions); + } + + /** + * Function: update + * Performs an UDATE with given criteria and data. + * + * Parameters: + * $table - Table to update. + * $conds - Rows to update. + * $data - An associative array of data to update. + * $params - An associative array of parameters used in the query. + * $throw_exceptions - Should exceptions be thrown on error? + */ + public function update($table, $conds, $data, $params = array(), $throw_exceptions = false) { + return $this->query(QueryBuilder::build_update($table, $conds, $data, $params), $params, $throw_exceptions); + } + + /** + * Function: delete + * Performs a DELETE with given criteria. + * + * Parameters: + * $table - Table to delete from. + * $conds - Rows to delete.. + * $params - An associative array of parameters used in the query. + * $throw_exceptions - Should exceptions be thrown on error? + */ + public function delete($table, $conds, $params = array(), $throw_exceptions = false) { + return $this->query(QueryBuilder::build_delete($table, $conds, $params), $params, $throw_exceptions); + } + + /** + * Function: latest + * Returns the last inserted sequential value. + * Both function arguments are only relevant for PostgreSQL. + * + * Parameters: + * $table - Table to get the latest value from. + * $seq - Name of the sequence. + */ + public function latest($table, $seq = "id_seq") { + if (!isset($this->db)) + $this->connect(); + + switch($this->method) { + case "pdo": + return $this->db->lastInsertId($this->prefix.$table."_".$seq); + break; + case "mysqli": + return $this->db->insert_id; + break; + case "mysql": + return @mysql_insert_id(); + break; + } + } + + /** + * Function: escape + * Escapes a string, escaping things like $1 and C:\foo\bar so that they don't get borked by the preg_replace. + * + * This also handles calling the SQL connection method's "escape_string" functions. + * + * Parameters: + * $string - String to escape. + * $quotes - Auto-wrap the string in quotes (@'@)? + */ + public function escape($string, $quotes = true) { + if (!isset($this->db)) + $this->connect(); + + switch($this->method) { + case "pdo": + $string = ltrim(rtrim($this->db->quote($string), "'"), "'"); + break; + case "mysqli": + $string = $this->db->escape_string($string); + break; + case "mysql": + $string = mysql_real_escape_string($string); + break; + } + + # I don't think this ever worked how it intended. + # I've tested PDO, MySQLi, and MySQL and they all + # properly escape with this disabled, but get double + # escaped with this uncommented: + # $string = str_replace('\\', '\\\\', $string); + $string = str_replace('$', '\$', $string); + + if ($quotes and !is_numeric($string)) + $string = "'".$string."'"; + + return $string; + } + + /** + * Function: year_from_datetime + * Returns the year of a datetime. + * + * Parameters: + * $datetime - DATETIME value. + */ + public function year_from_datetime($datetime) { + return when("Y", $datetime); + } + + /** + * Function: month_from_datetime + * Returns the month of a datetime. + * + * Parameters: + * $datetime - DATETIME value. + */ + public function month_from_datetime($datetime) { + return when("m", $datetime); + } + + /** + * Function: day_from_datetime + * Returns the day of a datetime. + * + * Parameters: + * $datetime - DATETIME value. + */ + public function day_from_datetime($datetime) { + return when("d", $datetime); + } + + /** + * Function: hour_from_datetime + * Returns the hour of a datetime. + * + * Parameters: + * $datetime - DATETIME value. + */ + public function hour_from_datetime($datetime) { + return when("g", $datetime); + } + + /** + * Function: minute_from_datetime + * Returns the minute of a datetime. + * + * Parameters: + * $datetime - DATETIME value. + */ + public function minute_from_datetime($datetime) { + return when("i", $datetime); + } + + /** + * Function: second_from_datetime + * Returns the second of a datetime. + * + * Parameters: + * $datetime - DATETIME value. + */ + public function second_from_datetime($datetime) { + return when("s", $datetime); + } + + /** + * Function: current + * Returns a singleton reference to the current connection. + */ + public static function & current($settings = false) { + if ($settings) { + static $loaded_settings = null; + return $loaded_settings = new self($settings); + } else { + static $instance = null; + return $instance = (empty($instance)) ? new self() : $instance ; + } + } + } diff --git a/News/includes/class/Session.php b/News/includes/class/Session.php new file mode 100644 index 0000000..af1dc9a --- /dev/null +++ b/News/includes/class/Session.php @@ -0,0 +1,95 @@ +select("sessions", + "data", + array("id" => $id), + "id")->fetchColumn(); + + return fallback(self::$data, ""); + } + + /** + * Function: write + * Writes their session to the database, or updates it if it already exists. + * + * Parameters: + * $id - Session ID. + * $data - Data to write. + */ + static function write($id, $data) { + if (empty($data) or $data == self::$data) + return; + + $sql = SQL::current(); + + if ($sql->count("sessions", array("id" => $id))) + $sql->update("sessions", + array("id" => $id), + array("data" => $data, + "user_id" => Visitor::current()->id, + "updated_at" => datetime())); + else + $sql->insert("sessions", + array("id" => $id, + "data" => $data, + "user_id" => Visitor::current()->id, + "created_at" => datetime())); + } + + /** + * Function: destroy + * Destroys their session. + * + * Parameters: + * $id - Session ID. + */ + static function destroy($id) { + if (SQL::current()->delete("sessions", array("id" => $id))) + return true; + + return false; + } + + /** + * Function: gc + * Garbage collector. Removes sessions older than 30 days and sessions with no stored data. + */ + static function gc() { + SQL::current()->delete("sessions", + "created_at >= :thirty_days OR data = '' OR data IS NULL", + array(":thirty_days" => datetime(strtotime("+30 days")))); + return true; + } + } diff --git a/News/includes/class/Theme.php b/News/includes/class/Theme.php new file mode 100644 index 0000000..ec0f802 --- /dev/null +++ b/News/includes/class/Theme.php @@ -0,0 +1,332 @@ +, and sets up the theme l10n domain. + */ + private function __construct() { + $config = Config::current(); + + # Load the theme translator + if (file_exists(THEME_DIR."/locale/".$config->locale.".mo")) + load_translator("theme", THEME_DIR."/locale/".$config->locale.".mo"); + + # Load the theme's info into the Theme class. + foreach (YAML::load(THEME_DIR."/info.yaml") as $key => $val) + $this->$key = $val; + + $this->url = THEME_URL; + } + + /** + * Function: pages_list + * Returns a simple array of list items to be used by the theme to generate a recursive array of pages. + * + * Parameters: + * $start - Page ID or slug to start at. + * $exclude - Page ID to exclude from the list. Used in the admin area. + */ + public function pages_list($start = 0, $exclude = null) { + if (isset($this->pages_list[$start])) + return $this->pages_list[$start]; + + $this->linear_children = array(); + $this->pages_flat = array(); + $this->children = array(); + $this->end_tags_for = array(); + + if ($start and !is_numeric($start)) + $begin_page = new Page(array("url" => $start)); + + $start = ($start and !is_numeric($start)) ? $begin_page->id : $start ; + + $where = ADMIN ? array("id not" => $exclude) : array("show_in_list" => true) ; + $pages = Page::find(array("where" => $where, "order" => "list_order ASC")); + + if (empty($pages)) + return $this->pages_list[$start] = array(); + + foreach ($pages as $page) + $this->end_tags_for[$page->id] = $this->children[$page->id] = array(); + + foreach ($pages as $page) + if ($page->parent_id != 0) + $this->children[$page->parent_id][] = $page; + + foreach ($pages as $page) + if ((!$start and $page->parent_id == 0) or ($start and $page->id == $start)) + $this->recurse_pages($page); + + $array = array(); + foreach ($this->pages_flat as $page) { + $array[$page->id]["has_children"] = !empty($this->children[$page->id]); + + if ($array[$page->id]["has_children"]) + $this->end_tags_for[$this->get_last_linear_child($page->id)][] = array("
", ""); + + $array[$page->id]["end_tags"] =& $this->end_tags_for[$page->id]; + $array[$page->id]["page"] = $page; + } + + return $this->pages_list[$start] = $array; + } + + /** + * Function: get_last_linear_child + * Gets the last linear child of a page. + * + * Parameters: + * $page - Page to get the last linear child of. + * $origin - Where to start. + */ + public function get_last_linear_child($page, $origin = null) { + fallback($origin, $page); + + $this->linear_children[$origin] = $page; + foreach ($this->children[$page] as $child) + $this->get_last_linear_child($child->id, $origin); + + return $this->linear_children[$origin]; + } + + /** + * Function: recurse_pages + * Prepares the pages into . + * + * Parameters: + * $page - Page to start recursion at. + */ + public function recurse_pages($page) { + $page->depth = oneof(@$page->depth, 1); + + $this->pages_flat[] = $page; + + foreach ($this->children[$page->id] as $child) { + $child->depth = $page->depth + 1; + $this->recurse_pages($child); + } + } + + /** + * Function: archive_list + * Generates an array of all of the archives, by month. + * + * Parameters: + * $limit - Amount of months to list + * $order_by - What to sort it by + * $order - "asc" or "desc" + * + * Returns: + * The array. Each entry as "month", "year", and "url" values, stored as an array. + */ + public function archives_list($limit = 0, $order_by = "created_at", $order = "desc") { + if (isset($this->archives_list["$limit,$order_by,$order"])) + return $this->archives_list["$limit,$order_by,$order"]; + + $sql = SQL::current(); + $dates = $sql->select("posts", + array("DISTINCT YEAR(created_at) AS year", + "MONTH(created_at) AS month", + "created_at AS created_at", + "COUNT(id) AS posts"), + array("status" => "public", Post::feathers()), + $order_by." ".strtoupper($order), + array(), + ($limit == 0) ? null : $limit, + null, + array("created_at")); + + $archives = array(); + $grouped = array(); + while ($date = $dates->fetchObject()) + if (isset($grouped[$date->month." ".$date->year])) + $archives[$grouped[$date->month." ".$date->year]]["count"]++; + else { + $grouped[$date->month." ".$date->year] = count($archives); + $archives[] = array("month" => $date->month, + "year" => $date->year, + "when" => $date->created_at, + "url" => url("archive/".when("Y/m/", $date->created_at)), + "count" => $date->posts); + } + + return $this->archives_list["$limit,$order_by,$order"] = $archives; + } + + /** + * Function: file_exists + * Returns whether the specified Twig file exists or not. + * + * Parameters: + * $file - The file's name + */ + public function file_exists($file) { + return file_exists(THEME_DIR."/".$file.".twig"); + } + + /** + * Function: stylesheets + * Outputs the default stylesheet links. + */ + public function stylesheets() { + $visitor = Visitor::current(); + $config = Config::current(); + $trigger = Trigger::current(); + + $stylesheets = array(); + Trigger::current()->filter($stylesheets, "stylesheets"); + + if (!empty($stylesheets)) + $stylesheets = ''; + else + $stylesheets = ""; + + if (file_exists(THEME_DIR."/style.css")) + $stylesheets = ''."\n\t\t"; + + if (!file_exists(THEME_DIR."/stylesheets/") and !file_exists(THEME_DIR."/css/")) + return $stylesheets; + + $long = (array) glob(THEME_DIR."/stylesheets/*"); + $short = (array) glob(THEME_DIR."/css/*"); + + $total = array_merge($long, $short); + foreach($total as $file) { + $path = preg_replace("/(.+)\/themes\/(.+)/", "/themes/\\2", $file); + $file = basename($file); + + if (substr_count($file, ".inc.css") or (substr($file, -4) != ".css" and substr($file, -4) != ".php")) + continue; + + if ($file == "ie.css") + $stylesheets.= ""; + + $stylesheets.= "\n\t\t"; + } + + return $stylesheets; + } + + /** + * Function: javascripts + * Outputs the default JavaScript script references. + */ + public function javascripts() { + $route = Route::current(); + + $args = ""; + foreach ($_GET as $key => $val) + if (!empty($val) and $val != $route->action) + $args.= "&".$key."=".urlencode($val); + + $config = Config::current(); + $trigger = Trigger::current(); + + $javascripts = array($config->chyrp_url."/includes/lib/gz.php?file=jquery.js", + $config->chyrp_url."/includes/lib/gz.php?file=plugins.js", + $config->chyrp_url.'/includes/javascript.php?action='.$route->action.$args); + Trigger::current()->filter($javascripts, "scripts"); + + $javascripts = ''."\n\t\t".''; + + if (file_exists(THEME_DIR."/javascripts/") or file_exists(THEME_DIR."/js/")) { + $long = (array) glob(THEME_DIR."/javascripts/*.js"); + $short = (array) glob(THEME_DIR."/js/*.js"); + + foreach(array_merge($long, $short) as $file) + if ($file and !substr_count($file, ".inc.js")) + $javascripts.= "\n\t\t".''; + + $long = (array) glob(THEME_DIR."/javascripts/*.php"); + $short = (array) glob(THEME_DIR."/js/*.php"); + foreach(array_merge($long, $short) as $file) + if ($file) + $javascripts.= "\n\t\t".''; + } + + return $javascripts; + } + + /** + * Function: feeds + * Outputs the Feed references. + */ + public function feeds() { + // Compute the URL of the per-page feed (if any): + $config = Config::current(); + $request = ($config->clean_urls) ? rtrim(Route::current()->request, "/") : fix($_SERVER['REQUEST_URI']) ; + $append = $config->clean_urls ? + "/feed" : + ((count($_GET) == 1 and Route::current()->action == "index") ? + "/?feed" : + "&feed") ; + $append.= $config->clean_urls ? + "/".urlencode($this->title) : + "&title=".urlencode($this->title) ; + + # Create basic list of links (site and page Atom feeds): + $feedurl = oneof(@$config->feed_url, url("feed")); + $pagefeedurl = $config->url.$request.$append; + $links = array(array("href" => $feedurl, "type" => "application/atom+xml", "title" => $config->name)); + if ($pagefeedurl != $feedurl) + $links[] = array("href" => $pagefeedurl, "type" => "application/atom+xml", "title" => "Current Page (if applicable)"); + + # Ask modules to pitch in by adding their own tag items to $links. + # Each item must be an array with "href" and "rel" properties (and optionally "title" and "type"): + Trigger::current()->filter($links, "links"); + + # Generate tags: + $tags = array(); + foreach ($links as $link) { + $rel = oneof(@$link["rel"], "alternate"); + $href = $link["href"]; + $type = @$link["type"]; + $title = @$link["title"]; + $tag = ''; + } + + return implode("\n\t", $tags); + } + + public function load_time() { + return timer_stop(); + } + + /** + * Function: current + * Returns a singleton reference to the current class. + */ + public static function & current() { + static $instance = null; + return $instance = (empty($instance)) ? new self() : $instance ; + } + } diff --git a/News/includes/class/Trigger.php b/News/includes/class/Trigger.php new file mode 100755 index 0000000..d383fee --- /dev/null +++ b/News/includes/class/Trigger.php @@ -0,0 +1,176 @@ + $call) { + $args = func_get_args(); + $args[0] = $call; + if ($index + 1 == count($name)) + return $this->exists($call) ? call_user_func_array(array($this, "call"), $args) : $return ; + else + $return = $this->exists($call) ? call_user_func_array(array($this, "call"), $args) : $return ; + } + } + + if (!$this->exists($name)) + return false; + + $arguments = func_get_args(); + array_shift($arguments); + + $return = null; + + $this->called[$name] = array(); + if (isset($this->priorities[$name])) { # Predefined priorities? + usort($this->priorities[$name], array($this, "cmp")); + + foreach ($this->priorities[$name] as $action) { + $return = call_user_func_array($action["function"], $arguments); + $this->called[$name][] = $action["function"]; + } + } + + foreach (Modules::$instances as $module) + if (!in_array(array($module, $name), $this->called[$name]) and is_callable(array($module, $name))) + $return = call_user_func_array(array($module, $name), $arguments); + + return $return; + } + + /** + * Function: filter + * Filters a variable through a trigger's actions. Similar to , except this is stackable and is intended to + * modify something instead of inject code. + * + * Any additional arguments passed to this function are passed to the function being called. + * + * Parameters: + * &$target - The variable to filter. + * $name - The name of the trigger. + * + * Returns: + * $target, filtered through any/all actions for the trigger $name. + */ + public function filter(&$target, $name) { + if (is_array($name)) + foreach ($name as $index => $filter) { + $args = func_get_args(); + $args[0] =& $target; + $args[1] = $filter; + if ($index + 1 == count($name)) + return $target = call_user_func_array(array($this, "filter"), $args); + else + $target = call_user_func_array(array($this, "filter"), $args); + } + + if (!$this->exists($name)) + return $target; + + $arguments = func_get_args(); + array_shift($arguments); + array_shift($arguments); + + $this->called[$name] = array(); + + if (isset($this->priorities[$name]) and usort($this->priorities[$name], array($this, "cmp"))) + foreach ($this->priorities[$name] as $action) { + $call = call_user_func_array($this->called[$name][] = $action["function"], + array_merge(array(&$target), $arguments)); + $target = fallback($call, $target); + } + + foreach (Modules::$instances as $module) + if (!in_array(array($module, $name), $this->called[$name]) and is_callable(array($module, $name))) { + $call = call_user_func_array(array($module, $name), + array_merge(array(&$target), $arguments)); + $target = fallback($call, $target); + } + + return $target; + } + + /** + * Function: remove + * Unregisters a given $action from a $trigger. + * + * Parameters: + * $trigger - The trigger to unregister from. + * $action - The action name. + */ + public function remove($trigger, $action) { + foreach ($this->actions[$trigger] as $index => $func) { + if ($func == $action) { + unset($this->actions[$trigger][$key]); + return; + } + } + $this->actions[$trigger]["disabled"][] = $action; + } + + /** + * Function: exists + * Checks if there are any actions for a given $trigger. + * + * Parameters: + * $trigger - The trigger name. + * + * Returns: + * @true@ or @false@ + */ + public function exists($name) { + if (isset($this->exists[$name])) + return $this->exists[$name]; + + foreach (Modules::$instances as $module) + if (is_callable(array($module, $name))) + return $this->exists[$name] = true; + + if (isset($this->priorities[$name])) + return $this->exists[$name] = true; + + return $this->exists[$name] = false; + } + + /** + * Function: current + * Returns a singleton reference to the current class. + */ + public static function & current() { + static $instance = null; + return $instance = (empty($instance)) ? new self() : $instance ; + } + } diff --git a/News/includes/class/Twig.php b/News/includes/class/Twig.php new file mode 100644 index 0000000..b0891a2 --- /dev/null +++ b/News/includes/class/Twig.php @@ -0,0 +1,55 @@ +getTemplate('index.html'); + * + * You can render templates by using the render and display methods. display + * works like render just that it prints the output whereas render returns + * the generated source as string. Both accept an array as context:: + * + * echo $template->render(array('users' => get_list_of_users())); + * $template->display(array('users' => get_list_of_users())); + * + * Custom Loaders + * -------------- + * + * For many applications it's a good idea to subclass the loader to add + * support for multiple template locations. For example many applications + * support plugins and you want to allow plugins to ship themes. + * + * The easiest way is subclassing Twig_Loader and override the getFilename + * method which calculates the path to the template on the file system. + * + * + * :copyright: 2008 by Armin Ronacher. + * :license: BSD. + */ + + + if (!defined('TWIG_BASE')) + define('TWIG_BASE', dirname(__FILE__) . '/Twig'); + + define('TWIG_VERSION', '0.1-dev'); + + + // the systems we load automatically on initialization. The compiler + // and other stuff is loaded on first request. + require TWIG_BASE . '/exceptions.php'; + require TWIG_BASE . '/runtime.php'; + require TWIG_BASE . '/api.php'; diff --git a/News/includes/class/Twig/api.php b/News/includes/class/Twig/api.php new file mode 100644 index 0000000..d103e70 --- /dev/null +++ b/News/includes/class/Twig/api.php @@ -0,0 +1,192 @@ +instance = $instance; + $this->charset = $charset; + $this->loader = $loader; + } + + /** + * Render the template with the given context and return it + * as string. + */ + public function render($context=NULL) + { + ob_start(); + $this->display($context); + return ob_get_clean(); + } + + /** + * Works like `render()` but prints the output. + */ + public function display($context=NULL) + { + global $twig_current_template; + $old = $twig_current_template; + $twig_current_template = $this; + if (is_null($context)) + $context = array(); + $this->instance->render($context); + $twig_current_template = $old; + } +} + +/** + * Baseclass for custom loaders. Subclasses have to provide a + * getFilename method. + */ +class Twig_BaseLoader +{ + public $cache; + public $charset; + + public function __construct($cache=NULL, $charset=NULL) + { + $this->cache = $cache; + $this->charset = $charset; + } + + public function getTemplate($name) + { + $cls = $this->requireTemplate($name); + return new Twig_Template(new $cls, $this->charset, $this); + } + + public function getCacheFilename($name) + { + return $this->cache . '/twig_' . md5($name) . '.cache'; + } + + public function requireTemplate($name) + { + $cls = '__TwigTemplate_' . md5($name); + if (!class_exists($cls)) { + if (is_null($this->cache)) { + $this->evalTemplate($name); + return $cls; + } + $fn = $this->getFilename($name); + if (!file_exists($fn)) + throw new Twig_TemplateNotFound($name); + $cache_fn = $this->getCacheFilename($name); + if (!file_exists($cache_fn) || + filemtime($cache_fn) < filemtime($fn)) { + twig_load_compiler(); + $fp = @fopen($cache_fn, 'wb'); + if (!$fp) { + $this->evalTemplate($name, $fn); + return $cls; + } + $compiler = new Twig_FileCompiler($fp); + $this->compileTemplate($name, $compiler, $fn); + fclose($fp); + } + include $cache_fn; + } + return $cls; + } + + public function compileTemplate($name, $compiler=NULL, $fn=NULL) + { + twig_load_compiler(); + if (is_null($compiler)) { + $compiler = new Twig_StringCompiler(); + $returnCode = true; + } + else + $returnCode = false; + if (is_null($fn)) + $fn = $this->getFilename($name); + + $node = twig_parse(file_get_contents($fn, $name), $name); + $node->compile($compiler); + if ($returnCode) + return $compiler->getCode(); + } + + private function evalTemplate($name, $fn=NULL) + { + $code = $this->compileTemplate($name, NULL, $fn); + # echo "ORIGINAL: "; + $code = preg_replace('/(?!echo twig_get_attribute.+)echo "[\\\\tn]+";/', "", $code); # Remove blank lines + #echo "STRIPPED: "; + eval('?>' . $code); + } +} + + +/** + * Helper class that loads templates. + */ +class Twig_Loader extends Twig_BaseLoader +{ + public $folder; + + public function __construct($folder, $cache=NULL, $charset=NULL) + { + parent::__construct($cache, $charset); + $this->folder = $folder; + } + + public function getFilename($name) + { + if ($name[0] == '/' or preg_match("/[a-zA-Z]:\\\/", $name)) return $name; + + $path = array(); + foreach (explode('/', $name) as $part) { + if ($part[0] != '.') + array_push($path, $part); + } + + return $this->folder . '/' . implode('/', $path) ; + } +} diff --git a/News/includes/class/Twig/ast.php b/News/includes/class/Twig/ast.php new file mode 100644 index 0000000..1561475 --- /dev/null +++ b/News/includes/class/Twig/ast.php @@ -0,0 +1,754 @@ +lineno = $lineno; + } + + public function compile($compiler) + { + } +} + + +class Twig_NodeList extends Twig_Node +{ + public $nodes; + + public function __construct($nodes, $lineno) + { + parent::__construct($lineno); + $this->nodes = $nodes; + } + + public function compile($compiler) + { + foreach ($this->nodes as $node) + $node->compile($compiler); + } + + public static function fromArray($array, $lineno) + { + if (count($array) == 1) + return $array[0]; + return new Twig_NodeList($array, $lineno); + } +} + + +class Twig_Module extends Twig_Node +{ + public $body; + public $extends; + public $blocks; + public $filename; + public $id; + + public function __construct($body, $extends, $blocks, $filename) + { + parent::__construct(1); + $this->body = $body; + $this->extends = $extends; + $this->blocks = $blocks; + $this->filename = $filename; + } + + public function compile($compiler) + { + $compiler->raw("extends)) { + $compiler->raw('$this->requireTemplate('); + $compiler->repr($this->extends); + $compiler->raw(");\n"); + } + $compiler->raw('class __TwigTemplate_' . md5($this->filename)); + if (!is_null($this->extends)) { + $parent = md5($this->extends); + $compiler->raw(" extends __TwigTemplate_$parent {\n"); + } + else { + $compiler->raw(" {\npublic function render(\$context) {\n"); + $this->body->compile($compiler); + $compiler->raw("}\n"); + } + + foreach ($this->blocks as $node) + $node->compile($compiler); + + $compiler->raw("}\n"); + } +} + + +class Twig_Print extends Twig_Node +{ + public $expr; + + public function __construct($expr, $lineno) + { + parent::__construct($lineno); + $this->expr = $expr; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $compiler->raw('echo '); + $this->expr->compile($compiler); + $compiler->raw(";\n"); + } +} + + +class Twig_Text extends Twig_Node +{ + public $data; + + public function __construct($data, $lineno) + { + parent::__construct($lineno); + $this->data = $data; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $compiler->raw('echo '); + $compiler->string($this->data); + $compiler->raw(";\n"); + } +} + + +class Twig_ForLoop extends Twig_Node +{ + public $is_multitarget; + public $item; + public $seq; + public $body; + public $else; + + public function __construct($is_multitarget, $item, $seq, $body, $else, + $lineno) + { + parent::__construct($lineno); + $this->is_multitarget = $is_multitarget; + $this->item = $item; + $this->seq = $seq; + $this->body = $body; + $this->else = $else; + $this->lineno = $lineno; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $compiler->pushContext(); + $compiler->raw('foreach (twig_iterate($context, '); + $this->seq->compile($compiler); + $compiler->raw(") as \$iterator) {\n"); + if ($this->is_multitarget) { + $compiler->raw('twig_set_loop_context_multitarget($context, ' . + '$iterator, array('); + $idx = 0; + foreach ($this->item as $node) { + if ($idx++) + $compiler->raw(', '); + $compiler->repr($node->name); + } + $compiler->raw("));\n"); + } + else { + $compiler->raw('twig_set_loop_context($context, $iterator, '); + $compiler->repr($this->item->name); + $compiler->raw(");\n"); + } + $this->body->compile($compiler); + $compiler->raw("}\n"); + if (!is_null($this->else)) { + $compiler->raw("if (!\$context['loop']['iterated']) {\n"); + $this->else->compile($compiler); + $compiler->raw('}'); + } + $compiler->popContext(); + } +} + +class Twig_PaginateLoop extends Twig_Node +{ + public $item; + public $seq; + public $body; + public $else; + + public function __construct($item, $per_page, $target, + $as, $body, $else, $lineno) + { + parent::__construct($lineno); + $this->item = $item; + $this->per_page = $per_page; + $this->seq = $target; + $this->as = $as; + $this->body = $body; + $this->else = $else; + $this->lineno = $lineno; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $compiler->pushContext(); + $compiler->raw('twig_paginate($context,'); + $compiler->raw('"'.$this->as->name.'", '); + if (isset($this->seq->node) and isset($this->seq->attr)) { + $compiler->raw('array($context["::parent"]["'); + $compiler->raw($this->seq->node->name.'"],'); + $compiler->raw('"'.$this->seq->attr->value.'")'); + } else + $this->seq->compile($compiler); + $compiler->raw(', '); + $this->per_page->compile($compiler); + $compiler->raw(");\n"); + $compiler->raw('foreach (twig_iterate($context,'); + $compiler->raw(' $context["::parent"]["'.$this->as->name); + $compiler->raw("\"]->paginated) as \$iterator) {\n"); + $compiler->raw('twig_set_loop_context($context, $iterator, '); + $compiler->repr($this->item->name); + $compiler->raw(");\n"); + $this->body->compile($compiler); + $compiler->raw("}\n"); + if (!is_null($this->else)) { + $compiler->raw("if (!\$context['loop']['iterated']) {\n"); + $this->else->compile($compiler); + $compiler->raw('}'); + } + $compiler->popContext(); + } +} + + +class Twig_IfCondition extends Twig_Node +{ + public $tests; + public $else; + + public function __construct($tests, $else, $lineno) + { + parent::__construct($lineno); + $this->tests = $tests; + $this->else = $else; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $idx = 0; + foreach ($this->tests as $test) { + $compiler->raw(($idx++ ? "}\nelse" : '') . 'if ('); + $test[0]->compile($compiler); + $compiler->raw(") {\n"); + $test[1]->compile($compiler); + } + if (!is_null($this->else)) { + $compiler->raw("} else {\n"); + $this->else->compile($compiler); + } + $compiler->raw("}\n"); + } +} + + +class Twig_Block extends Twig_Node +{ + public $name; + public $body; + public $parent; + + public function __construct($name, $body, $lineno, $parent=NULL) + { + parent::__construct($lineno); + $this->name = $name; + $this->body = $body; + $this->parent = $parent; + } + + public function replace($other) + { + $this->body = $other->body; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $compiler->format('public function block_%s($context) {' . "\n", + $this->name); + if (!is_null($this->parent)) + $compiler->raw('$context[\'::superblock\'] = array($this, ' . + "'parent::block_$this->name');\n"); + $this->body->compile($compiler); + $compiler->format("}\n\n"); + } +} + + +class Twig_BlockReference extends Twig_Node +{ + public $name; + + public function __construct($name, $lineno) + { + parent::__construct($lineno); + $this->name = $name; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $compiler->format('$this->block_%s($context);' . "\n", $this->name); + } +} + + +class Twig_Super extends Twig_Node +{ + public $block_name; + + public function __construct($block_name, $lineno) + { + parent::__construct($lineno); + $this->block_name = $block_name; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $compiler->raw('parent::block_' . $this->block_name . '($context);' . "\n"); + } +} + + +class Twig_Include extends Twig_Node +{ + public $expr; + + public function __construct($expr, $lineno) + { + parent::__construct($lineno); + $this->expr = $expr; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $compiler->raw('twig_get_current_template()->loader->getTemplate('); + $this->expr->compile($compiler); + $compiler->raw(')->display($context);' . "\n"); + } +} + + +class Twig_URL extends Twig_Node +{ + public $expr; + + public function __construct($expr, $cont, $lineno) + { + parent::__construct($lineno); + $this->expr = $expr; + $this->cont = $cont; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $compiler->raw('echo url('); + $this->expr->compile($compiler); + + if (!empty($this->cont) and class_exists($this->cont->name."Controller") and is_callable(array($this->cont->name."Controller", "current"))) + $compiler->raw(", ".$this->cont->name."Controller::current()"); + + $compiler->raw(');'."\n"); + } +} + + +class Twig_AdminURL extends Twig_Node +{ + public $expr; + + public function __construct($expr, $lineno) + { + parent::__construct($lineno); + $this->expr = $expr; + } + + public function compile($compiler) + { + $compiler->addDebugInfo($this); + $compiler->raw('echo fix(Config::current()->chyrp_url."/admin/?action=".('); + $this->expr->compile($compiler); + $compiler->raw('));'."\n"); + } +} + + +class Twig_Expression extends Twig_Node +{ + +} + + +class Twig_ConditionalExpression extends Twig_Expression +{ + public $expr1; + public $expr2; + public $expr3; + + public function __construct($expr1, $expr2, $expr3, $lineno) + { + parent::__construct($lineno); + $this->expr1 = $expr1; + $this->expr2 = $expr2; + $this->expr3 = $expr3; + } + + public function compile($compiler) + { + $compiler->raw('('); + $this->expr1->compile($compiler); + $compiler->raw(') ? ('); + $this->expr2->compile($compiler); + $compiler->raw(') ; ('); + $this->expr3->compile($compiler); + $compiler->raw(')'); + } +} + + +class Twig_BinaryExpression extends Twig_Expression +{ + public $left; + public $right; + + public function __construct($left, $right, $lineno) + { + parent::__construct($lineno); + $this->left = $left; + $this->right = $right; + } + + public function compile($compiler) + { + $compiler->raw('('); + $this->left->compile($compiler); + $compiler->raw(') '); + $this->operator($compiler); + $compiler->raw(' ('); + $this->right->compile($compiler); + $compiler->raw(')'); + } +} + + +class Twig_OrExpression extends Twig_BinaryExpression +{ + public function operator($compiler) + { + return $compiler->raw('||'); + } +} + + +class Twig_AndExpression extends Twig_BinaryExpression +{ + public function operator($compiler) + { + return $compiler->raw('&&'); + } +} + + +class Twig_AddExpression extends Twig_BinaryExpression +{ + public function operator($compiler) + { + return $compiler->raw('+'); + } +} + + +class Twig_SubExpression extends Twig_BinaryExpression +{ + public function operator($compiler) + { + return $compiler->raw('-'); + } +} + + +class Twig_ConcatExpression extends Twig_BinaryExpression +{ + public function operator($compiler) + { + return $compiler->raw('.'); + } +} + + +class Twig_MulExpression extends Twig_BinaryExpression +{ + public function operator($compiler) + { + return $compiler->raw('*'); + } +} + + +class Twig_DivExpression extends Twig_BinaryExpression +{ + public function operator($compiler) + { + return $compiler->raw('/'); + } +} + + +class Twig_ModExpression extends Twig_BinaryExpression +{ + public function operator($compiler) + { + return $compiler->raw('%'); + } +} + + +class Twig_CompareExpression extends Twig_Expression +{ + public $expr; + public $ops; + + public function __construct($expr, $ops, $lineno) + { + parent::__construct($lineno); + $this->expr = $expr; + $this->ops = $ops; + } + + public function compile($compiler) + { + $this->expr->compile($compiler); + $i = 0; + foreach ($this->ops as $op) { + if ($i) + $compiler->raw(' && ($tmp' . $i); + list($op, $node) = $op; + $compiler->raw(' ' . $op . ' '); + $compiler->raw('($tmp' . ++$i . ' = '); + $node->compile($compiler); + $compiler->raw(')'); + } + if ($i > 1) + $compiler->raw(')'); + } +} + + +class Twig_UnaryExpression extends Twig_Expression +{ + public $node; + + public function __construct($node, $lineno) + { + parent::__construct($lineno); + $this->node = $node; + } + + public function compile($compiler) + { + $compiler->raw('('); + $this->operator($compiler); + $this->node->compile($compiler); + $compiler->raw(')'); + } +} + + +class Twig_NotExpression extends Twig_UnaryExpression +{ + public function operator($compiler) + { + $compiler->raw('!'); + } +} + + +class Twig_NegExpression extends Twig_UnaryExpression +{ + public function operator($compiler) + { + $compiler->raw('-'); + } +} + + +class Twig_PosExpression extends Twig_UnaryExpression +{ + public function operator($compiler) + { + $compiler->raw('+'); + } +} + + +class Twig_Constant extends Twig_Expression +{ + public $value; + + public function __construct($value, $lineno) + { + parent::__construct($lineno); + $this->value = $value; + } + + public function compile($compiler) + { + $compiler->repr($this->value); + } +} + + +class Twig_NameExpression extends Twig_Expression +{ + public $name; + + public function __construct($name, $lineno) + { + parent::__construct($lineno); + $this->name = $name; + } + + public function compile($compiler) + { + $compiler->format('(isset($context[\'%s\']) ? $context[\'%s\'] ' . + ': NULL)', $this->name, $this->name); + } +} + + +class Twig_AssignNameExpression extends Twig_NameExpression +{ + + public function compile($compiler) + { + $compiler->format('$context[\'%s\']', $this->name); + } +} + + +class Twig_GetAttrExpression extends Twig_Expression +{ + public $node; + public $attr; + + public function __construct($node, $attr, $lineno, $token_value) + { + parent::__construct($lineno); + $this->node = $node; + $this->attr = $attr; + $this->token_value = $token_value; + } + + public function compile($compiler) + { + $compiler->raw('twig_get_attribute('); + $this->node->compile($compiler); + $compiler->raw(', '); + $this->attr->compile($compiler); + if ($this->token_value == "[") # Don't look for functions if they're using foo[bar] + $compiler->raw(', false'); + $compiler->raw(')'); + } +} + + +class Twig_MethodCallExpression extends Twig_Expression +{ + public $node; + public $method; + public $arguments; + + public function __construct($node, $method, $arguments, $lineno) + { + parent::__construct($lineno); + $this->node = $node; + $this->method = $method; + $this->arguments = $arguments; + } + + public function compile($compiler) + { + $compiler->raw('call_user_func(array('); + $this->node->compile($compiler); + $compiler->raw(', '); + $this->method->compile($compiler); + $compiler->raw(')'); + foreach ($this->arguments as $argument) { + $compiler->raw(', '); + $argument->compile($compiler); + } + $compiler->raw(')'); + } +} + + +class Twig_FilterExpression extends Twig_Expression +{ + public $node; + public $filters; + + public function __construct($node, $filters, $lineno) + { + parent::__construct($lineno); + $this->node = $node; + $this->filters = $filters; + } + + public function compile($compiler) + { + global $twig_filters; + $postponed = array(); + for ($i = count($this->filters) - 1; $i >= 0; --$i) { + list($name, $attrs) = $this->filters[$i]; + if (!isset($twig_filters[$name])) { + $compiler->raw('twig_missing_filter('); + $compiler->repr($name); + $compiler->raw(', '); + } + else + $compiler->raw($twig_filters[$name] . '('); + $postponed[] = $attrs; + } + $this->node->compile($compiler); + foreach (array_reverse($postponed) as $attributes) { + foreach ($attributes as $node) { + $compiler->raw(', '); + $node->compile($compiler); + } + $compiler->raw(')'); + } + } +} diff --git a/News/includes/class/Twig/compiler.php b/News/includes/class/Twig/compiler.php new file mode 100644 index 0000000..96ffc7b --- /dev/null +++ b/News/includes/class/Twig/compiler.php @@ -0,0 +1,133 @@ +compile($compiler); + if (is_null($fp)) + return $compiler->getCode(); +} + + +class Twig_Compiler +{ + private $last_lineno; + + public function __construct() + { + $this->last_lineno = NULL; + } + + public function format() + { + $arguments = func_get_args(); + $this->raw(call_user_func_array('sprintf', $arguments)); + } + + public function string($value) + { + $this->format('"%s"', addcslashes($value, "\t\"")); + } + + public function repr($value) + { + if (is_int($value) || is_float($value)) + $this->raw($value); + else if (is_null($value)) + $this->raw('NULL'); + else if (is_bool($value)) + $this->raw($value ? 'true' : 'false'); + else if (is_array($value)) { + $this->raw('array('); + $i = 0; + foreach ($value as $key => $value) { + if ($i++) + $this->raw(', '); + $this->repr($key); + $this->raw(' => '); + $this->repr($value); + } + $this->raw(')'); + } + else + $this->string($value); + } + + public function pushContext() + { + $this->raw('$context[\'::parent\'] = $parent = $context;'. "\n"); + } + + public function popContext() + { + $this->raw('$context = $context[\'::parent\'];'. "\n"); + } + + public function addDebugInfo($node) + { + if ($node->lineno != $this->last_lineno) { + $this->last_lineno = $node->lineno; + $this->raw("/* LINE:$node->lineno */\n"); + } + } +} + + +class Twig_FileCompiler extends Twig_Compiler +{ + private $fp; + + public function __construct($fp) + { + parent::__construct(); + $this->fp = $fp; + } + + public function raw($string) + { + fwrite($this->fp, $string); + } +} + + +class Twig_StringCompiler extends Twig_Compiler +{ + private $source; + + public function __construct() + { + parent::__construct(); + $this->source = ''; + } + + public function raw($string) + { + $this->source .= $string; + } + + public function getCode() + { + return $this->source; + } +} diff --git a/News/includes/class/Twig/exceptions.php b/News/includes/class/Twig/exceptions.php new file mode 100644 index 0000000..a340523 --- /dev/null +++ b/News/includes/class/Twig/exceptions.php @@ -0,0 +1,66 @@ +lineno = $lineno; + $this->filename = $filename; + } +} + + +/** + * Thrown when Twig encounters an exception at runtime in the Twig + * core. + */ +class Twig_RuntimeError extends Twig_Exception +{ + public function __construct($message) + { + parent::__construct($message); + } +} + + +/** + * Raised if the loader is unable to find a template. + */ +class Twig_TemplateNotFound extends Twig_Exception +{ + public $name; + + public function __construct($name) + { + parent::__construct('Template not found: ' . $name); + $this->name = $name; + } +} diff --git a/News/includes/class/Twig/lexer.php b/News/includes/class/Twig/lexer.php new file mode 100644 index 0000000..18d536a --- /dev/null +++ b/News/includes/class/Twig/lexer.php @@ -0,0 +1,430 @@ +=?|[!=]=|[(){}.,%*\/+~|-]|\[|\]/A'; + + public function __construct($code, $filename=NULL) + { + $this->code = preg_replace('/(\r\n|\r|\n)/', '\n', $code); + $this->filename = $filename; + $this->cursor = 0; + $this->lineno = 1; + $this->pushedBack = array(); + $this->end = strlen($this->code); + $this->position = self::POSITION_DATA; + } + + /** + * parse the nex token and return it. + */ + public function nextToken() + { + // do we have tokens pushed back? get one + if (!empty($this->pushedBack)) + return array_shift($this->pushedBack); + // have we reached the end of the code? + if ($this->cursor >= $this->end) + return Twig_Token::EOF($this->lineno); + // otherwise dispatch to the lexing functions depending + // on our current position in the code. + switch ($this->position) { + case self::POSITION_DATA: + $tokens = $this->lexData(); break; + case self::POSITION_BLOCK: + $tokens = $this->lexBlock(); break; + case self::POSITION_VAR: + $tokens = $this->lexVar(); break; + } + + // if the return value is not an array it's a token + if (!is_array($tokens)) + return $tokens; + // empty array, call again + else if (empty($tokens)) + return $this->nextToken(); + // if we have multiple items we push them to the buffer + else if (count($tokens) > 1) { + $first = array_shift($tokens); + $this->pushedBack = $tokens; + return $first; + } + // otherwise return the first item of the array. + return $tokens[0]; + } + + private function lexData() + { + $match = NULL; + + // if no matches are left we return the rest of the template + // as simple text token + if (!preg_match('/(.*?)(\{[%#]|\$(?!\$))/A', $this->code, $match, + NULL, $this->cursor)) { + $rv = Twig_Token::Text(substr($this->code, $this->cursor), + $this->lineno); + $this->cursor = $this->end; + return $rv; + } + $this->cursor += strlen($match[0]); + + // update the lineno on the instance + $lineno = $this->lineno; + $this->lineno += substr_count($match[0], '\n'); + + // push the template text first + $text = $match[1]; + if (!empty($text)) { + $result = array(Twig_Token::Text($text, $lineno)); + $lineno += substr_count($text, '\n'); + } + else + $result = array(); + + // block start token, let's return a token for that. + if (($token = $match[2]) !== '$') { + // if our section is a comment, just return the text + if ($token[1] == '#') { + if (!preg_match('/.*?#\}/A', $this->code, $match, + NULL, $this->cursor)) + throw new Twig_SyntaxError('unclosed comment', + $this->lineno); + $this->cursor += strlen($match[0]); + $this->lineno += substr_count($match[0], '\n'); + return $result; + } + $result[] = new Twig_Token(Twig_Token::BLOCK_START_TYPE, + '', $lineno); + $this->position = self::POSITION_BLOCK; + } + + // quoted block + else if (isset($this->code[$this->cursor]) && + $this->code[$this->cursor] == '{') { + $this->cursor++; + $result[] = new Twig_Token(Twig_Token::VAR_START_TYPE, + '', $lineno); + $this->position = self::POSITION_VAR; + } + + // inline variable expressions. If there is no name next we + // fail silently. $ 42 could be common so no need to be a + // dickhead. + else if (preg_match(self::REGEX_NAME, $this->code, $match, + NULL, $this->cursor)) { + $result[] = new Twig_Token(Twig_Token::VAR_START_TYPE, + '', $lineno); + $result[] = Twig_Token::Name($match[0], $lineno); + $this->cursor += strlen($match[0]); + + // allow attribute lookup + while (isset($this->code[$this->cursor]) && + $this->code[$this->cursor] === '.') { + ++$this->cursor; + $result[] = Twig_Token::Operator('.', $this->lineno); + if (preg_match(self::REGEX_NAME, $this->code, + $match, NULL, $this->cursor)) { + $this->cursor += strlen($match[0]); + $result[] = Twig_Token::Name($match[0], + $this->lineno); + } + else if (preg_match(self::REGEX_NUMBER, $this->code, + $match, NULL, $this->cursor)) { + $this->cursor += strlen($match[0]); + $result[] = Twig_Token::Number($match[0], + $this->lineno); + } + else { + --$this->cursor; + break; + } + } + $result[] = new Twig_Token(Twig_Token::VAR_END_TYPE, + '', $lineno); + } + + return $result; + } + + private function lexBlock() + { + $match = NULL; + if (preg_match('/\s*%\}/A', $this->code, $match, NULL, $this->cursor)) { + $this->cursor += strlen($match[0]); + $lineno = $this->lineno; + $this->lineno += substr_count($match[0], '\n'); + $this->position = self::POSITION_DATA; + return new Twig_Token(Twig_Token::BLOCK_END_TYPE, '', $lineno); + } + return $this->lexExpression(); + } + + private function lexVar() + { + $match = NULL; + if (preg_match('/\s*\}/A', $this->code, $match, NULL, $this->cursor)) { + $this->cursor += strlen($match[0]); + $lineno = $this->lineno; + $this->lineno += substr_count($match[0], '\n'); + $this->position = self::POSITION_DATA; + return new Twig_Token(Twig_Token::VAR_END_TYPE, '', $lineno); + } + return $this->lexExpression(); + } + + private function lexExpression() + { + $match = NULL; + + // skip whitespace + while (preg_match('/\s+/A', $this->code, $match, NULL, + $this->cursor)) { + $this->cursor += strlen($match[0]); + $this->lineno += substr_count($match[0], '\n'); + } + + // sanity check + if ($this->cursor >= $this->end) + throw new Twig_SyntaxError('unexpected end of stream', + $this->lineno, $this->filename); + + // first parse operators + if (preg_match(self::REGEX_OPERATOR, $this->code, $match, NULL, + $this->cursor)) { + $this->cursor += strlen($match[0]); + return Twig_Token::Operator($match[0], $this->lineno); + } + + // now names + if (preg_match(self::REGEX_NAME, $this->code, $match, NULL, + $this->cursor)) { + $this->cursor += strlen($match[0]); + return Twig_Token::Name($match[0], $this->lineno); + } + + // then numbers + else if (preg_match(self::REGEX_NUMBER, $this->code, $match, + NULL, $this->cursor)) { + $this->cursor += strlen($match[0]); + $value = (float)$match[0]; + if ((int)$value === $value) + $value = (int)$value; + return Twig_Token::Number($value, $this->lineno); + } + + // and finally strings + else if (preg_match(self::REGEX_STRING, $this->code, $match, + NULL, $this->cursor)) { + $this->cursor += strlen($match[0]); + $this->lineno += substr_count($match[0], '\n'); + $value = stripcslashes(substr($match[0], 1, strlen($match[0]) - 2)); + return Twig_Token::String($value, $this->lineno); + } + + // unlexable + throw new Twig_SyntaxError("Unexpected character '" . + $this->code[$this->cursor] . "'.", + $this->lineno, $this->filename); + } +} + + +/** + * Wrapper around a lexer for simplified token access. + */ +class Twig_TokenStream +{ + private $pushed; + private $lexer; + public $filename; + public $current; + public $eof; + + public function __construct($lexer, $filename) + { + $this->pushed = array(); + $this->lexer = $lexer; + $this->filename = $filename; + $this->next(); + } + + public function push($token) + { + $this->pushed[] = $token; + } + + /** + * set the pointer to the next token and return the old one. + */ + public function next() + { + if (!empty($this->pushed)) + $token = array_shift($this->pushed); + else + $token = $this->lexer->nextToken(); + $old = $this->current; + $this->current = $token; + $this->eof = $token->type === Twig_Token::EOF_TYPE; + return $old; + } + + /** + * Look at the next token. + */ + public function look() + { + $old = $this->next(); + $new = $this->current; + $this->push($old); + $this->push($new); + return $new; + } + + /** + * Skip some tokens. + */ + public function skip($times=1) + { + for ($i = 0; $i < $times; ++$i) + $this->next(); + } + + /** + * expect a token (like $token->test()) and return it or raise + * a syntax error. + */ + public function expect($primary, $secondary=NULL) + { + $token = $this->current; + if (!$token->test($primary, $secondary)) + throw new Twig_SyntaxError('unexpected token', + $this->current->lineno); + $this->next(); + return $token; + } + + /** + * Forward that call to the current token. + */ + public function test($primary, $secondary=NULL) + { + return $this->current->test($primary, $secondary); + } +} + + +/** + * Simple struct for tokens. + */ +class Twig_Token +{ + public $type; + public $value; + public $lineno; + + const TEXT_TYPE = 0; + const EOF_TYPE = -1; + const BLOCK_START_TYPE = 1; + const VAR_START_TYPE = 2; + const BLOCK_END_TYPE = 3; + const VAR_END_TYPE = 4; + const NAME_TYPE = 5; + const NUMBER_TYPE = 6; + const STRING_TYPE = 7; + const OPERATOR_TYPE = 8; + + public function __construct($type, $value, $lineno) + { + $this->type = $type; + $this->value = $value; + $this->lineno = $lineno; + } + + /** + * Test the current token for a type. The first argument is the type + * of the token (if not given Twig_Token::NAME_NAME), the second the + * value of the token (if not given value is not checked). + * the token value can be an array if multiple checks shoudl be + * performed. + */ + public function test($type, $values=NULL) + { + if (is_null($values) && !is_int($type)) { + $values = $type; + $type = self::NAME_TYPE; + } + return ($this->type === $type) && ( + is_null($values) || + (is_array($values) && in_array($this->value, $values)) || + $this->value == $values + ); + } + + public static function Text($value, $lineno) + { + return new Twig_Token(self::TEXT_TYPE, $value, $lineno); + } + + public static function EOF($lineno) + { + return new Twig_Token(self::EOF_TYPE, '', $lineno); + } + + public static function Name($value, $lineno) + { + return new Twig_Token(self::NAME_TYPE, $value, $lineno); + } + + public static function Number($value, $lineno) + { + return new Twig_Token(self::NUMBER_TYPE, $value, $lineno); + } + + public static function String($value, $lineno) + { + return new Twig_Token(self::STRING_TYPE, $value, $lineno); + } + + public static function Operator($value, $lineno) + { + return new Twig_Token(self::OPERATOR_TYPE, $value, $lineno); + } +} diff --git a/News/includes/class/Twig/parser.php b/News/includes/class/Twig/parser.php new file mode 100644 index 0000000..b3839da --- /dev/null +++ b/News/includes/class/Twig/parser.php @@ -0,0 +1,603 @@ +parse(); +} + + +class Twig_Parser +{ + public $stream; + public $blocks; + public $extends; + public $current_block; + private $handlers; + + public function __construct($stream) + { + $this->stream = $stream; + $this->extends = NULL; + $this->blocks = array(); + $this->current_block = NULL; + $this->handlers = array( + 'for' => array($this, 'parseForLoop'), + 'if' => array($this, 'parseIfCondition'), + 'extends' => array($this, 'parseExtends'), + 'include' => array($this, 'parseInclude'), + 'block' => array($this, 'parseBlock'), + 'super' => array($this, 'parseSuper'), + + # Chyrp specific extensions + 'url' => array($this, 'parseURL'), + 'admin' => array($this, 'parseAdminURL'), + 'paginate' => array($this, 'parsePaginate') + ); + } + + public function parseForLoop($token) + { + $lineno = $token->lineno; + list($is_multitarget, $item) = $this->parseAssignmentExpression(); + $this->stream->expect('in'); + $seq = $this->parseExpression(); + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->subparse(array($this, 'decideForFork')); + if ($this->stream->next()->value == 'else') { + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + $else = $this->subparse(array($this, 'decideForEnd'), true); + } + else + $else = NULL; + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + return new Twig_ForLoop($is_multitarget, $item, $seq, $body, $else, + $lineno); + } + + public function parsePaginate($token) + { + $lineno = $token->lineno; + + $per_page = $this->parseExpression(); + $as = $this->parseExpression(); + $this->stream->expect('in'); + $loop = $this->parseExpression(); + $this->stream->expect('as'); + $item = $this->parseExpression(); + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->subparse(array($this, 'decidePaginateFork')); + if ($this->stream->next()->value == 'else') { + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + $else = $this->subparse(array($this, 'decidePaginateEnd'), true); + } + else + $else = NULL; + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + return new Twig_PaginateLoop($item, $per_page, + $loop, $as, $body, $else, $lineno); + } + + public function decideForFork($token) + { + return $token->test(array('else', 'endfor')); + } + + public function decideForEnd($token) + { + return $token->test('endfor'); + } + + public function decidePaginateFork($token) + { + return $token->test(array('else', 'endpaginate')); + } + + public function decidePaginateEnd($token) + { + return $token->test('endpaginate'); + } + + public function parseIfCondition($token) + { + $lineno = $token->lineno; + $expr = $this->parseExpression(); + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->subparse(array($this, 'decideIfFork')); + $tests = array(array($expr, $body)); + $else = NULL; + + $end = false; + while (!$end) + switch ($this->stream->next()->value) { + case 'else': + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + $else = $this->subparse(array($this, 'decideIfEnd')); + break; + case 'elseif': + $expr = $this->parseExpression(); + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->subparse(array($this, 'decideIfFork')); + $tests[] = array($expr, $body); + break; + case 'endif': + $end = true; + break; + } + + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + return new Twig_IfCondition($tests, $else, $lineno); + } + + public function decideIfFork($token) + { + return $token->test(array('elseif', 'else', 'endif')); + } + + public function decideIfEnd($token) + { + return $token->test(array('endif')); + } + + public function parseBlock($token) + { + $lineno = $token->lineno; + $name = $this->stream->expect(Twig_Token::NAME_TYPE)->value; + if (isset($this->blocks[$name])) + throw new Twig_SyntaxError("block '$name' defined twice.", + $lineno); + $this->current_block = $name; + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + $body = $this->subparse(array($this, 'decideBlockEnd'), true); + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + $block = new Twig_Block($name, $body, $lineno); + $this->blocks[$name] = $block; + $this->current_block = NULL; + return new Twig_BlockReference($name, $lineno); + } + + public function decideBlockEnd($token) + { + return $token->test('endblock'); + } + + public function parseExtends($token) + { + $lineno = $token->lineno; + if (!is_null($this->extends)) + throw new Twig_SyntaxError('multiple extend tags', $lineno); + $this->extends = $this->stream->expect(Twig_Token::STRING_TYPE)->value; + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + return NULL; + } + + public function parseInclude($token) + { + $expr = $this->parseExpression(); + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + return new Twig_Include($expr, $token->lineno); + } + + public function parseSuper($token) + { + if (is_null($this->current_block)) + throw new Twig_SyntaxError('super outside block', $token->lineno); + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + return new Twig_Super($this->current_block, $token->lineno); + } + + public function parseURL($token) + { + $expr = $this->parseExpression(); + + if ($this->stream->test("in")) { + $this->parseExpression(); + $cont = $this->parseExpression(); + } else + $cont = null; + + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + + return new Twig_URL($expr, $cont, $token->lineno); + } + + public function parseAdminURL($token) + { + $expr = $this->parseExpression(); + $this->stream->expect(Twig_Token::BLOCK_END_TYPE); + return new Twig_AdminURL($expr, $token->lineno); + } + + public function parseExpression() + { + return $this->parseConditionalExpression(); + } + + public function parseConditionalExpression() + { + $lineno = $this->stream->current->lineno; + $expr1 = $this->parseOrExpression(); + while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '?')) { + $this->stream->next(); + $expr2 = $this->parseOrExpression(); + $this->stream->expect(Twig_Token::OPERATOR_TYPE, ':'); + $expr3 = $this->parseConditionalExpression(); + $expr1 = new Twig_ConditionalExpression($expr1, $expr2, $expr3, + $this->lineno); + $lineno = $this->stream->current->lineno; + } + return $expr1; + } + + public function parseOrExpression() + { + $lineno = $this->stream->current->lineno; + $left = $this->parseAndExpression(); + while ($this->stream->test('or')) { + $this->stream->next(); + $right = $this->parseAndExpression(); + $left = new Twig_OrExpression($left, $right, $lineno); + $lineno = $this->stream->current->lineno; + } + return $left; + } + + public function parseAndExpression() + { + $lineno = $this->stream->current->lineno; + $left = $this->parseCompareExpression(); + while ($this->stream->test('and')) { + $this->stream->next(); + $right = $this->parseCompareExpression(); + $left = new Twig_AndExpression($left, $right, $lineno); + $lineno = $this->stream->current->lineno; + } + return $left; + } + + public function parseCompareExpression() + { + static $operators = array('==', '!=', '<', '>', '>=', '<='); + $lineno = $this->stream->current->lineno; + $expr = $this->parseAddExpression(); + $ops = array(); + while ($this->stream->test(Twig_Token::OPERATOR_TYPE, $operators)) + $ops[] = array($this->stream->next()->value, + $this->parseAddExpression()); + + if (empty($ops)) + return $expr; + return new Twig_CompareExpression($expr, $ops, $lineno); + } + + public function parseAddExpression() + { + $lineno = $this->stream->current->lineno; + $left = $this->parseSubExpression(); + while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '+')) { + $this->stream->next(); + $right = $this->parseSubExpression(); + $left = new Twig_AddExpression($left, $right, $lineno); + $lineno = $this->stream->current->lineno; + } + return $left; + } + + public function parseSubExpression() + { + $lineno = $this->stream->current->lineno; + $left = $this->parseConcatExpression(); + while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '-')) { + $this->stream->next(); + $right = $this->parseConcatExpression(); + $left = new Twig_SubExpression($left, $right, $lineno); + $lineno = $this->stream->current->lineno; + } + return $left; + } + + public function parseConcatExpression() + { + $lineno = $this->stream->current->lineno; + $left = $this->parseMulExpression(); + while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '~')) { + $this->stream->next(); + $right = $this->parseMulExpression(); + $left = new Twig_ConcatExpression($left, $right, $lineno); + $lineno = $this->stream->current->lineno; + } + return $left; + } + + public function parseMulExpression() + { + $lineno = $this->stream->current->lineno; + $left = $this->parseDivExpression(); + while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '*')) { + $this->stream->next(); + $right = $this->parseDivExpression(); + $left = new Twig_MulExpression($left, $right, $lineno); + $lineno = $this->stream->current->lineno; + } + return $left; + } + + public function parseDivExpression() + { + $lineno = $this->stream->current->lineno; + $left = $this->parseModExpression(); + while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '/')) { + $this->stream->next(); + $right = $this->parseModExpression(); + $left = new Twig_DivExpression($left, $right, $lineno); + $lineno = $this->stream->current->lineno; + } + return $left; + } + + public function parseModExpression() + { + $lineno = $this->stream->current->lineno; + $left = $this->parseUnaryExpression(); + while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '%')) { + $this->stream->next(); + $right = $this->parseUnaryExpression(); + $left = new Twig_ModExpression($left, $right, $lineno); + $lineno = $this->stream->current->lineno; + } + return $left; + } + + public function parseUnaryExpression() + { + if ($this->stream->test('not')) + return $this->parseNotExpression(); + if ($this->stream->current->type == Twig_Token::OPERATOR_TYPE) { + switch ($this->stream->current->value) { + case '-': + return $this->parseNegExpression(); + case '+': + return $this->parsePosExpression(); + } + } + return $this->parsePrimaryExpression(); + } + + public function parseNotExpression() + { + $token = $this->stream->next(); + $node = $this->parseUnaryExpression(); + return new Twig_NotExpression($node, $token->lineno); + } + + public function parseNegExpression() + { + $token = $this->stream->next(); + $node = $this->parseUnaryExpression(); + return new Twig_NegExpression($node, $token->lineno); + } + + public function parsePosExpression() + { + $token = $this->stream->next(); + $node = $this->parseUnaryExpression(); + return new Twig_PosExpression($node, $token->lineno); + } + + public function parsePrimaryExpression($assignment=false) + { + $token = $this->stream->current; + switch ($token->type) { + case Twig_Token::NAME_TYPE: + $this->stream->next(); + switch ($token->value) { + case 'true': + $node = new Twig_Constant(true, $token->lineno); + break; + case 'false': + $node = new Twig_Constant(false, $token->lineno); + break; + case 'none': + $node = new Twig_Constant(NULL, $token->lineno); + break; + default: + $cls = $assignment ? 'Twig_AssignNameExpression' + : 'Twig_NameExpression'; + $node = new $cls($token->value, $token->lineno); + } + break; + case Twig_Token::NUMBER_TYPE: + case Twig_Token::STRING_TYPE: + $this->stream->next(); + $node = new Twig_Constant($token->value, $token->lineno); + break; + default: + if ($token->test(Twig_Token::OPERATOR_TYPE, '(')) { + $this->stream->next(); + $node = $this->parseExpression(); + $this->stream->expect(Twig_Token::OPERATOR_TYPE, ')'); + } + else + throw new Twig_SyntaxError('unexpected token', + $token->lineno); + } + if (!$assignment) + $node = $this->parsePostfixExpression($node); + return $node; + } + + public function parsePostfixExpression($node) + { + $stop = false; + while (!$stop && $this->stream->current->type == + Twig_Token::OPERATOR_TYPE) + switch ($this->stream->current->value) { + case '.': + case '[': + $node = $this->parseSubscriptExpression($node); + break; + case '|': + $node = $this->parseFilterExpression($node); + break; + default: + $stop = true; + break; + } + return $node; + } + + public function parseSubscriptExpression($node) + { + $token = $this->stream->next(); + $lineno = $token->lineno; + if ($token->value == '.') { + $token = $this->stream->next(); + if ($token->type == Twig_Token::NAME_TYPE || + $token->type == Twig_Token::NUMBER_TYPE) + $arg = new Twig_Constant($token->value, $lineno); + else + throw new Twig_SyntaxError('expected name or number', + $lineno); + } + else { + $arg = $this->parseExpression(); + $this->stream->expect(Twig_Token::OPERATOR_TYPE, ']'); + } + + if (!$this->stream->test(Twig_Token::OPERATOR_TYPE, '(')) + return new Twig_GetAttrExpression($node, $arg, $lineno, $token->value); + + /* sounds like something wants to call a member with some + arguments. Let's parse the parameters */ + $this->stream->next(); + $arguments = array(); + while (!$this->stream->test(Twig_Token::OPERATOR_TYPE, ')')) { + if (count($arguments)) + $this->stream->expect(Twig_Token::OPERATOR_TYPE, ','); + $arguments[] = $this->parseExpression(); + } + $this->stream->expect(Twig_Token::OPERATOR_TYPE, ')'); + return new Twig_MethodCallExpression($node, $arg, $arguments, $lineno); + } + + public function parseFilterExpression($node) + { + $lineno = $this->stream->current->lineno; + $filters = array(); + while ($this->stream->test(Twig_Token::OPERATOR_TYPE, '|')) { + $this->stream->next(); + $token = $this->stream->expect(Twig_Token::NAME_TYPE); + $args = array(); + if ($this->stream->test( + Twig_Token::OPERATOR_TYPE, '(')) { + $this->stream->next(); + while (!$this->stream->test( + Twig_Token::OPERATOR_TYPE, ')')) { + if (!empty($args)) + $this->stream->expect( + Twig_Token::OPERATOR_TYPE, ','); + $args[] = $this->parseExpression(); + } + $this->stream->expect(Twig_Token::OPERATOR_TYPE, ')'); + } + $filters[] = array($token->value, $args); + } + return new Twig_FilterExpression($node, $filters, $lineno); + } + + public function parseAssignmentExpression() + { + $lineno = $this->stream->current->lineno; + $targets = array(); + $is_multitarget = false; + while (true) { + if (!empty($targets)) + $this->stream->expect(Twig_Token::OPERATOR_TYPE, ','); + if ($this->stream->test(Twig_Token::OPERATOR_TYPE, ')') || + $this->stream->test(Twig_Token::VAR_END_TYPE) || + $this->stream->test(Twig_Token::BLOCK_END_TYPE) || + $this->stream->test('in')) + break; + $targets[] = $this->parsePrimaryExpression(true); + if (!$this->stream->test(Twig_Token::OPERATOR_TYPE, ',')) + break; + $is_multitarget = true; + } + if (!$is_multitarget && count($targets) == 1) + return array(false, $targets[0]); + return array(true, $targets); + } + + public function subparse($test, $drop_needle=false) + { + $lineno = $this->stream->current->lineno; + $rv = array(); + while (!$this->stream->eof) { + switch ($this->stream->current->type) { + case Twig_Token::TEXT_TYPE: + $token = $this->stream->next(); + $rv[] = new Twig_Text($token->value, $token->lineno); + break; + case Twig_Token::VAR_START_TYPE: + $token = $this->stream->next(); + $expr = $this->parseExpression(); + $this->stream->expect(Twig_Token::VAR_END_TYPE); + $rv[] = new Twig_Print($expr, $token->lineno); + break; + case Twig_Token::BLOCK_START_TYPE: + $this->stream->next(); + $token = $this->stream->current; + if ($token->type !== Twig_Token::NAME_TYPE) + throw new Twig_SyntaxError('expected directive', + $token->lineno); + if (!is_null($test) && call_user_func($test, $token)) { + if ($drop_needle) + $this->stream->next(); + return Twig_NodeList::fromArray($rv, $lineno); + } + if (!isset($this->handlers[$token->value])) + throw new Twig_SyntaxError('unknown directive', + $token->lineno); + $this->stream->next(); + $node = call_user_func($this->handlers[$token->value], + $token); + if (!is_null($node)) + $rv[] = $node; + break; + default: + assert(false, 'Lexer or parser ended up in ' . + 'unsupported state.'); + } + } + + return Twig_NodeList::fromArray($rv, $lineno); + } + + public function parse() + { + try { + $body = $this->subparse(NULL); + } + catch (Twig_SyntaxError $e) { + if (is_null($e->filename)) + $e->filename = $this->stream->filename; + throw $e; + } + if (!is_null($this->extends)) + foreach ($this->blocks as $block) + $block->parent = $this->extends; + return new Twig_Module($body, $this->extends, $this->blocks, + $this->stream->filename); + } +} diff --git a/News/includes/class/Twig/runtime.php b/News/includes/class/Twig/runtime.php new file mode 100644 index 0000000..3ee3baf --- /dev/null +++ b/News/includes/class/Twig/runtime.php @@ -0,0 +1,485 @@ + 'twig_date_format_filter', + 'strftime' => 'twig_strftime_format_filter', + 'strtotime' => 'strtotime', + 'numberformat' => 'number_format', + 'moneyformat' => 'money_format', + 'filesizeformat' => 'twig_filesize_format_filter', + 'format' => 'sprintf', + 'relative' => 'relative_time', + + // numbers + 'even' => 'twig_is_even_filter', + 'odd' => 'twig_is_odd_filter', + + // escaping and encoding + 'escape' => 'twig_escape_filter', + 'e' => 'twig_escape_filter', + 'urlencode' => 'twig_urlencode_filter', + 'quotes' => 'twig_quotes_filter', + 'slashes' => 'addslashes', + + // string filters + 'title' => 'twig_title_string_filter', + 'capitalize' => 'twig_capitalize_string_filter', + 'upper' => 'strtoupper', + 'lower' => 'strtolower', + 'strip' => 'trim', + 'rstrip' => 'rtrim', + 'lstrip' => 'ltrim', + 'translate' => 'twig_translate_string_filter', + 'translate_plural' => 'twig_translate_plural_string_filter', + 'normalize' => 'normalize', + 'truncate' => 'twig_truncate_filter', + 'excerpt' => 'twig_excerpt_filter', + 'replace' => 'twig_replace_filter', + 'match' => 'twig_match_filter', + 'contains' => 'substr_count', + 'linebreaks' => 'nl2br', + 'camelize' => 'camelize', + 'strip_tags' => 'strip_tags', + 'pluralize' => 'twig_pluralize_string_filter', + 'depluralize' => 'twig_depluralize_string_filter', + 'sanitize' => 'sanitize', + 'repeat' => 'str_repeat', + + // array helpers + 'join' => 'twig_join_filter', + 'split' => 'twig_split_filter', + 'first' => 'twig_first_filter', + 'offset' => 'twig_offset_filter', + 'last' => 'twig_last_filter', + 'reverse' => 'array_reverse', + 'length' => 'twig_length_filter', + 'count' => 'count', + 'sort' => 'twig_sort_filter', + + // iteration and runtime + 'default' => 'twig_default_filter', + 'keys' => 'array_keys', + 'items' => 'twig_get_array_items_filter', + + // debugging + 'inspect' => 'twig_inspect_filter', + + 'uploaded' => 'uploaded', + 'fallback' => 'oneof', + 'selected' => 'twig_selected_filter', + 'checked' => 'twig_checked_filter', + 'option_selected' => 'twig_option_selected_filter' +); + + +class Twig_LoopContextIterator implements Iterator +{ + public $context; + public $seq; + public $idx; + public $length; + public $parent; + + public function __construct(&$context, $seq, $parent) + { + $this->context = $context; + $this->seq = $seq; + $this->idx = 0; + $this->length = count($seq); + $this->parent = $parent; + } + + public function rewind() {} + + public function key() {} + + public function valid() + { + return $this->idx < $this->length; + } + + public function next() + { + $this->idx++; + } + + public function current() + { + return $this; + } +} + +function unretarded_array_unshift(&$arr, &$val) { + $arr = array_merge(array(&$val), $arr); +} + +/** + * This is called like an ordinary filter just with the name of the filter + * as first argument. Currently we just raise an exception here but it + * would make sense in the future to allow dynamic filter lookup for plugins + * or something like that. + */ +function twig_missing_filter($name) +{ + $args = func_get_args(); + array_shift($args); + + $text = $args[0]; + array_shift($args); + + array_unshift($args, $name); + unretarded_array_unshift($args, $text); + + $trigger = Trigger::current(); + + if ($trigger->exists($name)) + return call_user_func_array(array($trigger, "filter"), $args); + + return $text; +} + +function twig_get_attribute($obj, $item, $function = true) +{ + if (is_array($obj) && isset($obj[$item])) + return $obj[$item]; + if (!is_object($obj)) + return NULL; + if ($function and method_exists($obj, $item)) + return call_user_func(array($obj, $item)); + if (property_exists($obj, $item)) { + $tmp = get_object_vars($obj); + return $tmp[$item]; + } + $method = 'get' . ucfirst($item); + if ($function and method_exists($obj, $method)) + return call_user_func(array($obj, $method)); + if (is_object($obj)) { + @$obj->$item; # Funky way of allowing __get to activate before returning the value. + return @$obj->$item; + } + return NULL; +} + +function twig_paginate(&$context, $as, $over, $per_page) +{ + $name = (in_array("page", Paginator::$names)) ? $as."_page" : "page" ; + + if (count($over) == 2 and $over[0] instanceof Model and is_string($over[1])) + $context[$as] = $context["::parent"][$as] = new Paginator($over[0]->__getPlaceholders($over[1]), $per_page, $name); + else + $context[$as] = $context["::parent"][$as] = new Paginator($over, $per_page, $name); +} + +function twig_iterate(&$context, $seq) +{ + $parent = isset($context['loop']) ? $context['loop'] : null; + $seq = twig_make_array($seq); + $context['loop'] = array('parent' => $parent, 'iterated' => false); + return new Twig_LoopContextIterator($context, $seq, $parent); +} + +function twig_set_loop_context(&$context, $iterator, $target) +{ + $context[$target] = $iterator->seq[$iterator->idx]; + $context['loop'] = twig_make_loop_context($iterator); +} + +function twig_set_loop_context_multitarget(&$context, $iterator, $targets) +{ + $values = $iterator->seq[$iterator->idx]; + if (!is_array($values)) + $values = array($values); + $idx = 0; + foreach ($values as $value) { + if (!isset($targets[$idx])) + break; + $context[$targets[$idx++]] = $value; + } + $context['loop'] = twig_make_loop_context($iterator); +} + +function twig_make_loop_context($iterator) +{ + return array( + 'parent' => $iterator->parent, + 'length' => $iterator->length, + 'index0' => $iterator->idx, + 'index' => $iterator->idx + 1, + 'revindex0' => $iterator->length - $iterator->idx - 1, + 'revindex '=> $iterator->length - $iterator->idx, + 'first' => $iterator->idx == 0, + 'last' => $iterator->idx + 1 == $iterator->length, + 'iterated' => true + ); +} + +function twig_make_array($object) +{ + if (is_array($object)) + return array_values($object); + elseif (is_object($object)) { + $result = array(); + foreach ($object as $value) + $result[] = $value; + return $result; + } + return array(); +} + +function twig_date_format_filter($timestamp, $format='F j, Y, G:i') +{ + return when($format, $timestamp); +} + +function twig_strftime_format_filter($timestamp, $format='%x %X') +{ + return when($format, $timestamp, true); +} + +function twig_urlencode_filter($url, $raw=false) +{ + if ($raw) + return rawurlencode($url); + return urlencode($url); +} + +function twig_join_filter($value, $glue='') +{ + return implode($glue, (array) $value); +} + +function twig_default_filter($value, $default='') +{ + return is_null($value) ? $default : $value; +} + +function twig_get_array_items_filter($array) +{ + $result = array(); + foreach ($array as $key => $value) + $result[] = array($key, $value); + return $result; +} + +function twig_filesize_format_filter($value) +{ + $value = max(0, (int)$value); + $places = strlen($value); + if ($places <= 9 && $places >= 7) { + $value = number_format($value / 1048576, 1); + return "$value MB"; + } + if ($places >= 10) { + $value = number_format($value / 1073741824, 1); + return "$value GB"; + } + $value = number_format($value / 1024, 1); + return "$value KB"; +} + +function twig_is_even_filter($value) +{ + return $value % 2 == 0; +} + +function twig_is_odd_filter($value) +{ + return $value % 2 == 1; +} + +function twig_replace_filter($str, $search, $replace, $regex = false) +{ + if ($regex) + return preg_replace($search, $replace, $str); + else + return str_replace($search, $replace, $str); +} + +function twig_match_filter($str, $match) +{ + return preg_match($match, $str); +} + + +// add multibyte extensions if possible +if (function_exists('mb_get_info')) { + function twig_upper_filter($string) + { + $template = twig_get_current_template(); + if (!is_null($template->charset)) + return mb_strtoupper($string, $template->charset); + return strtoupper($string); + } + + function twig_lower_filter($string) + { + $template = twig_get_current_template(); + if (!is_null($template->charset)) + return mb_strtolower($string, $template->charset); + return strtolower($string); + } + + function twig_title_string_filter($string) + { + $template = twig_get_current_template(); + if (is_null($template->charset)) + return ucwords(strtolower($string)); + return mb_convert_case($string, MB_CASE_TITLE, $template->charset); + } + + function twig_capitalize_string_filter($string) + { + $template = twig_get_current_template(); + if (is_null($template->charset)) + return ucfirst(strtolower($string)); + return mb_strtoupper(mb_substr($string, 0, 1, $template->charset)) . + mb_strtolower(mb_substr($string, 1, null, $template->charset)); + } + + // override the builtins + $twig_filters['upper'] = 'twig_upper_filter'; + $twig_filters['lower'] = 'twig_lower_filter'; +} + +// and byte fallback +else { + function twig_title_string_filter($string) + { + return ucwords(strtolower($string)); + } + + function twig_capitalize_string_filter($string) + { + return ucfirst(strtolower($string)); + } +} + +function twig_translate_string_filter($string, $domain = "theme") { + $domain = ($domain == "theme" and ADMIN) ? "chyrp" : $domain ; + return __($string, $domain); +} + +function twig_translate_plural_string_filter($single, $plural, $number, $domain = "theme") { + $domain = ($domain == "theme" and ADMIN) ? "chyrp" : $domain ; + return _p($single, $plural, $number, $domain); +} + +function twig_inspect_filter($thing) { + if (ini_get("xdebug.var_display_max_depth") == -1) + return var_dump($thing); + else + return '
' .
+               fix(var_export($thing, true)) .
+               '
'; +} + +function twig_split_filter($string, $cut = " ") { + return explode($cut, $string); +} + +function twig_first_filter($array) { + foreach ($array as $key => &$val) + return $val; # Return the first one. + + return false; +} + +function twig_last_filter($array) { + return $array[count($array) - 1]; +} + +function twig_offset_filter($array, $offset = 0) { + return $array[$offset]; +} + +function twig_selected_filter($foo) { + $try = func_get_args(); + array_shift($try); + + $just_class = (end($try) === true); + if ($just_class) + array_pop($try); + + if (is_array($try[0])) { + foreach ($try as $index => $it) + if ($index) + $try[0][] = $it; + + $try = $try[0]; + } + + if (in_array($foo, $try)) + return ($just_class) ? " selected" : ' class="selected"' ; +} + +function twig_checked_filter($foo) { + if ($foo) + return ' checked="checked"'; +} + +function twig_option_selected_filter($foo) { + $try = func_get_args(); + array_shift($try); + + if (in_array($foo, $try)) + return ' selected="selected"'; +} + +function twig_pluralize_string_filter($string, $number = null) { + if ($number and $number == 1) + return $string; + else + return pluralize($string); +} + +function twig_depluralize_string_filter($string) { + return depluralize($string); +} + +function twig_quotes_filter($string) { + return str_replace(array('"', "'"), array('\"', "\\'"), $string); +} + +function twig_length_filter($thing) { + if (is_string($thing)) + return strlen($thing); + else + return count($thing); +} + +function twig_escape_filter($string, $quotes = true, $decode = true) { + if (!is_string($string)) # Certain post attributes might be parsed from YAML to an array, + return $string; # in which case the module provides a value. However, the attr + # is still passed to the "fallback" and "fix" filters when editing. + + $safe = fix($string, $quotes); + return $decode ? preg_replace("/&(#?[A-Za-z0-9]+);/", "&\\1;", $safe) : $safe ; +} + +function twig_truncate_filter($text, $length = 100, $ending = "...", $exact = false, $html = true) { + return truncate($text, $length, $ending, $exact, $html); +} + +function twig_excerpt_filter($text, $length = 200, $ending = "...", $exact = false, $html = true) { + $paragraphs = preg_split("/(\r?\n\r?\n|\r\r)/", $text); + if (count($paragraphs) > 1) + return $paragraphs[0]; + else + return truncate($text, $length, $ending, $exact, $html); +} + +function twig_sort_filter($array) { + asort($array); + return $array; +} diff --git a/News/includes/class/Twig/spec.html b/News/includes/class/Twig/spec.html new file mode 100644 index 0000000..db10e70 --- /dev/null +++ b/News/includes/class/Twig/spec.html @@ -0,0 +1,807 @@ + + + + + + +Twig Template Engine Specification + + + +
+

Twig Template Engine Specification

+

This specification specifies a simple cross-language template engine for at least +PHP, Python and Ruby.

+
+

Purpose

+

A language independent and simple template engine is useful for applications that +use code which is written in more than one programming language. Good Examples +are portal systems which use a blog written in Ruby, a forum software written in +PHP and a planet system written in Python.

+
+
+

Inspiration

+

Twig uses a syntax similar to the Genshi text templates which in turn were +inspired by django which also inspired Jinja (all three of them python template +engines) which inspired the Twig runtime environment.

+
+
+

Undefined Behavior

+

To simplify porting the template language to different platforms in a couple of +situations the behavior is undefined. Template authors may never take advantage +of such a situation!

+
+
+

Syntax

+

I'm too lazy to write down the syntax as BNF diagram but the following snippet +should explain the syntax elements:

+
+<!DOCTYPE HTML>
+{# This is a comment #}
+<title>{% block title %}Page Title Goes Here{% endblock %}</title>
+{% if show_navigation %}
+<nav>
+  <ul>
+  {% for item in navigation %}
+    <li><a href="${item.href|e}">$item.caption</a></li>
+  {% endfor %}
+  </ul>
+</nav>
+{% endif %}
+<article>{% block body %}{% endblock %}</article>
+
+
+

Comments and Whitespace

+

Everything between {# and #} is ignored by the lexer. Inside blocks and +variable sections the Lexer has to remove whitespace too.

+
+
+

Output Expressions

+

To output expressions two syntaxes exist. Simple variable output or full +expression output:

+
+$this.is.a.variable.output
+${ expression | goes | here }
+
+

The former is what we call a variable expression, the second a full expression. +Variable expressions must not contain whitespace, whereas a full expression +must print the output of the full wrapped expression.

+
+
+

Expressions

+

Expressions allow basic string manipulation and arithmetic calculations. It is +an infix syntax with the following operators in this precedence:

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
OperatorDescription
+Convert both arguments into a number and add them up.
-Convert both arguments into a number and substract them.
*Convert both arguments into a number and multiply them.
/Convert both arguments into a number and divide them.
%Convert both arguments into a number and calculate the rest +of the integer division.
~Convert both arguments into a string and concatenate them.
orTrue if the left or the right expression is true.
andTrue if the left and the right expression is true.
notnegate the expression
+
+

All number conversions have an undefined precision but the implementations +should try to select the best possible type. For example, if the implementation +sees an integer and a float that looks like an integer it may convert the +latter into a long and add them.

+

Use parentheses to group expressions.

+

If an object cannot be compared the implementation might raise an error or fail +silently. Template authors may never apply mathematical operators to untrusted +data. This is especially true for the php implementation where the following +outputs 42:

+
+${ "foo41" + 1 }
+
+

This is undefined behavior and will break on different implementations or +return 0 as "foo41" is not a valid number.

+
+

Types

+

The following types exist:

+
+ +++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
TypeLiteralDescription
integerd+One of the two numeric types. Which of them +is used and when is up to the implementation.
floatd+.d+Floating point values.
stringsee belowA unicode string. The PHP implementation has +to use bytestrings here and may use mb_string.
bool(true|false)Represents boolean values.
nonenoneThis type is returned on missing variables or +attributes.
+
+

String regex:

+
+(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')(?sm)
+
+
+
+

Attribute Lookup

+

There are two ways to look up attributes on objects. The dot and the +subscript syntax, both inspired by JavaScript. Basically the following +expressions do the very same:

+
+foo.name.0
+foo['name'][0]
+
+

This is useful to dynamically get attributes from objects:

+
+foo[bar]
+
+

The underlaying implementation is free to specify on it's own what an attribute +lookup means. The PHP reference implementation for example performs those +actions on foo.bar:

+
    +
  • try $foo['bar']
  • +
  • try $foo->bar()
  • +
  • try $foo->bar
  • +
  • try $foo->getBar()
  • +
+

The first match returns the object, attribute access to nonexisting attributes +returns none.

+
+
+

Filtering

+

The template language does not specify function calls, but filters can be used +to further modify variables using functions the template engine provides.

+

The following snippet shows how filters are translated to function calls:

+
+${ 42 | foo(1, 2) | bar | baz }
+    ->  baz(bar(foo(42, 1, 2)))
+
+

The following filters must be provided by the implementation:

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
NameDescription
dateFormat the timestamp using the PHP date formatting +rules. This may sound like a nonstandard way of +formatting dates but it's a way very popular among +template designers and also used by django.
strftimeFormat the timestamp using standard strftime rules.
numberformatApply number formatting on the string. This may or +may not use local specific rules.
moneyformatLike numberformat but for money.
filesizeformatTakes a number of bytes and displays it as KB/MB/GB
format
+
Applies sprintf formatting on the string::
+
${ "%s %2f" | format(string, float) }
+
+
evenIs the number even?
oddIs the number odd?
escapeApply HTML escaping on a string. This also has to +convert " to &quot; but leave `' unmodified.
eAlias for escape.
urlencodeURL encode the string. If the second parameter is +true this function should encode for path sections, +otherwise for query strings.
quotesEscape quotes (', ", etc.)
titleMake the string lowercase and upper case the first +characters of all words.
capitalizeLike title but capitalizes only the first char of +the whole string.
upperConvert the string to uppercase.
lowerConvert the string to lowercase.
stripTrim leading and trailing whitespace.
lstripTrim leading whitespace.
rstripTrim trailing whitespace.
translateTranslate the string using either the "theme" domain +or the "chyrp" domain if in Admin. (Chyrp-specific)
translate_pluralTranslate the (singular) string, or the plural string +if the number passed is not 1.
normalizeConvert all excessive whitespace (including linebreaks) +into a single space.
truncateTruncate a string, providing ellipsis, if it is longer +than the passed length. Keeps words in tact by default, +but with a second boolean parameter will be strict.
replaceReplaces the occurrence of the first argument with the +second argument in the string.
linebreaksConvert linebreaks to <br />'s.
camelizeConvert string to camelcase.
strip_tagsStrip HTML from the string.
pluralizeReturn the pluralization of a string, or if a number +is passed and it is 1, don't pluralize.
sanitizeRemove special characters from a string.
joinConcatenate the array items and join them with the +string provided (or commas by default).
splitSplit a string into an array at the given breakpoints.
firstFirst entry of an Array.
offsetEntry at Array[offset].
lastLast entry of an Array.
reverseReverse the Array items.
countCount the number of items in an array or string +characters.
lengthAlias for count.
defaultIf the value is none the first argument is returned
keysKeys of an Array.
itemsItems of an Array.
inspectDumps the variable or value.
fallbackIf the value is empty or none, return this value.
selectedIf the first argument is the same as the value, output +class="selected", or selected if the second +argument is true.
option_selectedSame as selected, but for selected="selected".
checkedSame as selected, but for checked="checked".
+
+

Additionally, if a filter is missing (say, ${ foo | bar_filter }, in Chyrp it +checks for an associated Trigger filter by that filter's name.

+
+
+
+

For Loops

+

Iteration works via for loops. Loops work a bit like their Python counterparts, +except that they don't support multilevel tuple unpacking and that they add a new +layer to the context. Thus at the end of the iteration all the modifications on +the context disappear. Additionally, inside loops you have access to a special +loop object which provides runtime information:

+
+ ++++ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableDescription
loop.indexThe current iteration of the loop (1-indexed)
loop.index0The current iteration of the loop (0-indexed)
loop.revindexThe number of iterations from the end of the +loop (1-indexed)
loop.revindex0The number of iterations from the end of the +loop (0-indexed)
loop.firstTrue if this is the first time through the loop
loop.lastTrue if this is the last time through the loop
loop.parentFor nested loops, this is the loop "above" the +current one
+
+

Additionally for loops can have an else section that is executed if no +iteration took place.

+
+

Example

+
+<ul>
+{% for user in users %}
+    <li><a href="$user.href">${ user.username | escape }</a></li>
+{% else %}
+    <li><em>No users found!</em></li>
+{% endfor %}
+</ul>
+
+
+
+

Notes on Iteration

+

Because we have to cope with PHP too, which has problematic arrays that are +neither hashmaps nor lists, we have no support for associative array iteration +at all. How do you iterate over associative arrays then? Using a filter:

+
+{% for key, value in array | items %}
+    ...
+{% endfor %}
+
+

To iterate over the keys only:

+
+{% for key in array | keys %}
+    ...
+{% endfor %}
+
+
+
+
+

If Conditions

+

If conditions work like like Ruby, PHP and Python, just that we use PHP +keywords. Also, use elseif and not else if:

+
+{% if expr1 %}
+    ...
+{% elseif expr2 %}
+    ...
+{% else %}
+    ...
+{% endif %}
+
+
+
+

Inheritance

+

Template inheritance allows you to build a base "skeleton" template that +contains all the common elements of your site and defines blocks that +child templates can override.

+

Here a small template inheritance example:

+
+<!DOCTYPE HTML>
+<html lang="en">
+  <link rel="stylesheet" href="style.css">
+  <title>{% block title %}My site{% endblock %}</title>
+  <div id="sidebar">
+    {% block sidebar %}
+     <ul>
+        <li><a href="/">Home</a></li>
+        <li><a href="/blog/">Blog</a></li>
+     </ul>
+     {% endblock %}
+  </div>
+  <div id="content">
+     {% block content %}{% endblock %}
+  </div>
+</html>
+
+

If we call that template "base.html" a "index.html" template could override +it and fill in the blocks:

+
+{% extends "base.html" %}
+{% block title %}Foo &mdash; {% super %}{% endblock %}
+{% block content %}
+    This is the content
+{% endblock %}
+
+

By using {% super %} you can render the parent's block. The template +filenames must be constant strings (we don't support dynamic inheritance +for simplicity) and are relative to the loader folder, not the current +template.

+
+
+
+ + diff --git a/News/includes/class/Twig/spec.txt b/News/includes/class/Twig/spec.txt new file mode 100644 index 0000000..878133c --- /dev/null +++ b/News/includes/class/Twig/spec.txt @@ -0,0 +1,363 @@ +================================== +Twig Template Engine Specification +================================== + + +This specification specifies a simple cross-language template engine for at least +PHP, Python and Ruby. + + +Purpose +======= + +A language independent and simple template engine is useful for applications that +use code which is written in more than one programming language. Good Examples +are portal systems which use a blog written in Ruby, a forum software written in +PHP and a planet system written in Python. + + +Inspiration +=========== + +Twig uses a syntax similar to the Genshi text templates which in turn were +inspired by django which also inspired Jinja (all three of them python template +engines) which inspired the Twig runtime environment. + + +Undefined Behavior +================== + +To simplify porting the template language to different platforms in a couple of +situations the behavior is undefined. Template authors may never take advantage +of such a situation! + + +Syntax +====== + +I'm too lazy to write down the syntax as BNF diagram but the following snippet +should explain the syntax elements:: + + + {# This is a comment #} + {% block title %}Page Title Goes Here{% endblock %} + {% if show_navigation %} + + {% endif %} +
{% block body %}{% endblock %}
+ + +Comments and Whitespace +----------------------- + +Everything between ``{#`` and ``#}`` is ignored by the lexer. Inside blocks and +variable sections the Lexer has to remove whitespace too. + + +Output Expressions +------------------ + +To output expressions two syntaxes exist. Simple variable output or full +expression output:: + + $this.is.a.variable.output + ${ expression | goes | here } + +The former is what we call a variable expression, the second a full expression. +Variable expressions must not contain whitespace, whereas a full expression +must print the output of the full wrapped expression. + + +Expressions +----------- + +Expressions allow basic string manipulation and arithmetic calculations. It is +an infix syntax with the following operators in this precedence: + + =========== ============================================================== + Operator Description + =========== ============================================================== + ``+`` Convert both arguments into a number and add them up. + ``-`` Convert both arguments into a number and substract them. + ``*`` Convert both arguments into a number and multiply them. + ``/`` Convert both arguments into a number and divide them. + ``%`` Convert both arguments into a number and calculate the rest + of the integer division. + ``~`` Convert both arguments into a string and concatenate them. + ``or`` True if the left or the right expression is true. + ``and`` True if the left and the right expression is true. + ``not`` negate the expression + =========== ============================================================== + +All number conversions have an undefined precision but the implementations +should try to select the best possible type. For example, if the implementation +sees an integer and a float that looks like an integer it may convert the +latter into a long and add them. + +Use parentheses to group expressions. + +If an object cannot be compared the implementation might raise an error or fail +silently. Template authors may never apply mathematical operators to untrusted +data. This is especially true for the php implementation where the following +outputs ``42``:: + + ${ "foo41" + 1 } + +This is undefined behavior and will break on different implementations or +return ``0`` as ``"foo41"`` is not a valid number. + +Types +~~~~~ + +The following types exist: + + =========== =============== ============================================== + Type Literal Description + =========== =============== ============================================== + ``integer`` `\d+` One of the two numeric types. Which of them + is used and when is up to the implementation. + ``float`` `\d+\.\d+` Floating point values. + ``string`` see below A unicode string. The PHP implementation has + to use bytestrings here and may use mb_string. + ``bool`` `(true|false)` Represents boolean values. + ``none`` `none` This type is returned on missing variables or + attributes. + =========== =============== ============================================== + +String regex:: + + (?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\')(?sm) + +Attribute Lookup +~~~~~~~~~~~~~~~~ + +There are two ways to look up attributes on objects. The dot and the +subscript syntax, both inspired by JavaScript. Basically the following +expressions do the very same:: + + foo.name.0 + foo['name'][0] + +This is useful to dynamically get attributes from objects:: + + foo[bar] + +The underlaying implementation is free to specify on it's own what an attribute +lookup means. The PHP reference implementation for example performs these +actions on ``foo.bar``: + +- try ``$foo['bar']`` +- try ``$foo->bar()`` (if they're using ``foo.bar`` and not ``foo[bar]``) +- try ``$foo->bar`` +- try ``$foo->getBar()`` (if they're using ``foo.bar`` and not ``foo[bar]``) + +The first match returns the object, attribute access to nonexisting attributes +returns `none`. + +Filtering +~~~~~~~~~ + +The template language does not specify function calls, but filters can be used +to further modify variables using functions the template engine provides. + +The following snippet shows how filters are translated to function calls:: + + ${ 42 | foo(1, 2) | bar | baz } + -> baz(bar(foo(42, 1, 2))) + +The following filters must be provided by the implementation: + + =================== ====================================================== + Name Description + =================== ====================================================== + `date` Format the timestamp using the PHP date formatting + rules. This may sound like a nonstandard way of + formatting dates but it's a way very popular among + template designers and also used by django. + `strftime` Format the timestamp using standard strftime rules. + `numberformat` Apply number formatting on the string. This may or + may not use local specific rules. + `moneyformat` Like `numberformat` but for money. + `filesizeformat` Takes a number of bytes and displays it as KB/MB/GB + `format` Applies `sprintf` formatting on the string:: + ${ "%s %2f" | format(string, float) } + `even` Is the number even? + `odd` Is the number odd? + `escape` Apply HTML escaping on a string. This also has to + convert `"` to `" but leave `'` unmodified. + `e` Alias for `escape`. + `urlencode` URL encode the string. If the second parameter is + true this function should encode for path sections, + otherwise for query strings. + `quotes` Escape quotes (\', \", etc.) + `title` Make the string lowercase and upper case the first + characters of all words. + `capitalize` Like `title` but capitalizes only the first char of + the whole string. + `upper` Convert the string to uppercase. + `lower` Convert the string to lowercase. + `strip` Trim leading and trailing whitespace. + `lstrip` Trim leading whitespace. + `rstrip` Trim trailing whitespace. + `translate` Translate the string using either the "theme" domain + or the "chyrp" domain if in Admin. (Chyrp-specific) + `translate_plural` Translate the (singular) string, or the plural string + if the number passed is not 1. + `normalize` Convert all excessive whitespace (including linebreaks) + into a single space. + `truncate` Truncate a string, providing ellipsis, if it is longer + than the passed length. Keeps words in tact by default, + but with a second boolean parameter will be strict. + `replace` Replaces the occurrence of the first argument with the + second argument in the string. + `linebreaks` Convert linebreaks to
's. + `camelize` Convert string to camelcase. + `strip_tags` Strip HTML from the string. + `pluralize` Return the pluralization of a string, or if a number + is passed and it is 1, don't pluralize. + `sanitize` Remove special characters from a string. + `join` Concatenate the array items and join them with the + string provided (or commas by default). + `split` Split a string into an array at the given breakpoints. + `first` First entry of an Array. + `offset` Entry at Array[offset]. + `last` Last entry of an Array. + `reverse` Reverse the Array items. + `count` Count the number of items in an array or string + characters. + `length` Alias for `count`. + `default` If the value is `none` the first argument is returned + `keys` Keys of an Array. + `items` Items of an Array. + `inspect` Dumps the variable or value. + `fallback` If the value is empty or `none`, return this value. + `selected` If the first argument is the same as the value, output + `class="selected"`, or `selected` if the second + argument is `true`. + `option_selected` Same as `selected`, but for `selected="selected"`. + `checked` Same as `selected`, but for `checked="checked"`. + =================== ====================================================== + +Additionally, if a filter is missing (say, ${ foo | bar_filter }, in Chyrp it +checks for an associated Trigger filter by that filter's name. + + +For Loops +--------- + +Iteration works via for loops. Loops work a bit like their Python counterparts, +except that they don't support multilevel tuple unpacking and that they add a new +layer to the context. Thus at the end of the iteration all the modifications on +the context disappear. Additionally, inside loops you have access to a special +`loop` object which provides runtime information: + + ====================== =================================================== + Variable Description + ====================== =================================================== + ``loop.index`` The current iteration of the loop (1-indexed) + ``loop.index0`` The current iteration of the loop (0-indexed) + ``loop.revindex`` The number of iterations from the end of the + loop (1-indexed) + ``loop.revindex0`` The number of iterations from the end of the + loop (0-indexed) + ``loop.first`` True if this is the first time through the loop + ``loop.last`` True if this is the last time through the loop + ``loop.parent`` For nested loops, this is the loop "above" the + current one + ====================== =================================================== + +Additionally for loops can have an `else` section that is executed if no +iteration took place. + +Example +~~~~~~~ + +:: + + + + +Notes on Iteration +~~~~~~~~~~~~~~~~~~ + +Because we have to cope with PHP too, which has problematic arrays that are +neither hashmaps nor lists, we have no support for associative array iteration +at all. How do you iterate over associative arrays then? Using a filter:: + + {% for key, value in array | items %} + ... + {% endfor %} + +To iterate over the keys only:: + + {% for key in array | keys %} + ... + {% endfor %} + + +If Conditions +------------- + +If conditions work like like Ruby, PHP and Python, just that we use PHP +keywords. Also, use `elseif` and not `else if`:: + + {% if expr1 %} + ... + {% elseif expr2 %} + ... + {% else %} + ... + {% endif %} + + +Inheritance +----------- + +Template inheritance allows you to build a base "skeleton" template that +contains all the common elements of your site and defines **blocks** that +child templates can override. + +Here a small template inheritance example:: + + + + + {% block title %}My site{% endblock %} + +
+ {% block content %}{% endblock %} +
+ + +If we call that template "base.html" a "index.html" template could override +it and fill in the blocks:: + + {% extends "base.html" %} + {% block title %}Foo — {% super %}{% endblock %} + {% block content %} + This is the content + {% endblock %} + +By using `{% super %}` you can render the parent's block. The template +filenames must be constant strings (we don't support dynamic inheritance +for simplicity) and are relative to the loader folder, not the current +template. diff --git a/News/includes/close.png b/News/includes/close.png new file mode 100755 index 0000000..8d42fec Binary files /dev/null and b/News/includes/close.png differ diff --git a/News/includes/common.php b/News/includes/common.php new file mode 100755 index 0000000..9ad37d9 --- /dev/null +++ b/News/includes/common.php @@ -0,0 +1,307 @@ + + require_once INCLUDES_DIR."/class/Config.php"; + + # File: SQL + # See Also: + # + require_once INCLUDES_DIR."/class/SQL.php"; + + # File: Model + # See Also: + # + require_once INCLUDES_DIR."/class/Model.php"; + + # File: User + # See Also: + # + require_once INCLUDES_DIR."/model/User.php"; + + # File: Visitor + # See Also: + # + require_once INCLUDES_DIR."/model/Visitor.php"; + + # File: Post + # See Also: + # + require_once INCLUDES_DIR."/model/Post.php"; + + # File: Page + # See Also: + # + require_once INCLUDES_DIR."/model/Page.php"; + + # File: Group + # See Also: + # + require_once INCLUDES_DIR."/model/Group.php"; + + # File: Session + # See Also: + # + require_once INCLUDES_DIR."/class/Session.php"; + + # File: Flash + # See Also: + # + require_once INCLUDES_DIR."/class/Flash.php"; + + # File: Theme + # See Also: + # + require_once INCLUDES_DIR."/class/Theme.php"; + + # File: Trigger + # See Also: + # + require_once INCLUDES_DIR."/class/Trigger.php"; + + # File: Module + # See Also: + # + require_once INCLUDES_DIR."/class/Modules.php"; + + # File: Feathers + # See Also: + # + require_once INCLUDES_DIR."/class/Feathers.php"; + + # File: Paginator + # See Also: + # + require_once INCLUDES_DIR."/class/Paginator.php"; + + # File: Twig + # Chyrp's templating engine. + require_once INCLUDES_DIR."/class/Twig.php"; + + # File: Route + # See Also: + # + require_once INCLUDES_DIR."/class/Route.php"; + + # File: Main + # See Also: + #
+ require_once INCLUDES_DIR."/controller/Main.php"; + + # File: Admin + # See Also: + # + require_once INCLUDES_DIR."/controller/Admin.php"; + + # File: Feather + # See Also: + # + require_once INCLUDES_DIR."/interface/Feather.php"; + + # Set the error handler to exit on error if this is being run from the tester. + if (TESTER) + set_error_handler("error_panicker"); + + # Redirect to the installer if there is no config. + if (!file_exists(INCLUDES_DIR."/config.yaml.php")) + redirect("install.php"); + + # Start the timer that keeps track of Chyrp's load time. + timer_start(); + + # Load the config settings. + $config = Config::current(); + + # Prepare the SQL interface. + $sql = SQL::current(); + + # Set the timezone for date(), etc. + set_timezone($config->timezone); + + # Initialize connection to SQL server. + $sql->connect(); + + # Sanitize all input depending on magic_quotes_gpc's enabled status. + sanitize_input($_GET); + sanitize_input($_POST); + sanitize_input($_COOKIE); + sanitize_input($_REQUEST); + + # Begin the session. + session(); + + # Set the locale for gettext. + set_locale($config->locale); + + # Load the translation engine. + load_translator("chyrp", INCLUDES_DIR."/locale/".$config->locale.".mo"); + + # Constant: PREVIEWING + # Is the user previewing a theme? + define('PREVIEWING', !ADMIN and !empty($_SESSION['theme'])); + + # Constant: THEME_DIR + # Absolute path to /themes/(current/previewed theme) + define('THEME_DIR', MAIN_DIR."/themes/".(PREVIEWING ? $_SESSION['theme'] : $config->theme)); + + # Constant: THEME_URL + # URL to /themes/(current/previewed theme) + define('THEME_URL', $config->chyrp_url."/themes/".(PREVIEWING ? $_SESSION['theme'] : $config->theme)); + + # Initialize the theme. + $theme = Theme::current(); + + # Load the Visitor. + $visitor = Visitor::current(); + + # Prepare the notifier. + $flash = Flash::current(); + + # Initiate the extensions. + init_extensions(); + + # Prepare the trigger class + $trigger = Trigger::current(); + + # Filter the visitor immediately after the Modules are initialized. + # Example usage scenario: custom auth systems (e.g. OpenID) + $trigger->filter($visitor, "visitor"); + + # First general-purpose trigger. There are many cases you may want to use @route_init@ instead of this, however. + $trigger->call("runtime"); + + # Set the content-type to the theme's "type" setting, or "text/html". + header("Content-type: ".(INDEX ? fallback($theme->type, "text/html") : "text/html")."; charset=UTF-8"); diff --git a/News/includes/controller/Admin.php b/News/includes/controller/Admin.php new file mode 100755 index 0000000..bba44cb --- /dev/null +++ b/News/includes/controller/Admin.php @@ -0,0 +1,2442 @@ +admin_theme = fallback($_SESSION['admin_theme'], "default"); + + $this->theme = new Twig_Loader(MAIN_DIR."/admin/themes/".$this->admin_theme, + (is_writable(INCLUDES_DIR."/caches") and !DEBUG) ? + INCLUDES_DIR."/caches" : + null); + + $this->default = new Twig_Loader(MAIN_DIR."/admin/themes/default", + (is_writable(INCLUDES_DIR."/caches") and !DEBUG) ? + INCLUDES_DIR."/caches" : + null); + } + + /** + * Function: parse + * Determines the action. + */ + public function parse($route) { + $visitor = Visitor::current(); + + # Protect non-responder functions. + if (in_array($route->action, array("__construct", "parse", "subnav_context", "display", "current"))) + show_404(); + + if (empty($route->action) or $route->action == "write") { + # "Write > Post", if they can add posts or drafts. + if (($visitor->group->can("add_post") or $visitor->group->can("add_draft")) and + !empty(Config::current()->enabled_feathers)) + return $route->action = "write_post"; + + # "Write > Page", if they can add pages. + if ($visitor->group->can("add_page")) + return $route->action = "write_page"; + } + + if (empty($route->action) or $route->action == "manage") { + # "Manage > Posts", if they can manage any posts. + if (Post::any_editable() or Post::any_deletable()) + return $route->action = "manage_posts"; + + # "Manage > Pages", if they can manage pages. + if ($visitor->group->can("edit_page") or $visitor->group->can("delete_page")) + return $route->action = "manage_pages"; + + # "Manage > Users", if they can manage users. + if ($visitor->group->can("edit_user") or $visitor->group->can("delete_user")) + return $route->action = "manage_users"; + + # "Manage > Groups", if they can manage groups. + if ($visitor->group->can("edit_group") or $visitor->group->can("delete_group")) + return $route->action = "manage_groups"; + } + + if (empty($route->action) or $route->action == "settings") { + # "General Settings", if they can configure the installation. + if ($visitor->group->can("change_settings")) + return $route->action = "general_settings"; + } + + if (empty($route->action) or $route->action == "extend") { + # "Modules", if they can configure the installation. + if ($visitor->group->can("toggle_extensions")) + return $route->action = "modules"; + } + + Trigger::current()->filter($route->action, "admin_determine_action"); + + if (!isset($route->action)) + show_403(__("Access Denied"), __("You do not have sufficient privileges to view this area.")); + } + + /** + * Function: write + * Post writing. + */ + public function write_post() { + $visitor = Visitor::current(); + + if (!$visitor->group->can("add_post", "add_draft")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to create posts.")); + + $config = Config::current(); + + if (empty($config->enabled_feathers)) + error(__("No Feathers"), __("Please install a feather or two in order to add a post.")); + + Trigger::current()->filter($options, array("write_post_options", "post_options")); + + fallback($_GET['feather'], reset($config->enabled_feathers)); + + $this->display("write_post", + array("groups" => Group::find(array("order" => "id ASC")), + "options" => $options, + "feathers" => Feathers::$instances, + "feather" => Feathers::$instances[$_GET['feather']])); + } + + /** + * Function: bookmarklet + * Post writing, from the bookmarklet. + */ + public function bookmarklet() { + $visitor = Visitor::current(); + if (!$visitor->group->can("add_post", "add_draft")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to create posts.")); + + $config = Config::current(); + + if (empty($config->enabled_feathers)) + error(__("No Feathers"), __("Please install a feather or two in order to add a post.")); + + if (!isset($this->selected_bookmarklet)) + fallback($feather, $config->enabled_feathers[0]); + else + $feather = $this->selected_bookmarklet; + + fallback($_GET['url']); + fallback($_GET['title']); + fallback($_GET['selection']); + + $this->display("bookmarklet", + array("done" => isset($_GET['done']), + "feathers" => Feathers::$instances, + "selected_feather" => Feathers::$instances[$feather], + "args" => array("url" => stripslashes($_GET['url']), + "page_url" => stripslashes($_GET['url']), + "page_link" => '(via '.$_GET['title'].')', + "title" => stripslashes($_GET['title']), + "page_title" => stripslashes($_GET['title']), + "selection" => stripslashes($_GET['selection'])))); + } + + /** + * Function: add_post + * Adds a post when the form is submitted. + */ + public function add_post() { + $visitor = Visitor::current(); + if (!$visitor->group->can("add_post", "add_draft")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to create posts.")); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + if (!isset($_POST['draft']) and !$visitor->group->can("add_post")) + $_POST['draft'] = 'true'; + + $post = Feathers::$instances[$_POST['feather']]->submit(); + + if (!$post->redirect) + $post->redirect = "/admin/?action=write_post"; + + if (!isset($_POST['bookmarklet'])) + Flash::notice(__("Post created!"), $post->redirect); + else + redirect($post->redirect); + } + + /** + * Function: edit_post + * Post editing. + */ + public function edit_post() { + if (empty($_GET['id'])) + error(__("No ID Specified"), __("An ID is required to edit a post.")); + + $post = new Post($_GET['id'], array("drafts" => true, "filter" => false)); + + if (!$post->editable()) + show_403(__("Access Denied"), __("You do not have sufficient privileges to edit this post.")); + + Trigger::current()->filter($options, array("edit_post_options", "post_options"), $post); + + $this->display("edit_post", + array("post" => $post, + "groups" => Group::find(array("order" => "id ASC")), + "options" => $options, + "feather" => Feathers::$instances[$post->feather])); + } + + /** + * Function: update_post + * Updates a post when the form is submitted. + */ + public function update_post() { + $post = new Post($_POST['id'], array("drafts" => true)); + + if ($post->no_results) + Flash::warning(__("Post not found."), "/admin/?action=manage_posts"); + + if (!$post->editable()) + show_403(__("Access Denied"), __("You do not have sufficient privileges to edit this post.")); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + Feathers::$instances[$post->feather]->update($post); + + if (!isset($_POST['ajax'])) + Flash::notice(_f("Post updated. View Post →", + array($post->url())), + "/admin/?action=manage_posts"); + else + exit((string) $_POST['id']); + } + + /** + * Function: delete_post + * Post deletion (confirm page). + */ + public function delete_post() { + if (empty($_GET['id'])) + error(__("No ID Specified"), __("An ID is required to delete a post.")); + + $post = new Post($_GET['id'], array("drafts" => true)); + + if (!$post->deletable()) + show_403(__("Access Denied"), __("You do not have sufficient privileges to delete this post.")); + + $this->display("delete_post", array("post" => $post)); + } + + /** + * Function: destroy_post + * Destroys a post (the real deal). + */ + public function destroy_post() { + if (empty($_POST['id'])) + error(__("No ID Specified"), __("An ID is required to delete a post.")); + + if ($_POST['destroy'] == "bollocks") + redirect("/admin/?action=manage_posts"); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + $post = new Post($_POST['id'], array("drafts" => true)); + if (!$post->deletable()) + show_403(__("Access Denied"), __("You do not have sufficient privileges to delete this post.")); + + Post::delete($_POST['id']); + + Flash::notice(__("Post deleted."), "/admin/?action=manage_posts"); + } + + /** + * Function: manage_posts + * Post managing. + */ + public function manage_posts() { + if (!Post::any_editable() and !Post::any_deletable()) + show_403(__("Access Denied"), __("You do not have sufficient privileges to manage any posts.")); + + fallback($_GET['query'], ""); + + list($where, $params) = keywords($_GET['query'], "post_attributes.value LIKE :query OR url LIKE :query", "post_attributes"); + + if (!empty($_GET['month'])) + $where["created_at like"] = $_GET['month']."-%"; + + $visitor = Visitor::current(); + if (!$visitor->group->can("view_draft", "edit_draft", "edit_post", "delete_draft", "delete_post")) + $where["user_id"] = $visitor->id; + + $results = Post::find(array("placeholders" => true, + "drafts" => true, + "where" => $where, + "params" => $params)); + + $ids = array(); + foreach ($results[0] as $result) + $ids[] = $result["id"]; + + if (!empty($ids)) + $posts = new Paginator(Post::find(array("placeholders" => true, + "drafts" => true, + "where" => array("id" => $ids))), + 25); + else + $posts = new Paginator(array()); + + foreach ($posts->paginated as &$post) { + if (preg_match_all("/\{([0-9]+)\}/", $post->status, $matches)) { + $groups = array(); + $groupClasses = array(); + + foreach ($matches[1] as $id) { + $group = new Group($id); + $groups[] = "Group: ".$group->name; + $groupClasses[] = "group-".$id; + } + + $post->status_name = join(", ", $groups); + $post->status_class = join(" ", $groupClasses); + } else { + $post->status_name = camelize($post->status, true); + $post->status_class = $post->status; + } + } + + $this->display("manage_posts", array("posts" => $posts)); + } + + /** + * Function: write_page + * Page creation. + */ + public function write_page() { + if (!Visitor::current()->group->can("add_page")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to create pages.")); + + $this->display("write_page", array("pages" => Page::find())); + } + + /** + * Function: add_page + * Adds a page when the form is submitted. + */ + public function add_page() { + if (!Visitor::current()->group->can("add_page")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to create pages.")); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + if (empty($_POST['title']) and empty($_POST['slug'])) + error(__("Error"), __("Title and slug cannot be blank.")); + + $page = Page::add($_POST['title'], + $_POST['body'], + null, + $_POST['parent_id'], + !empty($_POST['show_in_list']), + 0, + (!empty($_POST['slug']) ? $_POST['slug'] : sanitize($_POST['title']))); + + Flash::notice(__("Page created!"), $page->url()); + } + + /** + * Function: edit_page + * Page editing. + */ + public function edit_page() { + if (!Visitor::current()->group->can("edit_page")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to edit this page.")); + + if (empty($_GET['id'])) + error(__("No ID Specified"), __("An ID is required to edit a page.")); + + $this->display("edit_page", + array("page" => new Page($_GET['id'], array("filter" => false)), + "pages" => Page::find(array("where" => array("id not" => $_GET['id']))))); + } + + /** + * Function: update_page + * Updates a page when the form is submitted. + */ + public function update_page() { + if (!Visitor::current()->group->can("edit_page")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to edit pages.")); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + if (empty($_POST['title']) and empty($_POST['slug'])) + error(__("Error"), __("Title and slug cannot be blank.")); + + $page = new Page($_POST['id']); + + if ($page->no_results) + Flash::warning(__("Page not found."), "/admin/?action=manage_pages"); + + $page->update($_POST['title'], $_POST['body'], null, $_POST['parent_id'], !empty($_POST['show_in_list']), $page->list_order, null, $_POST['slug']); + + if (!isset($_POST['ajax'])) + Flash::notice(_f("Page updated. View Page →", + array($page->url())), + "/admin/?action=manage_pages"); + } + + /** + * Function: reorder_pages + * Reorders pages. + */ + public function reorder_pages() { + foreach ($_POST['list_order'] as $id => $order) { + $page = new Page($id); + $page->update($page->title, $page->body, null, $page->parent_id, $page->show_in_list, $order, null, $page->url); + } + + Flash::notice(__("Pages reordered."), "/admin/?action=manage_pages"); + } + + /** + * Function: delete_page + * Page deletion (confirm page). + */ + public function delete_page() { + if (!Visitor::current()->group->can("delete_page")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to delete pages.")); + + if (empty($_GET['id'])) + error(__("No ID Specified"), __("An ID is required to delete a page.")); + + $this->display("delete_page", array("page" => new Page($_GET['id']))); + } + + /** + * Function: destroy_page + * Destroys a page. + */ + public function destroy_page() { + if (!Visitor::current()->group->can("delete_page")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to delete pages.")); + + if (empty($_POST['id'])) + error(__("No ID Specified"), __("An ID is required to delete a post.")); + + if ($_POST['destroy'] == "bollocks") + redirect("/admin/?action=manage_pages"); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + $page = new Page($_POST['id']); + + if (!$page->no_results) + foreach ($page->children as $child) + if (isset($_POST['destroy_children'])) + Page::delete($child->id, true); + else + $child->update($child->title, $child->body, 0, $child->show_in_list, $child->list_order, $child->url); + + Page::delete($_POST['id']); + + Flash::notice(__("Page deleted."), "/admin/?action=manage_pages"); + } + + /** + * Function: manage_pages + * Page managing. + */ + public function manage_pages() { + $visitor = Visitor::current(); + if (!$visitor->group->can("edit_page") and !$visitor->group->can("delete_page")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to manage pages.")); + + fallback($_GET['query'], ""); + list($where, $params) = keywords($_GET['query'], "title LIKE :query OR body LIKE :query", "pages"); + + $this->display("manage_pages", + array("pages" => new Paginator(Page::find(array("placeholders" => true, + "where" => $where, + "params" => $params)), 25))); + } + + /** + * Function: new_user + * User creation. + */ + public function new_user() { + if (!Visitor::current()->group->can("add_user")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to add users.")); + + $config = Config::current(); + + $this->display("new_user", + array("default_group" => new Group($config->default_group), + "groups" => Group::find(array("where" => array("id not" => array($config->guest_group, + $config->default_group)), + "order" => "id DESC")))); + } + + /** + * Function: add_user + * Add a user when the form is submitted. + */ + public function add_user() { + if (!Visitor::current()->group->can("add_user")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to add users.")); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + if (empty($_POST['login'])) + error(__("Error"), __("Please enter a username for your account.")); + + $check = new User(array("login" => $_POST['login'])); + if (!$check->no_results) + error(__("Error"), __("That username is already in use.")); + + if (empty($_POST['password1']) or empty($_POST['password2'])) + error(__("Error"), __("Password cannot be blank.")); + elseif ($_POST['password1'] != $_POST['password2']) + error(__("Error"), __("Passwords do not match.")); + + if (empty($_POST['email'])) + error(__("Error"), __("E-mail address cannot be blank.")); + elseif (!preg_match("/^[_A-z0-9-]+((\.|\+)[_A-z0-9-]+)*@[A-z0-9-]+(\.[A-z0-9-]+)*(\.[A-z]{2,4})$/", $_POST['email'])) + error(__("Error"), __("Invalid e-mail address.")); + + if (!empty($_POST['website']) and strpos($_POST['website'], '://') === false) { + $_POST['website'] = 'http://' . $_POST['website']; + } + + User::add($_POST['login'], + $_POST['password1'], + $_POST['email'], + $_POST['full_name'], + $_POST['website'], + $_POST['group']); + + Flash::notice(__("User added."), "/admin/?action=manage_users"); + } + + /** + * Function: edit_user + * User editing. + */ + public function edit_user() { + if (!Visitor::current()->group->can("edit_user")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to edit this user.")); + + if (empty($_GET['id'])) + error(__("No ID Specified"), __("An ID is required to edit a user.")); + + $this->display("edit_user", + array("user" => new User($_GET['id']), + "groups" => Group::find(array("order" => "id ASC", + "where" => array("id not" => Config::current()->guest_group))))); + } + + /** + * Function: update_user + * Updates a user when the form is submitted. + */ + public function update_user() { + if (empty($_POST['id'])) + error(__("No ID Specified"), __("An ID is required to edit a user.")); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + $visitor = Visitor::current(); + + if (!$visitor->group->can("edit_user")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to edit users.")); + + $check_name = new User(null, array("where" => array("login" => $_POST['login'], + "id not" => $_POST['id']))); + + if (!$check_name->no_results) + Flash::notice(_f("Login “%s” is already in use.", array($_POST['login'])), + "/admin/?action=edit_user&id=".$_POST['id']); + + $user = new User($_POST['id']); + + if ($user->no_results) + Flash::warning(__("User not found."), "/admin/?action=manage_user"); + + $password = (!empty($_POST['new_password1']) and $_POST['new_password1'] == $_POST['new_password2']) ? + User::hashPassword($_POST['new_password1']) : + $user->password ; + + $website = (!empty($_POST['website']) and strpos($_POST['website'], '://') === false) ? + $_POST['website'] = 'http://' . $_POST['website'] : + $_POST['website'] ; + + $user->update($_POST['login'], $password, $_POST['email'], $_POST['full_name'], $website, $_POST['group']); + + if ($_POST['id'] == $visitor->id) + $_SESSION['password'] = $password; + + Flash::notice(__("User updated."), "/admin/?action=manage_users"); + } + + /** + * Function: delete_user + * User deletion. + */ + public function delete_user() { + if (!Visitor::current()->group->can("delete_user")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to delete users.")); + + if (empty($_GET['id'])) + error(__("No ID Specified"), __("An ID is required to delete a user.")); + + $this->display("delete_user", + array("user" => new User($_GET['id']), + "users" => User::find(array("where" => array("id not" => $_GET['id']))))); + } + + /** + * Function: destroy_user + * Destroys a user. + */ + public function destroy_user() { + if (!Visitor::current()->group->can("delete_user")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to delete users.")); + + if (empty($_POST['id'])) + error(__("No ID Specified"), __("An ID is required to delete a user.")); + + if ($_POST['destroy'] == "bollocks") + redirect("/admin/?action=manage_users"); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + $sql = SQL::current(); + $user = new User($_POST['id']); + + if (isset($_POST['posts'])) { + if ($_POST['posts'] == "delete") + foreach ($user->post as $post) + Post::delete($post->id); + elseif ($_POST['posts'] == "move") + $sql->update("posts", + array("user_id" => $user->id), + array("user_id" => $_POST['move_posts'])); + } + + if (isset($_POST['pages'])) { + if ($_POST['pages'] == "delete") + foreach ($user->page as $page) + Page::delete($page->id); + elseif ($_POST['pages'] == "move") + $sql->update("pages", + array("user_id" => $user->id), + array("user_id" => $_POST['move_pages'])); + } + + User::delete($_POST['id']); + + Flash::notice(__("User deleted."), "/admin/?action=manage_users"); + } + + /** + * Function: manage_users + * User managing. + */ + public function manage_users() { + $visitor = Visitor::current(); + if (!$visitor->group->can("edit_user") and !$visitor->group->can("delete_user") and !$visitor->group->can("add_user")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to manage users.")); + + fallback($_GET['query'], ""); + list($where, $params) = keywords($_GET['query'], "login LIKE :query OR full_name LIKE :query OR email LIKE :query OR website LIKE :query", "users"); + + $this->display("manage_users", + array("users" => new Paginator(User::find(array("placeholders" => true, + "where" => $where, + "params" => $params)), + 25))); + } + + /** + * Function: new_group + * Group creation. + */ + public function new_group() { + if (!Visitor::current()->group->can("add_group")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to create groups.")); + + $this->display("new_group", + array("permissions" => SQL::current()->select("permissions", "*", array("group_id" => 0))->fetchAll())); + } + + /** + * Function: add_group + * Adds a group when the form is submitted. + */ + public function add_group() { + if (!Visitor::current()->group->can("add_group")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to create groups.")); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + Group::add($_POST['name'], array_keys($_POST['permissions'])); + + Flash::notice(__("Group added."), "/admin/?action=manage_groups"); + } + + /** + * Function: edit_group + * Group editing. + */ + public function edit_group() { + if (!Visitor::current()->group->can("edit_group")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to edit groups.")); + + if (empty($_GET['id'])) + error(__("No ID Specified"), __("An ID is required to edit a group.")); + + $this->display("edit_group", + array("group" => new Group($_GET['id']), + "permissions" => SQL::current()->select("permissions", "*", array("group_id" => 0))->fetchAll())); + } + + /** + * Function: update_group + * Updates a group when the form is submitted. + */ + public function update_group() { + if (!Visitor::current()->group->can("edit_group")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to edit groups.")); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + $permissions = array_keys($_POST['permissions']); + + $check_name = new Group(null, array("where" => array("name" => $_POST['name'], + "id not" => $_POST['id']))); + + if (!$check_name->no_results) + Flash::notice(_f("Group name “%s” is already in use.", array($_POST['name'])), + "/admin/?action=edit_group&id=".$_POST['id']); + + $group = new Group($_POST['id']); + + if ($group->no_results) + Flash::warning(__("Group not found."), "/admin/?action=manage_groups"); + + $group->update($_POST['name'], $permissions); + + Flash::notice(__("Group updated."), "/admin/?action=manage_groups"); + } + + /** + * Function: delete_group + * Group deletion (confirm page). + */ + public function delete_group() { + if (!Visitor::current()->group->can("delete_group")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to delete groups.")); + + if (empty($_GET['id'])) + error(__("No ID Specified"), __("An ID is required to delete a group.")); + + $this->display("delete_group", + array("group" => new Group($_GET['id']), + "groups" => Group::find(array("where" => array("id not" => $_GET['id']), + "order" => "id ASC")))); + } + + /** + * Function: destroy_group + * Destroys a group. + */ + public function destroy_group() { + if (!Visitor::current()->group->can("delete_group")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to delete groups.")); + + if (!isset($_POST['id'])) + error(__("No ID Specified"), __("An ID is required to delete a group.")); + + if ($_POST['destroy'] == "bollocks") + redirect("/admin/?action=manage_groups"); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + $group = new Group($_POST['id']); + foreach ($group->users as $user) + $user->update($user->login, $user->password, $user->email, $user->full_name, $user->website, $_POST['move_group']); + + $config = Config::current(); + if (!empty($_POST['default_group'])) + $config->set("default_group", $_POST['default_group']); + if (!empty($_POST['guest_group'])) + $config->set("guest_group", $_POST['guest_group']); + + Group::delete($_POST['id']); + + Flash::notice(__("Group deleted."), "/admin/?action=manage_groups"); + } + + /** + * Function: manage_groups + * Group managing. + */ + public function manage_groups() { + $visitor = Visitor::current(); + if (!$visitor->group->can("edit_group") and !$visitor->group->can("delete_group") and !$visitor->group->can("add_group")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to manage groups.")); + + if (!empty($_GET['search'])) { + $user = new User(array("login" => $_GET['search'])); + if (!$user->no_results) + $groups = new Paginator(array($user->group), 10); + else + $groups = new Paginator(array(), 10); + } else + $groups = new Paginator(Group::find(array("placeholders" => true, "order" => "id ASC")), 10); + + $this->display("manage_groups", + array("groups" => $groups)); + } + + /** + * Function: export + * Export posts, pages, etc. + */ + public function export() { + if (!Visitor::current()->group->can("add_post")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to export content.")); + + if (empty($_POST)) + return $this->display("export"); + + $config = Config::current(); + $trigger = Trigger::current(); + $route = Route::current(); + $exports = array(); + + if (isset($_POST['posts'])) { + list($where, $params) = keywords($_POST['filter_posts'], "post_attributes.value LIKE :query OR url LIKE :query", "post_attributes"); + + if (!empty($_GET['month'])) + $where["created_at like"] = $_GET['month']."-%"; + + $visitor = Visitor::current(); + if (!$visitor->group->can("view_draft", "edit_draft", "edit_post", "delete_draft", "delete_post")) + $where["user_id"] = $visitor->id; + + $results = Post::find(array("placeholders" => true, + "drafts" => true, + "where" => $where, + "params" => $params)); + + $ids = array(); + foreach ($results[0] as $result) + $ids[] = $result["id"]; + + if (!empty($ids)) + $posts = Post::find(array("drafts" => true, + "where" => array("id" => $ids), + "order" => "id ASC"), + array("filter" => false)); + else + $posts = new Paginator(array()); + + $latest_timestamp = 0; + foreach ($posts as $post) + if (strtotime($post->created_at) > $latest_timestamp) + $latest_timestamp = strtotime($post->created_at); + + $id = substr(strstr($config->url, "//"), 2); + $id = str_replace("#", "/", $id); + $id = preg_replace("/(".preg_quote(parse_url($config->url, PHP_URL_HOST)).")/", "\\1,".date("Y", $latest_timestamp).":", $id, 1); + + $posts_atom = ''."\r"; + $posts_atom.= ''."\r"; + $posts_atom.= ' '.fix($config->name).' Posts'."\r"; + $posts_atom.= ' '.fix($config->description).''."\r"; + $posts_atom.= ' tag:'.parse_url($config->url, PHP_URL_HOST).','.date("Y", $latest_timestamp).':Chyrp'."\r"; + $posts_atom.= ' '.date("c", $latest_timestamp).''."\r"; + $posts_atom.= ' '."\r"; + $posts_atom.= ' Chyrp'."\r"; + + foreach ($posts as $post) { + $title = fix($post->title(), false); + fallback($title, ucfirst($post->feather)." Post #".$post->id); + + $updated = ($post->updated) ? $post->updated_at : $post->created_at ; + + $tagged = substr(strstr(url("id/".$post->id), "//"), 2); + $tagged = str_replace("#", "/", $tagged); + $tagged = preg_replace("/(".preg_quote(parse_url($post->url(), PHP_URL_HOST)).")/", "\\1,".when("Y-m-d", $updated).":", $tagged, 1); + + $url = $post->url(); + $posts_atom.= ' '."\r"; + $posts_atom.= ' '.$title.''."\r"; + $posts_atom.= ' tag:'.$tagged.''."\r"; + $posts_atom.= ' '.when("c", $updated).''."\r"; + $posts_atom.= ' '.when("c", $post->created_at).''."\r"; + $posts_atom.= ' '."\r"; + $posts_atom.= ' '."\r"; + $posts_atom.= ' '.fix(oneof($post->user->full_name, $post->user->login)).''."\r"; + + if (!empty($post->user->website)) + $posts_atom.= ' '.fix($post->user->website).''."\r"; + + $posts_atom.= ' '.fix($post->user->login).''."\r"; + $posts_atom.= ' '."\r"; + $posts_atom.= ' '."\r"; + + foreach ($post->attributes as $key => $val) + $posts_atom.= ' <'.$key.'>'.fix($val).''."\r"; + + $posts_atom.= ' '."\r"; + + foreach (array("feather", "clean", "url", "pinned", "status") as $attr) + $posts_atom.= ' '.fix($post->$attr).''."\r"; + + $trigger->filter($posts_atom, "posts_export", $post); + + $posts_atom.= ' '."\r"; + + } + $posts_atom.= ''."\r"; + + $exports["posts.atom"] = $posts_atom; + } + + if (isset($_POST['pages'])) { + list($where, $params) = keywords($_POST['filter_pages'], "title LIKE :query OR body LIKE :query", "pages"); + + $pages = Page::find(array("where" => $where, "params" => $params, "order" => "id ASC"), + array("filter" => false)); + + $latest_timestamp = 0; + foreach ($pages as $page) + if (strtotime($page->created_at) > $latest_timestamp) + $latest_timestamp = strtotime($page->created_at); + + $pages_atom = ''."\r"; + $pages_atom.= ''."\r"; + $pages_atom.= ' '.fix($config->name).' Pages'."\r"; + $pages_atom.= ' '.fix($config->description).''."\r"; + $pages_atom.= ' tag:'.parse_url($config->url, PHP_URL_HOST).','.date("Y", $latest_timestamp).':Chyrp'."\r"; + $pages_atom.= ' '.date("c", $latest_timestamp).''."\r"; + $pages_atom.= ' '."\r"; + $pages_atom.= ' Chyrp'."\r"; + + foreach ($pages as $page) { + $updated = ($page->updated) ? $page->updated_at : $page->created_at ; + + $tagged = substr(strstr($page->url(), "//"), 2); + $tagged = str_replace("#", "/", $tagged); + $tagged = preg_replace("/(".preg_quote(parse_url($page->url(), PHP_URL_HOST)).")/", "\\1,".when("Y-m-d", $updated).":", $tagged, 1); + + $url = $page->url(); + $pages_atom.= ' '."\r"; + $pages_atom.= ' '.fix($page->title).''."\r"; + $pages_atom.= ' tag:'.$tagged.''."\r"; + $pages_atom.= ' '.when("c", $updated).''."\r"; + $pages_atom.= ' '.when("c", $page->created_at).''."\r"; + $pages_atom.= ' '."\r"; + $pages_atom.= ' '."\r"; + $pages_atom.= ' '.fix(oneof($page->user->full_name, $page->user->login)).''."\r"; + + if (!empty($page->user->website)) + $pages_atom.= ' '.fix($page->user->website).''."\r"; + + $pages_atom.= ' '.fix($page->user->login).''."\r"; + $pages_atom.= ' '."\r"; + $pages_atom.= ' '.fix($page->body).''."\r"; + + foreach (array("show_in_list", "list_order", "clean", "url") as $attr) + $pages_atom.= ' '.fix($page->$attr).''."\r"; + + + $trigger->filter($pages_atom, "pages_export", $page); + + $pages_atom.= ' '."\r"; + } + $pages_atom.= ''."\r"; + + $exports["pages.atom"] = $pages_atom; + } + + if (isset($_POST['groups'])) { + list($where, $params) = keywords($_POST['filter_groups'], "name LIKE :query", "groups"); + + $groups = Group::find(array("where" => $where, "params" => $params, "order" => "id ASC")); + + $groups_yaml = array("groups" => array(), + "permissions" => array()); + + foreach (SQL::current()->select("permissions", "*", array("group_id" => 0))->fetchAll() as $permission) + $groups_yaml["permissions"][$permission["id"]] = $permission["name"]; + + foreach ($groups as $index => $group) + $groups_yaml["groups"][$group->name] = $group->permissions; + + $exports["groups.yaml"] = YAML::dump($groups_yaml); + } + + if (isset($_POST['users'])) { + list($where, $params) = keywords($_POST['filter_users'], "login LIKE :query OR full_name LIKE :query OR email LIKE :query OR website LIKE :query", "users"); + + $users = User::find(array("where" => $where, "params" => $params, "order" => "id ASC")); + + $users_yaml = array(); + foreach ($users as $user) { + $users_yaml[$user->login] = array(); + + foreach ($user as $name => $attr) + if (!in_array($name, array("no_results", "group_id", "group", "id", "login", "belongs_to", "has_many", "has_one", "queryString"))) + $users_yaml[$user->login][$name] = $attr; + elseif ($name == "group_id") + $users_yaml[$user->login]["group"] = $user->group->name; + } + + $exports["users.yaml"] = YAML::dump($users_yaml); + } + + $trigger->filter($exports, "export"); + + require INCLUDES_DIR."/lib/zip.php"; + + $zip = new ZipFile(); + foreach ($exports as $filename => $content) + $zip->addFile($content, $filename); + + $zip_contents = $zip->file(); + + $filename = sanitize(camelize($config->name), false, true)."_Export_".date("Y-m-d"); + header("Content-type: application/octet-stream"); + header("Content-Disposition: attachment; filename=\"".$filename.".zip\""); + header("Content-length: ".strlen($zip_contents)."\n\n"); + + echo $zip_contents; + } + + /** + * Function: import + * Importing content from other systems. + */ + public function import() { + if (!Visitor::current()->group->can("add_post")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to import content.")); + + $this->display("import"); + } + + /** + * Function: import_chyrp + * Chyrp importing. + */ + public function import_chyrp() { + if (empty($_POST)) + redirect("/admin/?action=import"); + + if (!Visitor::current()->group->can("add_post")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to import content.")); + + if (isset($_FILES['posts_file']) and $_FILES['posts_file']['error'] == 0) + if (!$posts = simplexml_load_file($_FILES['posts_file']['tmp_name']) or $posts->generator != "Chyrp") + Flash::warning(__("Chyrp Posts export file is invalid."), "/admin/?action=import"); + + if (isset($_FILES['pages_file']) and $_FILES['pages_file']['error'] == 0) + if (!$pages = simplexml_load_file($_FILES['pages_file']['tmp_name']) or $pages->generator != "Chyrp") + Flash::warning(__("Chyrp Pages export file is invalid."), "/admin/?action=import"); + + if (ini_get("memory_limit") < 20) + ini_set("memory_limit", "20M"); + + $trigger = Trigger::current(); + $visitor = Visitor::current(); + $sql = SQL::current(); + + function media_url_scan(&$value) { + $config = Config::current(); + + $regexp_url = preg_quote($_POST['media_url'], "/"); + if (preg_match_all("/{$regexp_url}([^\.\!,\?;\"\'<>\(\)\[\]\{\}\s\t ]+)\.([a-zA-Z0-9]+)/", $value, $media)) + foreach ($media[0] as $matched_url) { + $filename = upload_from_url($matched_url); + $value = str_replace($matched_url, $config->url.$config->uploads_path.$filename, $value); + } + } + + if (isset($_FILES['groups_file']) and $_FILES['groups_file']['error'] == 0) { + $import = YAML::load($_FILES['groups_file']['tmp_name']); + + foreach ($import["groups"] as $name => $permissions) + if (!$sql->count("groups", array("name" => $name))) + $trigger->call("import_chyrp_group", Group::add($name, (array) $permissions)); + + foreach ($import["permissions"] as $id => $name) + if (!$sql->count("permissions", array("id" => $id))) + $sql->insert("permissions", array("id" => $id, "name" => $name)); + } + + if (isset($_FILES['users_file']) and $_FILES['users_file']['error'] == 0) { + $users = YAML::load($_FILES['users_file']['tmp_name']); + + foreach ($users as $login => $user) { + $group_id = $sql->select("groups", "id", array("name" => $user["group"]), "id DESC")->fetchColumn(); + + $group = ($group_id) ? $group_id : $config->default_group ; + + if (!$sql->count("users", array("login" => $login))) + $user = User::add($login, + $user["password"], + $user["email"], + $user["full_name"], + $user["website"], + $group, + $user["joined_at"]); + + $trigger->call("import_chyrp_user", $user); + } + } + + if (isset($_FILES['posts_file']) and $_FILES['posts_file']['error'] == 0) + foreach ($posts->entry as $entry) { + $chyrp = $entry->children("http://chyrp.net/export/1.0/"); + + $login = $entry->author->children("http://chyrp.net/export/1.0/")->login; + $user_id = $sql->select("users", "id", array("login" => $login), "id DESC")->fetchColumn(); + + $data = xml2arr($entry->content); + $data["imported_from"] = "chyrp"; + + if (!empty($_POST['media_url'])) + array_walk_recursive($data, "media_url_scan"); + + $post = Post::add($data, + $chyrp->clean, + Post::check_url($chyrp->url), + $chyrp->feather, + ($user_id ? $user_id : $visitor->id), + (bool) (int) $chyrp->pinned, + $chyrp->status, + datetime($entry->published), + ($entry->updated == $entry->published) ? + null : + datetime($entry->updated), + "", + false); + + $trigger->call("import_chyrp_post", $entry, $post); + } + + if (isset($_FILES['pages_file']) and $_FILES['pages_file']['error'] == 0) + foreach ($pages->entry as $entry) { + $chyrp = $entry->children("http://chyrp.net/export/1.0/"); + $attr = $entry->attributes("http://chyrp.net/export/1.0/"); + + $login = $entry->author->children("http://chyrp.net/export/1.0/")->login; + $user_id = $sql->select("users", "id", array("login" => $login), "id DESC")->fetchColumn(); + + $page = Page::add($entry->title, + $entry->content, + ($user_id ? $user_id : $visitor->id), + $attr->parent_id, + (bool) (int) $chyrp->show_in_list, + $chyrp->list_order, + $chyrp->clean, + Page::check_url($chyrp->url), + datetime($entry->published), + ($entry->updated == $entry->published) ? null : datetime($entry->updated)); + + $trigger->call("import_chyrp_page", $entry, $page); + } + + Flash::notice(__("Chyrp content successfully imported!"), "/admin/?action=import"); + } + + /** + * Function: import_wordpress + * WordPress importing. + */ + public function import_wordpress() { + if (empty($_POST)) + redirect("/admin/?action=import"); + + if (!Visitor::current()->group->can("add_post")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to import content.")); + + $config = Config::current(); + + if (!in_array("text", $config->enabled_feathers)) + error(__("Missing Feather"), __("Importing from WordPress requires the Text feather to be installed and enabled.")); + + if (ini_get("memory_limit") < 20) + ini_set("memory_limit", "20M"); + + $trigger = Trigger::current(); + + $stupid_xml = file_get_contents($_FILES['xml_file']['tmp_name']); + $sane_xml = preg_replace(array("//", "/<\/wp:comment_content>/"), + array(""), + $stupid_xml); + + $sane_xml = str_replace(array("]]>"), + array(""), + $sane_xml); + + if (!substr_count($sane_xml, "xmlns:excerpt")) + $sane_xml = preg_replace("/xmlns:content=\"([^\"]+)\"(\s+)/m", + "xmlns:content=\"\\1\"\\2xmlns:excerpt=\"http://wordpress.org/excerpt/1.0/\"\\2", + $sane_xml); + + $fix_amps_count = 1; + while ($fix_amps_count) + $sane_xml = preg_replace("/(.+)&(?!amp;)(.+)<\/wp:meta_value>/m", + "\\1&\\2", + $sane_xml, -1, $fix_amps_count); + + # Remove null (x00) characters + $sane_xml = str_replace("", "", $sane_xml); + + $xml = simplexml_load_string($sane_xml, "SimpleXMLElement", LIBXML_NOCDATA); + + if (!$xml or !substr_count($xml->channel->generator, "wordpress.org")) + Flash::warning(__("File does not seem to be a valid WordPress export file, or could not be parsed. Please check your PHP error log."), + "/admin/?action=import"); + + foreach ($xml->channel->item as $item) { + $wordpress = $item->children("http://wordpress.org/export/1.0/"); + $content = $item->children("http://purl.org/rss/1.0/modules/content/"); + if ($wordpress->status == "attachment" or $item->title == "zz_placeholder") + continue; + + $regexp_url = preg_quote($_POST['media_url'], "/"); + if (!empty($_POST['media_url']) and + preg_match_all("/{$regexp_url}([^\.\!,\?;\"\'<>\(\)\[\]\{\}\s\t ]+)\.([a-zA-Z0-9]+)/", + $content->encoded, + $media)) + foreach ($media[0] as $matched_url) { + $filename = upload_from_url($matched_url); + $content->encoded = str_replace($matched_url, $config->url.$config->uploads_path.$filename, $content->encoded); + } + + $clean = (isset($wordpress->post_name)) ? $wordpress->post_name : sanitize($item->title) ; + + $pinned = (isset($wordpress->is_sticky)) ? $wordpress->is_sticky : 0 ; + + if (empty($wordpress->post_type) or $wordpress->post_type == "post") { + $status_translate = array("publish" => "public", + "draft" => "draft", + "private" => "private", + "static" => "public", + "object" => "public", + "inherit" => "public", + "future" => "draft", + "pending" => "draft"); + + $data = array("title" => trim($item->title), + "body" => trim($content->encoded), + "imported_from" => "wordpress"); + + $post = Post::add($data, + $clean, + Post::check_url($clean), + "text", + null, + $pinned, + $status_translate[(string) $wordpress->status], + (string) ($wordpress->post_date == "0000-00-00 00:00:00" ? datetime() : $wordpress->post_date), + null, + "", + false); + + $trigger->call("import_wordpress_post", $item, $post); + } elseif ($wordpress->post_type == "page") { + $page = Page::add(trim($item->title), + trim($content->encoded), + null, + 0, + true, + 0, + $clean, + Page::check_url($clean), + (string) ($wordpress->post_date == "0000-00-00 00:00:00" ? datetime() : $wordpress->post_date)); + + $trigger->call("import_wordpress_page", $item, $page); + } + } + + Flash::notice(__("WordPress content successfully imported!"), "/admin/?action=import"); + } + + /** + * Function: import_tumblr + * Tumblr importing. + */ + public function import_tumblr() { + if (empty($_POST)) + redirect("/admin/?action=import"); + + if (!Visitor::current()->group->can("add_post")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to import content.")); + + $config = Config::current(); + if (!in_array("text", $config->enabled_feathers) or + !in_array("video", $config->enabled_feathers) or + !in_array("audio", $config->enabled_feathers) or + !in_array("chat", $config->enabled_feathers) or + !in_array("photo", $config->enabled_feathers) or + !in_array("quote", $config->enabled_feathers) or + !in_array("link", $config->enabled_feathers)) + error(__("Missing Feather"), __("Importing from Tumblr requires the Text, Video, Audio, Chat, Photo, Quote, and Link feathers to be installed and enabled.")); + + if (ini_get("memory_limit") < 20) + ini_set("memory_limit", "20M"); + + if (!parse_url($_POST['tumblr_url'], PHP_URL_SCHEME)) + $_POST['tumblr_url'] = "http://".$_POST['tumblr_url']; + + set_time_limit(3600); + $url = rtrim($_POST['tumblr_url'], "/")."/api/read?num=50"; + $api = preg_replace("/<(\/?)([a-z]+)\-([a-z]+)/", "<\\1\\2_\\3", get_remote($url)); + $api = preg_replace("/ ([a-z]+)\-([a-z]+)=/", " \\1_\\2=", $api); + $xml = simplexml_load_string($api); + + if (!isset($xml->tumblelog)) + Flash::warning(_f("Content could not be retrieved from the given URL. ". get_remote($url)), + "/admin/?action=import"); + + $already_in = $posts = array(); + foreach ($xml->posts->post as $post) { + $posts[] = $post; + $already_in[] = $post->attributes()->id; + } + + while ($xml->posts->attributes()->total > count($posts)) { + set_time_limit(3600); + $api = preg_replace("/<(\/?)([a-z]+)\-([a-z]+)/", "<\\1\\2_\\3", get_remote($url."&start=".count($posts))); + $api = preg_replace("/ ([a-z]+)\-([a-z]+)=/", " \\1_\\2=", $api); + $xml = simplexml_load_string($api, "SimpleXMLElement", LIBXML_NOCDATA); + foreach ($xml->posts->post as $post) + if (!in_array($post->attributes()->id, $already_in)) { + $posts[] = $post; + $already_in[] = $post->attributes()->id; + } + } + + function reverse($a, $b) { + if (empty($a) or empty($b)) return 0; + return (strtotime($a->attributes()->date) < strtotime($b->attributes()->date)) ? -1 : 1 ; + } + + set_time_limit(3600); + usort($posts, "reverse"); + + foreach ($posts as $key => $post) { + set_time_limit(3600); + if ($post->attributes()->type == "audio") + break; # Can't import Audio posts since Tumblr has the files locked in to Amazon. + + $translate_types = array("regular" => "text", "conversation" => "chat"); + + $clean = ""; + switch($post->attributes()->type) { + case "regular": + $title = fallback($post->regular_title); + $values = array("title" => $title, + "body" => $post->regular_body); + $clean = sanitize($title); + break; + case "video": + $values = array("embed" => $post->video_player, + "caption" => fallback($post->video_caption)); + break; + case "conversation": + $title = fallback($post->conversation_title); + + $lines = array(); + foreach ($post->conversation_line as $line) + $lines[] = $line->attributes()->label." ".$line; + + $values = array("title" => $title, + "dialogue" => implode("\n", $lines)); + $clean = sanitize($title); + break; + case "photo": + $values = array("filename" => upload_from_url($post->photo_url[0]), + "caption" => fallback($post->photo_caption)); + break; + case "quote": + $values = array("quote" => $post->quote_text, + "source" => preg_replace("/^— /", "", + fallback($post->quote_source))); + break; + case "link": + $name = fallback($post->link_text); + $values = array("name" => $name, + "source" => $post->link_url, + "description" => fallback($post->link_description)); + $clean = sanitize($name); + break; + } + + $values["imported_from"] = "tumblr"; + + $new_post = Post::add($values, + $clean, + Post::check_url($clean), + fallback($translate_types[(string) $post->attributes()->type], (string) $post->attributes()->type), + null, + null, + "public", + datetime((int) $post->attributes()->unix_timestamp), + null, + "", + false); + + Trigger::current()->call("import_tumble", $post, $new_post); + } + + Flash::notice(__("Tumblr content successfully imported!"), "/admin/?action=import"); + } + + /** + * Function: import_textpattern + * TextPattern importing. + */ + public function import_textpattern() { + if (empty($_POST)) + redirect("/admin/?action=import"); + + if (!Visitor::current()->group->can("add_post")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to import content.")); + + $config = Config::current(); + $trigger = Trigger::current(); + + $dbcon = $dbsel = false; + if ($link = @mysql_connect($_POST['host'], $_POST['username'], $_POST['password'])) { + $dbcon = true; + $dbsel = @mysql_select_db($_POST['database'], $link); + } + + if (!$dbcon or !$dbsel) + Flash::warning(__("Could not connect to the specified TextPattern database."), + "/admin/?action=import"); + + mysql_query("SET NAMES 'utf8'"); + + $get_posts = mysql_query("SELECT * FROM {$_POST['prefix']}textpattern ORDER BY ID ASC", $link) or error(__("Database Error"), mysql_error()); + $posts = array(); + while ($post = mysql_fetch_array($get_posts)) + $posts[$post["ID"]] = $post; + + foreach ($posts as $post) { + $regexp_url = preg_quote($_POST['media_url'], "/"); + if (!empty($_POST['media_url']) and + preg_match_all("/{$regexp_url}([^\.\!,\?;\"\'<>\(\)\[\]\{\}\s\t ]+)\.([a-zA-Z0-9]+)/", + $post["Body"], + $media)) + foreach ($media[0] as $matched_url) { + $filename = upload_from_url($matched_url); + $post["Body"] = str_replace($matched_url, $config->url.$config->uploads_path.$filename, $post["Body"]); + } + + $status_translate = array(1 => "draft", + 2 => "private", + 3 => "draft", + 4 => "public", + 5 => "public"); + + $clean = fallback($post["url_title"], sanitize($post["Title"])); + + $new_post = Post::add(array("title" => $post["Title"], + "body" => $post["Body"], + "imported_from" => "textpattern"), + $clean, + Post::check_url($clean), + "text", + null, + ($post["Status"] == "5"), + $status_translate[$post["Status"]], + $post["Posted"], + null, + "", + false); + + $trigger->call("import_textpattern_post", $post, $new_post); + } + + mysql_close($link); + + Flash::notice(__("TextPattern content successfully imported!"), "/admin/?action=import"); + } + + /** + * Function: import_movabletype + * MovableType importing. + */ + public function import_movabletype() { + if (empty($_POST)) + redirect("/admin/?action=import"); + + if (!Visitor::current()->group->can("add_post")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to import content.")); + + $config = Config::current(); + $trigger = Trigger::current(); + + $dbcon = $dbsel = false; + if ($link = @mysql_connect($_POST['host'], $_POST['username'], $_POST['password'])) { + $dbcon = true; + $dbsel = @mysql_select_db($_POST['database'], $link); + } + + if (!$dbcon or !$dbsel) + Flash::warning(__("Could not connect to the specified MovableType database."), + "/admin/?action=import"); + + mysql_query("SET NAMES 'utf8'"); + + $get_authors = mysql_query("SELECT * FROM mt_author ORDER BY author_id ASC", $link) or error(__("Database Error"), mysql_error()); + $users = array(); + while ($author = mysql_fetch_array($get_authors)) { + # Try to figure out if this author is the same as the person doing the import. + if ($author["author_name"] == Visitor::current()->login or + $author["author_nickname"] == Visitor::current()->login or + $author["author_nickname"] == Visitor::current()->full_name or + $author["author_url"] == Visitor::current()->website or + $author["author_email"] == Visitor::current()->email) + $users[$author["author_id"]] = Visitor::current(); + else + $users[$author["author_id"]] = User::add($author["author_name"], + $author["author_password"], + $author["author_email"], + ($author["author_nickname"] != $author["author_name"] ? + $author["author_nickname"] : + ""), + $author["author_url"], + ($author["author_can_create_blog"] == "1" ? + Visitor::current()->group : + null), + $author["author_created_on"], + false); + } + + $get_posts = mysql_query("SELECT * FROM mt_entry ORDER BY entry_id ASC", $link) or error(__("Database Error"), mysql_error()); + $posts = array(); + while ($post = mysql_fetch_array($get_posts)) + $posts[$post["entry_id"]] = $post; + + foreach ($posts as $post) { + $body = $post["entry_text"]; + + if (!empty($post["entry_text_more"])) + $body.= "\n\n\n\n".$post["entry_text_more"]; + + $regexp_url = preg_quote($_POST['media_url'], "/"); + if (!empty($_POST['media_url']) and + preg_match_all("/{$regexp_url}([^\.\!,\?;\"\'<>\(\)\[\]\{\}\s\t ]+)\.([a-zA-Z0-9]+)/", + $body, + $media)) + foreach ($media[0] as $matched_url) { + $filename = upload_from_url($matched_url); + $body = str_replace($matched_url, $config->url.$config->uploads_path.$filename, $body); + } + + $status_translate = array(1 => "draft", + 2 => "public", + 3 => "draft", + 4 => "draft"); + + $clean = oneof($post["entry_basename"], sanitize($post["entry_title"])); + + if (empty($post["entry_class"]) or $post["entry_class"] == "entry") { + $new_post = Post::add(array("title" => $post["entry_title"], + "body" => $body, + "imported_from" => "movabletype"), + $clean, + Post::check_url($clean), + "text", + @$users[$post["entry_author_id"]], + false, + $status_translate[$post["entry_status"]], + oneof(@$post["entry_authored_on"], @$post["entry_created_on"], datetime()), + $post["entry_modified_on"], + "", + false); + $trigger->call("import_movabletype_post", $post, $new_post, $link); + } elseif (@$post["entry_class"] == "page") { + $new_page = Page::add($post["entry_title"], $body, null, 0, true, 0, $clean, Page::check_url($clean)); + $trigger->call("import_movabletype_page", $post, $new_page, $link); + } + } + + mysql_close($link); + + Flash::notice(__("MovableType content successfully imported!"), "/admin/?action=import"); + } + + /** + * Function: modules + * Module enabling/disabling. + */ + public function modules() { + if (!Visitor::current()->group->can("toggle_extensions")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to enable/disable modules.")); + + $config = Config::current(); + + $this->context["enabled_modules"] = $this->context["disabled_modules"] = array(); + + if (!$open = @opendir(MODULES_DIR)) + return Flash::warning(__("Could not read modules directory.")); + + $classes = array(); + + while (($folder = readdir($open)) !== false) { + if (!file_exists(MODULES_DIR."/".$folder."/".$folder.".php") or !file_exists(MODULES_DIR."/".$folder."/info.yaml")) continue; + + if (file_exists(MODULES_DIR."/".$folder."/locale/".$config->locale.".mo")) + load_translator($folder, MODULES_DIR."/".$folder."/locale/".$config->locale.".mo"); + + if (!isset($classes[$folder])) + $classes[$folder] = array($folder); + else + array_unshift($classes[$folder], $folder); + + $info = YAML::load(MODULES_DIR."/".$folder."/info.yaml"); + + $info["conflicts_true"] = array(); + $info["depends_true"] = array(); + + if (!empty($info["conflicts"])) { + $classes[$folder][] = "conflict"; + + foreach ((array) $info["conflicts"] as $conflict) + if (file_exists(MODULES_DIR."/".$conflict."/".$conflict.".php")) + $classes[$folder][] = "conflict_".$conflict; + } + + $dependencies_needed = array(); + if (!empty($info["depends"])) { + $classes[$folder][] = "depends"; + + foreach ((array) $info["depends"] as $dependency) { + if (!module_enabled($dependency)) { + if (!in_array("missing_dependency", $classes[$folder])) + $classes[$folder][] = "missing_dependency"; + + $classes[$folder][] = "needs_".$dependency; + + $dependencies_needed[] = $dependency; + } + + $classes[$folder][] = "depends_".$dependency; + + fallback($classes[$dependency], array()); + $classes[$dependency][] = "depended_by_".$folder; + } + } + + fallback($info["name"], $folder); + fallback($info["version"], "0"); + fallback($info["url"]); + fallback($info["description"]); + fallback($info["author"], array("name" => "", "url" => "")); + fallback($info["help"]); + + $info["description"] = __($info["description"], $folder); + $info["description"] = preg_replace(array("/(.+)<\/code>/se", "/
(.+)<\/pre>/se"),
+                                                    array("''.fix('\\1').''", "'
'.fix('\\1').'
'"), + $info["description"]); + + $info["author"]["link"] = !empty($info["author"]["url"]) ? + ''.fix($info["author"]["name"]).'' : + $info["author"]["name"] ; + + $category = (module_enabled($folder)) ? "enabled_modules" : "disabled_modules" ; + $this->context[$category][$folder] = array("name" => $info["name"], + "version" => $info["version"], + "url" => $info["url"], + "description" => $info["description"], + "author" => $info["author"], + "help" => $info["help"], + "classes" => $classes[$folder], + "dependencies_needed" => $dependencies_needed); + } + + foreach ($this->context["enabled_modules"] as $module => &$attrs) + $attrs["classes"] = $classes[$module]; + + foreach ($this->context["disabled_modules"] as $module => &$attrs) + $attrs["classes"] = $classes[$module]; + + $this->display("modules"); + } + + /** + * Function: feathers + * Feather enabling/disabling. + */ + public function feathers() { + if (!Visitor::current()->group->can("toggle_extensions")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to enable/disable feathers.")); + + $config = Config::current(); + + $this->context["enabled_feathers"] = $this->context["disabled_feathers"] = array(); + + if (!$open = @opendir(FEATHERS_DIR)) + return Flash::warning(__("Could not read feathers directory.")); + + while (($folder = readdir($open)) !== false) { + if (!file_exists(FEATHERS_DIR."/".$folder."/".$folder.".php") or !file_exists(FEATHERS_DIR."/".$folder."/info.yaml")) continue; + + if (file_exists(FEATHERS_DIR."/".$folder."/locale/".$config->locale.".mo")) + load_translator($folder, FEATHERS_DIR."/".$folder."/locale/".$config->locale.".mo"); + + $info = YAML::load(FEATHERS_DIR."/".$folder."/info.yaml"); + + fallback($info["name"], $folder); + fallback($info["version"], "0"); + fallback($info["url"]); + fallback($info["description"]); + fallback($info["author"], array("name" => "", "url" => "")); + fallback($info["help"]); + + $info["description"] = __($info["description"], $folder); + $info["description"] = preg_replace("/(.+)<\/code>/se", "''.fix('\\1').''", $info["description"]); + $info["description"] = preg_replace("/
(.+)<\/pre>/se", "'
'.fix('\\1').'
'", $info["description"]); + + $info["author"]["link"] = !empty($info["author"]["url"]) ? + ''.fix($info["author"]["name"]).'' : + $info["author"]["name"] ; + + $category = (feather_enabled($folder)) ? "enabled_feathers" : "disabled_feathers" ; + $this->context[$category][$folder] = array("name" => $info["name"], + "version" => $info["version"], + "url" => $info["url"], + "description" => $info["description"], + "author" => $info["author"], + "help" => $info["help"]); + } + + $this->display("feathers"); + } + + /** + * Function: themes + * Theme switching/previewing. + */ + public function themes() { + $config = Config::current(); + + $this->context["preview"] = !empty($_SESSION['theme']) ? $_SESSION['theme'] : "" ; + + $this->context["themes"] = array(); + + if (!$open = @opendir(THEMES_DIR)) + return Flash::warning(__("Could not read themes directory.")); + + while (($folder = readdir($open)) !== false) { + if (!file_exists(THEMES_DIR."/".$folder."/info.yaml")) + continue; + + if (file_exists(THEMES_DIR."/".$folder."/locale/".$config->locale.".mo")) + load_translator($folder, THEMES_DIR."/".$folder."/locale/".$config->locale.".mo"); + + $info = YAML::load(THEMES_DIR."/".$folder."/info.yaml"); + + fallback($info["name"], $folder); + fallback($info["version"], "0"); + fallback($info["url"]); + fallback($info["description"]); + fallback($info["author"], array("name" => "", "url" => "")); + + $info["author"]["link"] = !empty($info["author"]["url"]) ? + ''.$info["author"]["name"].'' : + $info["author"]["name"] ; + $info["description"] = preg_replace("/(.+)<\/code>/se", + "''.fix('\\1').''", + $info["description"]); + + $info["description"] = preg_replace("/
(.+)<\/pre>/se",
+                                                    "'
'.fix('\\1').'
'", + $info["description"]); + + $this->context["themes"][] = array("name" => $folder, + "screenshot" => (file_exists(THEMES_DIR."/".$folder."/screenshot.png") ? + $config->chyrp_url."/themes/".$folder."/screenshot.png" : + ""), + "info" => $info); + } + + if (!$open = @opendir(ADMIN_THEMES_DIR)) + return Flash::warning(__("Could not read themes directory.")); + + while (($folder = readdir($open)) !== false) { + if (!file_exists(ADMIN_THEMES_DIR."/".$folder."/info.yaml")) + continue; + + if (file_exists(ADMIN_THEMES_DIR."/".$folder."/locale/".$config->locale.".mo")) + load_translator($folder, ADMIN_THEMES_DIR."/".$folder."/locale/".$config->locale.".mo"); + + $info = YAML::load(ADMIN_THEMES_DIR."/".$folder."/info.yaml"); + + fallback($info["name"], $folder); + fallback($info["version"], "0"); + fallback($info["url"]); + fallback($info["description"]); + fallback($info["author"], array("name" => "", "url" => "")); + + $info["author"]["link"] = !empty($info["author"]["url"]) ? + ''.$info["author"]["name"].'' : + $info["author"]["name"] ; + $info["description"] = preg_replace("/(.+)<\/code>/se", + "''.fix('\\1').''", + $info["description"]); + + $info["description"] = preg_replace("/
(.+)<\/pre>/se",
+                                                    "'
'.fix('\\1').'
'", + $info["description"]); + + $this->context["admin_themes"][] = array("name" => $folder, + "screenshot" => (file_exists(ADMIN_THEMES_DIR."/".$folder."/screenshot.png") ? + $config->chyrp_url."/admin/themes/".$folder."/screenshot.png" : + ""), + "info" => $info); + } + + closedir($open); + + $this->display("themes"); + } + + /** + * Function: enable + * Enables a module or feather. + */ + public function enable() { + $config = Config::current(); + $visitor = Visitor::current(); + + $type = (isset($_GET['module'])) ? "module" : "feather" ; + + if (!$visitor->group->can("toggle_extensions")) + if ($type == "module") + show_403(__("Access Denied"), __("You do not have sufficient privileges to enable/disable modules.")); + else + show_403(__("Access Denied"), __("You do not have sufficient privileges to enable/disable feathers.")); + + if ($type == "module" and module_enabled($_GET[$type])) + Flash::warning(__("Module already enabled."), "/admin/?action=modules"); + + if ($type == "feather" and feather_enabled($_GET[$type])) + Flash::warning(__("Feather already enabled."), "/admin/?action=feathers"); + + $enabled_array = ($type == "module") ? "enabled_modules" : "enabled_feathers" ; + $folder = ($type == "module") ? MODULES_DIR : FEATHERS_DIR ; + + require $folder."/".$_GET[$type]."/".$_GET[$type].".php"; + + $class_name = camelize($_GET[$type]); + + if ($type == "module" and !is_subclass_of($class_name, "Modules")) + Flash::warning(__("Item is not a module."), "/admin/?action=modules"); + + if ($type == "feather" and !is_subclass_of($class_name, "Feathers")) + Flash::warning(__("Item is not a feather."), "/admin/?action=feathers"); + + if (method_exists($class_name, "__install")) + call_user_func(array($class_name, "__install")); + + $new = $config->$enabled_array; + array_push($new, $_GET[$type]); + $config->set($enabled_array, $new); + + if (file_exists($folder."/".$_GET[$type]."/locale/".$config->locale.".mo")) + load_translator($_GET[$type], $folder."/".$_GET[$type]."/locale/".$config->locale.".mo"); + + $info = YAML::load($folder."/".$_GET[$type]."/info.yaml"); + fallback($info["uploader"], false); + fallback($info["notifications"], array()); + + foreach ($info["notifications"] as &$notification) + $notification = __($notification, $_GET[$type]); + + if ($info["uploader"]) + if (!file_exists(MAIN_DIR.$config->uploads_path)) + $info["notifications"][] = _f("Please create the %s directory at your Chyrp install's root and CHMOD it to 777.", array($config->uploads_path)); + elseif (!is_writable(MAIN_DIR.$config->uploads_path)) + $info["notifications"][] = _f("Please CHMOD %s to 777.", array($config->uploads_path)); + + foreach ($info["notifications"] as $message) + Flash::message($message); + + if ($type == "module") + Flash::notice(_f("“%s” module enabled.", + array($info["name"])), + "/admin/?action=".pluralize($type)); + elseif ($type == "feather") + Flash::notice(_f("“%s” feather enabled.", + array($info["name"])), + "/admin/?action=".pluralize($type)); + } + + /** + * Function: disable + * Disables a module or feather. + */ + public function disable() { + $config = Config::current(); + $visitor = Visitor::current(); + + $type = (isset($_GET['module'])) ? "module" : "feather" ; + + if (!$visitor->group->can("toggle_extensions")) + if ($type == "module") + show_403(__("Access Denied"), __("You do not have sufficient privileges to enable/disable modules.")); + else + show_403(__("Access Denied"), __("You do not have sufficient privileges to enable/disable feathers.")); + + if ($type == "module" and !module_enabled($_GET[$type])) + Flash::warning(__("Module already disabled."), "/admin/?action=modules"); + + if ($type == "feather" and !feather_enabled($_GET[$type])) + Flash::warning(__("Feather already disabled."), "/admin/?action=feathers"); + + $enabled_array = ($type == "module") ? "enabled_modules" : "enabled_feathers" ; + $folder = ($type == "module") ? MODULES_DIR : FEATHERS_DIR ; + + $class_name = camelize($_GET[$type]); + if (method_exists($class_name, "__uninstall")) + call_user_func(array($class_name, "__uninstall"), false); + + $config->set(($type == "module" ? "enabled_modules" : "enabled_feathers"), + array_diff($config->$enabled_array, array($_GET[$type]))); + + $info = YAML::load($folder."/".$_GET[$type]."/info.yaml"); + if ($type == "module") + Flash::notice(_f("“%s” module disabled.", + array($info["name"])), + "/admin/?action=".pluralize($type)); + elseif ($type == "feather") + Flash::notice(_f("“%s” feather disabled.", + array($info["name"])), + "/admin/?action=".pluralize($type)); + } + + /** + * Function: change_theme + * Changes the theme. + */ + public function change_theme() { + if (!Visitor::current()->group->can("change_settings")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to change settings.")); + if (empty($_GET['theme'])) + error(__("No Theme Specified"), __("You did not specify a theme to switch to.")); + + $config = Config::current(); + + $config->set("theme", $_GET['theme']); + + if (file_exists(THEMES_DIR."/".$_GET['theme']."/locale/".$config->locale.".mo")) + load_translator($_GET['theme'], THEMES_DIR."/".$_GET['theme']."/locale/".$config->locale.".mo"); + + $info = YAML::load(THEMES_DIR."/".$_GET['theme']."/info.yaml"); + fallback($info["notifications"], array()); + + foreach ($info["notifications"] as &$notification) + $notification = __($notification, $_GET['theme']); + + foreach ($info["notifications"] as $message) + Flash::message($message); + + # Clear the caches made by the previous theme. + foreach ((array) glob(INCLUDES_DIR."/caches/*.cache") as $cache) + @unlink($cache); + + Flash::notice(_f("Theme changed to “%s”.", array($info["name"])), "/admin/?action=themes"); + } + + /** + * Function: theme + * Changes the admin theme. + */ + public function change_admin_theme() { + if (empty($_GET['theme'])) + error(__("No Theme Specified"), __("You did not specify a theme to switch to.")); + + $config = Config::current(); + + $_SESSION['admin_theme'] = $_GET['theme']; + + if (file_exists(ADMIN_THEMES_DIR."/".$_GET['theme']."/locale/".$config->locale.".mo")) + load_translator($_GET['theme'], ADMIN_THEMES_DIR."/".$_GET['theme']."/locale/".$config->locale.".mo"); + + $info = YAML::load(ADMIN_THEMES_DIR."/".$_GET['theme']."/info.yaml"); + fallback($info["notifications"], array()); + + foreach ($info["notifications"] as &$notification) + $notification = __($notification, $_GET['theme']); + + foreach ($info["notifications"] as $message) + Flash::message($message); + + # Clear the caches made by the previous theme. + foreach (glob(INCLUDES_DIR."/caches/*.cache") as $cache) + @unlink($cache); + + Flash::notice(_f("Admin theme changed to “%s”.", array($info["name"])), "/admin/?action=themes"); + } + + /** + * Function: preview_theme + * Previews the theme. + */ + public function preview_theme() { + if (!Visitor::current()->group->can("change_settings")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to preview themes.")); + if (empty($_GET['theme'])) + error(__("No Theme Specified"), __("You did not specify a theme to preview.")); + + $info = YAML::load(THEMES_DIR."/".$_GET['theme']."/info.yaml"); + + # Clear the caches made by the previous theme. + foreach (glob(INCLUDES_DIR."/caches/*.cache") as $cache) + @unlink($cache); + + if (!empty($_SESSION['theme'])) { + unset($_SESSION['theme']); + Flash::notice(_f("Stopped previewing “%s”.", array($info["name"])), "/admin/?action=themes"); + } else { + $_SESSION['theme'] = $_GET['theme']; + Flash::notice(_f("Previewing theme “%s”. Press the theme's “Preview” button again to stop previewing.", array($info["name"])), "/"); + } + } + + /** + * Function: general_settings + * General Settings page. + */ + public function general_settings() { + if (!Visitor::current()->group->can("change_settings")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to change settings.")); + + $locales = array(); + + if ($open = opendir(INCLUDES_DIR."/locale/")) { + while (($folder = readdir($open)) !== false) { + $split = explode(".", $folder); + if (end($split) == "mo") + $locales[] = array("code" => $split[0], "name" => lang_code($split[0])); + } + closedir($open); + } + + if (empty($_POST)) + return $this->display("general_settings", + array("locales" => $locales, + "timezones" => timezones())); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + $config = Config::current(); + $set = array($config->set("name", $_POST['name']), + $config->set("description", $_POST['description']), + $config->set("chyrp_url", rtrim($_POST['chyrp_url'], "/")), + $config->set("url", rtrim(oneof($_POST['url'], $_POST['chyrp_url']), "/")), + $config->set("email", $_POST['email']), + $config->set("timezone", $_POST['timezone']), + $config->set("locale", $_POST['locale'])); + + if (!in_array(false, $set)) + Flash::notice(__("Settings updated."), "/admin/?action=general_settings"); + } + + /** + * Function: user_settings + * User Settings page. + */ + public function user_settings() { + if (!Visitor::current()->group->can("change_settings")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to change settings.")); + + if (empty($_POST)) + return $this->display("user_settings", array("groups" => Group::find(array("order" => "id DESC")))); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + $config = Config::current(); + $set = array($config->set("can_register", !empty($_POST['can_register'])), + $config->set("default_group", $_POST['default_group']), + $config->set("guest_group", $_POST['guest_group'])); + + if (!in_array(false, $set)) + Flash::notice(__("Settings updated."), "/admin/?action=user_settings"); + } + + /** + * Function: content_settings + * Content Settings page. + */ + public function content_settings() { + if (!Visitor::current()->group->can("change_settings")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to change settings.")); + + if (empty($_POST)) + return $this->display("content_settings"); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + $config = Config::current(); + $set = array($config->set("posts_per_page", $_POST['posts_per_page']), + $config->set("feed_items", $_POST['feed_items']), + $config->set("feed_url", $_POST['feed_url']), + $config->set("uploads_path", $_POST['uploads_path']), + $config->set("enable_trackbacking", !empty($_POST['enable_trackbacking'])), + $config->set("send_pingbacks", !empty($_POST['send_pingbacks'])), + $config->set("enable_xmlrpc", !empty($_POST['enable_xmlrpc'])), + $config->set("enable_ajax", !empty($_POST['enable_ajax']))); + + if (!in_array(false, $set)) + Flash::notice(__("Settings updated."), "/admin/?action=content_settings"); + } + + /** + * Function: route_settings + * Route Settings page. + */ + public function route_settings() { + if (!Visitor::current()->group->can("change_settings")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to change settings.")); + + if (empty($_POST)) + return $this->display("route_settings"); + + if (!isset($_POST['hash']) or $_POST['hash'] != Config::current()->secure_hashkey) + show_403(__("Access Denied"), __("Invalid security key.")); + + $config = Config::current(); + $set = array($config->set("clean_urls", !empty($_POST['clean_urls'])), + $config->set("post_url", $_POST['post_url'])); + + if (!in_array(false, $set)) + Flash::notice(__("Settings updated."), "/admin/?action=route_settings"); + } + + /** + * Function: help + * Sets the $title and $body for various help IDs. + */ + public function help() { + list($title, $body) = Trigger::current()->call("help_".$_GET['id']); + + switch($_GET['id']) { + case "filtering_results": + $title = __("Filtering Results"); + $body = "

".__("Use this to search for specific items. You can either enter plain text to match the item with, or use keywords:")."

"; + $body.= "

".__("Keywords")."

"; + $body.= "".__("Usage").": attr:val\n".__("Use this syntax to quickly match specific results. Keywords will modify the query to match items where attr is equal to val (case insensitive)."); + break; + case "slugs": + $title = __("Post Slugs"); + $body = __("Post slugs are strings to use for the URL of a post. They are directly respondible for the (url) attribute in a post's clean URL, or the /?action=view&url=foo in a post's dirty URL. A post slug should not contain any special characters other than hyphens."); + break; + case "trackbacks": + $title = __("Trackbacks"); + $body = __("Trackbacks are special urls to posts from other blogs that your post is related to or references. The other blog will be notified of your post, and in some cases a comment will automatically be added to the post in question linking back to your post. It's basically a way to network between blogs via posts."); + break; + case "alternate_urls": + $title = __("Alternate URL"); + $body = "

".__("An alternate URL will allow you to keep Chyrp in its own directory, while having your site URLs point to someplace else. For example, you could have Chyrp in a /chyrp directory, and have your site at /. There are two requirements for this to work.")."

\n\n"; + $body.= "
    \n\t
  1. ".__("Create an index.php file in your destination directory with the following in it:")."\n\n"; + $body.= "
    <?php
    +    require \"path/to/chyrp/index.php\";
    +?>
    "; + $body.= "
  2. \n\t
  3. ".__("Move the .htaccess file from the original Chyrp directory, and change the RewriteBase line to reflect the new website location.")."
  4. \n
"; + } + + require "help.php"; + } + + /** + * Function: subnav_context + * Generates the context variables for the subnav. + */ + public function subnav_context($action) { + $trigger = Trigger::current(); + $visitor = Visitor::current(); + + $this->context["subnav"] = array(); + $subnav =& $this->context["subnav"]; + + $subnav["write"] = array(); + $pages = array("manage" => array()); + + foreach (Config::current()->enabled_feathers as $index => $feather) { + $info = YAML::load(FEATHERS_DIR."/".$feather."/info.yaml"); + $subnav["write"]["write_post&feather=".$feather] = array("title" => __($info["name"], $feather), + "show" => $visitor->group->can("add_draft", "add_post"), + "attributes" => ' id="list_feathers['.$feather.']"', + "selected" => (isset($_GET['feather']) and $_GET['feather'] == $feather) or + (!isset($_GET['feather']) and $action == "write_post" and !$index)); + } + + # Write navs + $subnav["write"]["write_page"] = array("title" => __("Page"), + "show" => $visitor->group->can("add_page")); + $trigger->filter($subnav["write"], array("admin_write_nav", "write_nav")); + $pages["write"] = array_merge(array("write_post"), array_keys($subnav["write"]));; + + # Manage navs + $subnav["manage"] = array("manage_posts" => array("title" => __("Posts"), + "show" => (Post::any_editable() or Post::any_deletable()), + "selected" => array("edit_post", "delete_post")), + "manage_pages" => array("title" => __("Pages"), + "show" => ($visitor->group->can("edit_page", "delete_page")), + "selected" => array("edit_page", "delete_page")), + "manage_users" => array("title" => __("Users"), + "show" => ($visitor->group->can("add_user", + "edit_user", + "delete_user")), + "selected" => array("edit_user", "delete_user", "new_user")), + "manage_groups" => array("title" => __("Groups"), + "show" => ($visitor->group->can("add_group", + "edit_group", + "delete_group")), + "selected" => array("edit_group", "delete_group", "new_group"))); + $trigger->filter($subnav["manage"], "manage_nav"); + + $subnav["manage"]["import"] = array("title" => __("Import"), + "show" => ($visitor->group->can("add_post"))); + $subnav["manage"]["export"] = array("title" => __("Export"), + "show" => ($visitor->group->can("add_post"))); + + $pages["manage"][] = "new_user"; + $pages["manage"][] = "new_group"; + foreach (array_keys($subnav["manage"]) as $manage) + $pages["manage"] = array_merge($pages["manage"], array($manage, + preg_replace("/manage_(.+)/e", + "'edit_'.depluralize('\\1')", + $manage), + preg_replace("/manage_(.+)/e", + "'delete_'.depluralize('\\1')", + $manage))); + + # Settings navs + $subnav["settings"] = array("general_settings" => array("title" => __("General"), + "show" => $visitor->group->can("change_settings")), + "content_settings" => array("title" => __("Content"), + "show" => $visitor->group->can("change_settings")), + "user_settings" => array("title" => __("Users"), + "show" => $visitor->group->can("change_settings")), + "route_settings" => array("title" => __("Routes"), + "show" => $visitor->group->can("change_settings"))); + $trigger->filter($subnav["settings"], "settings_nav"); + $pages["settings"] = array_keys($subnav["settings"]); + + # Extend navs + $subnav["extend"] = array("modules" => array("title" => __("Modules"), + "show" => $visitor->group->can("toggle_extensions")), + "feathers" => array("title" => __("Feathers"), + "show" => $visitor->group->can("toggle_extensions")), + "themes" => array("title" => __("Themes"), + "show" => $visitor->group->can("toggle_extensions"))); + $trigger->filter($subnav["extend"], "extend_nav"); + $pages["extend"] = array_keys($subnav["extend"]); + + foreach (array_keys($subnav) as $main_nav) + foreach ($trigger->filter($pages[$main_nav], $main_nav."_nav_pages") as $extend) + $subnav[$extend] =& $subnav[$main_nav]; + + foreach ($subnav as $main_nav => &$sub_nav) + foreach ($sub_nav as &$nav) + $nav["show"] = (!isset($nav["show"]) or $nav["show"]); + + $trigger->filter($subnav, "admin_subnav"); + } + + /** + * Function: display + * Renders the page. + * + * Parameters: + * $action - The template file to display, in (theme dir)/pages. + * $context - Context for the template. + * $title - The title for the page. Defaults to a camlelization of the action, e.g. foo_bar -> Foo Bar. + */ + public function display($action, $context = array(), $title = "") { + $this->displayed = true; + + fallback($title, camelize($action, true)); + + $this->context = array_merge($context, $this->context); + + $trigger = Trigger::current(); + + $trigger->filter($this->context, array("admin_context", "admin_context_".str_replace("/", "_", $action))); + + # Are there any extension-added pages? + foreach (array("write" => array(), + "manage" => array("import", "export"), + "settings" => array(), + "extend" => array("modules", "feathers", "themes")) as $main_nav => $val) { + $$main_nav = $val; + $trigger->filter($$main_nav, $main_nav."_pages"); + } + + $visitor = Visitor::current(); + $route = Route::current(); + + $this->context["theme"] = Theme::current(); + $this->context["flash"] = Flash::current(); + $this->context["trigger"] = $trigger; + $this->context["title"] = $title; + $this->context["site"] = Config::current(); + $this->context["visitor"] = $visitor; + $this->context["logged_in"] = logged_in(); + $this->context["route"] = $route; + $this->context["hide_admin"] = isset($_SESSION["hide_admin"]); + $this->context["now"] = time(); + $this->context["version"] = CHYRP_VERSION; + $this->context["debug"] = DEBUG; + $this->context["feathers"] = Feathers::$instances; + $this->context["modules"] = Modules::$instances; + $this->context["admin_theme"] = $this->admin_theme; + $this->context["theme_url"] = Config::current()->chyrp_url."/admin/themes/".$this->admin_theme; + $this->context["POST"] = $_POST; + $this->context["GET"] = $_GET; + + $this->context["navigation"] = array(); + + $show = array("write" => array($visitor->group->can("add_draft", "add_post", "add_page")), + "manage" => array($visitor->group->can("view_own_draft", + "view_draft", + "edit_own_draft", + "edit_own_post", + "edit_post", + "delete_own_draft", + "delete_own_post", + "delete_post", + "add_page", + "edit_page", + "delete_page", + "add_user", + "edit_user", + "delete_user", + "add_group", + "edit_group", + "delete_group")), + "settings" => array($visitor->group->can("change_settings")), + "extend" => array($visitor->group->can("toggle_extensions"))); + + foreach ($show as $name => &$arr) + $trigger->filter($arr, $name."_nav_show"); + + $this->context["navigation"]["write"] = array("title" => __("Write"), + "show" => in_array(true, $show["write"]), + "selected" => (in_array($action, $write) or + match("/^write_/", $action))); + + $this->context["navigation"]["manage"] = array("title" => __("Manage"), + "show" => in_array(true, $show["manage"]), + "selected" => (in_array($action, $manage) or + match(array("/^manage_/", + "/^edit_/", + "/^delete_/", + "/^new_/"), $action))); + + $this->context["navigation"]["settings"] = array("title" => __("Settings"), + "show" => in_array(true, $show["settings"]), + "selected" => (in_array($action, $settings) or + match("/_settings$/", $action))); + + $this->context["navigation"]["extend"] = array("title" => __("Extend"), + "show" => in_array(true, $show["extend"]), + "selected" => (in_array($action, $extend))); + + $this->subnav_context($route->action); + + $trigger->filter($this->context["selected"], "nav_selected"); + + $this->context["sql_debug"] = SQL::current()->debug; + + $file = MAIN_DIR."/admin/themes/%s/pages/".$action.".twig"; + $template = file_exists(sprintf($file, $this->admin_theme)) ? + sprintf($file, $this->admin_theme) : + sprintf($file, "default"); + + $config = Config::current(); + if (!file_exists($template)) { + foreach (array(MODULES_DIR => $config->enabled_modules, + FEATHERS_DIR => $config->enabled_feathers) as $path => $try) + foreach ($try as $extension) + if (file_exists($path."/".$extension."/pages/admin/".$action.".twig")) + $template = $path."/".$extension."/pages/admin/".$action.".twig"; + + if (!file_exists($template)) + error(__("Template Missing"), _f("Couldn't load template: %s", array($template))); + } + + # Try the theme first + try { + $this->theme->getTemplate($template)->display($this->context); + } catch (Exception $t) { + # Fallback to the default + try { + $this->default->getTemplate($template)->display($this->context); + } catch (Exception $e) { + $prettify = preg_replace("/([^:]+): (.+)/", "\\1: \\2", $e->getMessage()); + $trace = debug_backtrace(); + $twig = array("file" => $e->filename, "line" => $e->lineno); + array_unshift($trace, $twig); + error(__("Error"), $prettify, $trace); + } + } + } + + /** + * Function: current + * Returns a singleton reference to the current class. + */ + public static function & current() { + static $instance = null; + return $instance = (empty($instance)) ? new self() : $instance ; + } + } diff --git a/News/includes/controller/Main.php b/News/includes/controller/Main.php new file mode 100755 index 0000000..fc3a55a --- /dev/null +++ b/News/includes/controller/Main.php @@ -0,0 +1,767 @@ + dirty URL translations. + public $urls = array('|/id/([0-9]+)/|' => '/?action=view&id=$1', + '|/page/(([^/]+)/)+|' => '/?action=page&url=$2', + '|/search/|' => '/?action=search', + '|/search/([^/]+)/|' => '/?action=search&query=$1', + '|/archive/([0-9]{4})/([0-9]{2})/|' + => '/?action=archive&year=$1&month=$2', + '|/archive/([0-9]{4})/([0-9]{2})/([0-9]{2})/|' + => '/?action=archive&year=$1&month=$2&day=$3', + '|/([^/]+)/feed/([^/]+)/|' => '/?action=$1&feed&title=$2', + '|/([^/]+)/feed/|' => '/?action=$1&feed'); + + # Boolean: $displayed + # Has anything been displayed? + public $displayed = false; + + # Array: $context + # Context for displaying pages. + public $context = array(); + + # Boolean: $feed + # Is the visitor requesting a feed? + public $feed = false; + + /** + * Function: __construct + * Loads the Twig parser into , and sets up the theme l10n domain. + */ + private function __construct() { + $this->feed = (isset($_GET['feed']) or (isset($_GET['action']) and $_GET['action'] == "feed")); + $this->post_limit = Config::current()->posts_per_page; + + $cache = (is_writable(INCLUDES_DIR."/caches") and + !DEBUG and + !PREVIEWING and + !defined('CACHE_TWIG') or CACHE_TWIG); + + if (defined('THEME_DIR')) + $this->twig = new Twig_Loader(THEME_DIR, + $cache ? + INCLUDES_DIR."/caches" : + null) ; + } + + /** + * Function: parse + * Determines the action. + */ + public function parse($route) { + $config = Config::current(); + + if (empty($route->arg[0]) and !isset($config->routes["/"])) # If they're just at /, don't bother with all this. + return $route->action = "index"; + + # Protect non-responder functions. + if (in_array($route->arg[0], array("__construct", "parse", "post_from_url", "display", "current"))) + show_404(); + + # Feed + if (preg_match("/\/feed\/?$/", $route->request)) { + $this->feed = true; + $this->post_limit = $config->feed_items; + + if ($route->arg[0] == "feed") # Don't set $route->action to "feed" (bottom of this function). + return $route->action = "index"; + } + + # Feed with a title parameter + if (preg_match("/\/feed\/([^\/]+)\/?$/", $route->request, $title)) { + $this->feed = true; + $this->post_limit = $config->feed_items; + $_GET['title'] = $title[1]; + + if ($route->arg[0] == "feed") # Don't set $route->action to "feed" (bottom of this function). + return $route->action = "index"; + } + + # Paginator + if (preg_match_all("/\/((([^_\/]+)_)?page)\/([0-9]+)/", $route->request, $page_matches)) { + foreach ($page_matches[1] as $key => $page_var) + $_GET[$page_var] = (int) $page_matches[4][$key]; + + if ($route->arg[0] == $page_matches[1][0]) # Don't fool ourselves into thinking we're viewing a page. + return $route->action = (isset($config->routes["/"])) ? $config->routes["/"] : "index" ; + } + + # Viewing a post by its ID + if ($route->arg[0] == "id") { + $_GET['id'] = $route->arg[1]; + return $route->action = "id"; + } + + # Archive + if ($route->arg[0] == "archive") { + # Make sure they're numeric; there might be a /page/ in there. + if (isset($route->arg[1]) and is_numeric($route->arg[1])) + $_GET['year'] = $route->arg[1]; + if (isset($route->arg[2]) and is_numeric($route->arg[2])) + $_GET['month'] = $route->arg[2]; + if (isset($route->arg[3]) and is_numeric($route->arg[3])) + $_GET['day'] = $route->arg[3]; + + return $route->action = "archive"; + } + + # Searching + if ($route->arg[0] == "search") { + if (isset($route->arg[1])) + $_GET['query'] = $route->arg[1]; + + return $route->action = "search"; + } + + # Custom pages added by Modules, Feathers, Themes, etc. + foreach ($config->routes as $path => $action) { + if (is_numeric($action)) + $action = $route->arg[0]; + + preg_match_all("/\(([^\)]+)\)/", $path, $matches); + + if ($path != "/") + $path = trim($path, "/"); + + $escape = preg_quote($path, "/"); + $to_regexp = preg_replace("/\\\\\(([^\)]+)\\\\\)/", "([^\/]+)", $escape); + + if ($path == "/") + $to_regexp = "\$"; + + if (preg_match("/^\/{$to_regexp}/", $route->request, $url_matches)) { + array_shift($url_matches); + + if (isset($matches[1])) + foreach ($matches[1] as $index => $parameter) + $_GET[$parameter] = urldecode($url_matches[$index]); + + $params = explode(";", $action); + $action = $params[0]; + + array_shift($params); + foreach ($params as $param) { + $split = explode("=", $param); + $_GET[$split[0]] = oneof(@$split[1], ""); + } + + $route->action = $action; + } + } + + # Are we viewing a post? + $this->post_from_url($route, $route->request); + + # Try viewing a page. + $route->try["page"] = array($route->arg); + } + + /** + * Function: post_from_url + * Check to see if we're viewing a post, and if it is, handle it. + * + * This can also be used for grabbing a Post from a given URL. + * + * Parameters: + * $route - The route to respond to. + * $url - If this argument is passed, it will attempt to grab a post from a given URL. + * If a post is found by that URL, it will be returned. + * $return_post - Return a Post? + */ + public function post_from_url($route, $request, $return_post = false) { + $config = Config::current(); + + $post_url = $config->post_url; + + foreach (explode("/", $post_url) as $path) + foreach (preg_split("/\(([^\)]+)\)/", $path) as $leftover) { + $request = preg_replace("/".preg_quote($leftover)."/", "", $request, 1); + $post_url = preg_replace("/".preg_quote($leftover)."/", "", $post_url, 1); + } + + $args = array_map("urldecode", explode("/", trim($request, "/"))); + + $post_url = $this->key_regexp(rtrim($post_url, "/")); + $post_url_attrs = array(); + preg_match_all("/\(([^\/]+)\)/", $config->post_url, $parameters); + if (preg_match("/".$post_url."/", rtrim($request, "/"), $matches)) { + array_shift($matches); + + foreach ($parameters[0] as $index => $parameter) + if ($parameter[0] == "(") { + if ($parameter == "(id)") { + $post_url_attrs = array("id" => $args[$index]); + break; + } else + $post_url_attrs[rtrim(ltrim($parameter, "("), ")")] = $args[$index]; + } + + if ($return_post) + return Post::from_url($post_url_attrs); + else + $route->try["view"] = array($post_url_attrs, $args); + } + } + + /** + * Function: key_regexp + * Converts the values in $config->post_url to regular expressions. + * + * Parameters: + * $key - Input URL with the keys from . + * + * Returns: + * $key values replaced with their regular expressions from $code>. + */ + private function key_regexp($key) { + Trigger::current()->filter(Post::$url_attrs, "url_code"); + return str_replace(array_keys(Post::$url_attrs), array_values(Post::$url_attrs), str_replace("/", "\\/", $key)); + } + + /** + * Function: index + * Grabs the posts for the main page. + */ + public function index() { + $this->display("pages/index", + array("posts" => new Paginator(Post::find(array("placeholders" => true)), + $this->post_limit))); + } + + /** + * Function: archive + * Grabs the posts for the Archive page when viewing a year or a month. + */ + public function archive() { + fallback($_GET['year']); + fallback($_GET['month']); + fallback($_GET['day']); + + if (isset($_GET['year']) and isset($_GET['month']) and isset($_GET['day'])) + $posts = new Paginator(Post::find(array("placeholders" => true, + "where" => array("YEAR(created_at)" => $_GET['year'], + "MONTH(created_at)" => $_GET['month'], + "DAY(created_at)" => $_GET['day']))), + $this->post_limit); + elseif (isset($_GET['year']) and isset($_GET['month'])) + $posts = new Paginator(Post::find(array("placeholders" => true, + "where" => array("YEAR(created_at)" => $_GET['year'], + "MONTH(created_at)" => $_GET['month']))), + $this->post_limit); + + $sql = SQL::current(); + + if (empty($_GET['year']) or empty($_GET['month'])) { + if (!empty($_GET['year'])) + $timestamps = $sql->select("posts", + array("DISTINCT YEAR(created_at) AS year", + "MONTH(created_at) AS month", + "created_at AS created_at", + "id"), + array("YEAR(created_at)" => $_GET['year']), + array("created_at DESC", "id DESC"), + array(), + null, + null, + array("YEAR(created_at)", "MONTH(created_at)", "created_at", "id")); + else + $timestamps = $sql->select("posts", + array("DISTINCT YEAR(created_at) AS year", + "MONTH(created_at) AS month", + "created_at AS created_at", + "id"), + null, + array("created_at DESC", "id DESC"), + array(), + null, + null, + array("YEAR(created_at)", "MONTH(created_at)", "created_at", "id")); + + $archives = array(); + $archive_hierarchy = array(); + while ($time = $timestamps->fetchObject()) { + $year = mktime(0, 0, 0, 1, 0, $time->year); + $month = mktime(0, 0, 0, $time->month + 1, 0, $time->year); + + $posts = Post::find(array("where" => array("YEAR(created_at)" => when("Y", $time->created_at), + "MONTH(created_at)" => when("m", $time->created_at)))); + + $archives[$month] = array("posts" => $posts, + "year" => $time->year, + "month" => strftime("%B", $month), + "timestamp" => $month, + "url" => url("archive/".when("Y/m/", $time->created_at))); + + $archive_hierarchy[$year][$month] = $posts; + } + + $this->display("pages/archive", + array("archives" => $archives, + "archive_hierarchy" => $archive_hierarchy), + __("Archive")); + } else { + if (!is_numeric($_GET['year']) or !is_numeric($_GET['month'])) + error(__("Error"), __("Please enter a valid year and month.")); + + $timestamp = mktime(0, 0, 0, $_GET['month'], oneof(@$_GET['day'], 1), $_GET['year']); + $depth = isset($_GET['day']) ? "day" : (isset($_GET['month']) ? "month" : (isset($_GET['year']) ? "year" : "")); + + $this->display("pages/archive", + array("posts" => $posts, + "archive" => array("year" => $_GET['year'], + "month" => strftime("%B", $timestamp), + "day" => strftime("%d", $timestamp), + "timestamp" => $timestamp, + "depth" => $depth)), + _f("Archive of %s", array(strftime("%B %Y", $timestamp)))); + } + } + + /** + * Function: search + * Grabs the posts for a search query. + */ + public function search() { + fallback($_GET['query'], ""); + $config = Config::current(); + + if ($config->clean_urls and + substr_count($_SERVER['REQUEST_URI'], "?") and + !substr_count($_SERVER['REQUEST_URI'], "%2F")) # Searches with / and clean URLs = server 404 + redirect("search/".urlencode($_GET['query'])."/"); + + if (empty($_GET['query'])) + return Flash::warning(__("Please enter a search term.")); + + list($where, $params) = keywords($_GET['query'], "post_attributes.value LIKE :query OR url LIKE :query", "posts"); + + $results = Post::find(array("placeholders" => true, + "where" => $where, + "params" => $params)); + + $ids = array(); + foreach ($results[0] as $result) + $ids[] = $result["id"]; + + if (!empty($ids)) + $posts = new Paginator(Post::find(array("placeholders" => true, + "where" => array("id" => $ids))), + $this->post_limit); + else + $posts = new Paginator(array()); + + $this->display(array("pages/search", "pages/index"), + array("posts" => $posts, + "search" => $_GET['query']), + fix(_f("Search results for \"%s\"", array($_GET['query'])))); + } + + /** + * Function: drafts + * Grabs the posts for viewing the Drafts lists. + */ + public function drafts() { + $visitor = Visitor::current(); + + if (!$visitor->group->can("view_own_draft", "view_draft")) + show_403(__("Access Denied"), __("You do not have sufficient privileges to view drafts.")); + + $posts = new Paginator(Post::find(array("placeholders" => true, + "where" => array("status" => "draft", + "user_id" => $visitor->id))), + $this->post_limit); + + $this->display(array("pages/drafts", "pages/index"), + array("posts" => $posts), + __("Drafts")); + } + + /** + * Function: view + * Views a post. + */ + public function view($attrs = null, $args = array()) { + if (isset($attrs)) + $post = Post::from_url($attrs, array("drafts" => true)); + else + $post = new Post(array("url" => @$_GET['url']), array("drafts" => true)); + + if ($post->no_results) + return false; + + if ((oneof(@$attrs["url"], @$attrs["clean"]) == "feed") and # do some checking to see if they're trying + (count(explode("/", trim($post_url, "/"))) > count($args) or # to view the post or the post's feed. + end($args) != "feed")) + $this->feed = false; + + if (!$post->theme_exists()) + error(__("Error"), __("The feather theme file for this post does not exist. The post cannot be displayed.")); + + if ($post->status == "draft") + Flash::message(__("This post is a draft.")); + + if ($post->groups() and !substr_count($post->status, "{".Visitor::current()->group->id."}")) + Flash::message(_f("This post is only visible by the following groups: %s.", $post->groups())); + + $this->display(array("pages/view", "pages/index"), + array("post" => $post, "posts" => array($post)), + $post->title()); + } + + /** + * Function: page + * Handles page viewing. + */ + public function page($urls = null) { + if (isset($urls)) { # Viewing with clean URLs, e.g. /parent/child/child-of-child/ + $valids = Page::find(array("where" => array("url" => $urls))); + + if (count($valids) == count($urls)) { # Make sure all page slugs are valid. + foreach ($valids as $page) + if ($page->url == end($urls)) # Loop until we reach the last one. + break; + } else + return false; # A "link in the chain" is broken + } else + $page = new Page(array("url" => $_GET['url'])); + + if ($page->no_results) + return false; # Page not found; the 404 handling is handled externally. + + $this->display(array("pages/page", "pages/".$page->url), array("page" => $page), $page->title); + } + + /** + * Function: rss + * Redirects to /feed (backwards compatibility). + */ + public function rss() { + header("HTTP/1.1 301 Moved Permanently"); + redirect(oneof(@Config::current()->feed_url, url("feed"))); + } + + /** + * Function: id + * Views a post by its static ID. + */ + public function id() { + $post = new Post($_GET['id']); + redirect($post->url()); + } + + /** + * Function: toggle_admin + * Toggles the Admin control panel (if available). + */ + public function toggle_admin() { + if (!isset($_SESSION['hide_admin'])) + $_SESSION['hide_admin'] = true; + else + unset($_SESSION['hide_admin']); + + redirect("/"); + } + + /** + * Function: register + * Process registration. If registration is disabled or if the user is already logged in, it will error. + */ + public function register() { + $config = Config::current(); + if (!$config->can_register) + error(__("Registration Disabled"), __("I'm sorry, but this site is not allowing registration.")); + + if (logged_in()) + error(__("Error"), __("You're already logged in.")); + + if (!empty($_POST)) { + $route = Route::current(); + + if (empty($_POST['login'])) + Flash::warning(__("Please enter a username for your account.")); + elseif (count(User::find(array("where" => array("login" => $_POST['login']))))) + Flash::warning(__("That username is already in use.")); + + if (empty($_POST['password1']) and empty($_POST['password2'])) + Flash::warning(__("Password cannot be blank.")); + elseif ($_POST['password1'] != $_POST['password2']) + Flash::warning(__("Passwords do not match.")); + + if (empty($_POST['email'])) + Flash::warning(__("E-mail address cannot be blank.")); + elseif (!preg_match("/^[_A-z0-9-]+((\.|\+)[_A-z0-9-]+)*@[A-z0-9-]+(\.[A-z0-9-]+)*(\.[A-z]{2,4})$/", $_POST['email'])) + Flash::warning(__("Invalid e-mail address.")); + + if (!Flash::exists("warning")) { + $user = User::add($_POST['login'], $_POST['password1'], $_POST['email']); + + Trigger::current()->call("user_registered", $user); + + $_SESSION['user_id'] = $user->id; + + Flash::notice(__("Registration successful."), "/"); + } + } + + $this->display("forms/user/register", array(), __("Register")); + } + + /** + * Function: login + * Process logging in. If the username and password are incorrect or if the user is already logged in, it will error. + */ + public function login() { + if (logged_in()) + error(__("Error"), __("You're already logged in.")); + + if (!empty($_POST)) { + fallback($_POST['login']); + fallback($_POST['password']); + + $trigger = Trigger::current(); + + if ($trigger->exists("authenticate")) + return $trigger->call("authenticate"); + + if (!User::authenticate($_POST['login'], $_POST['password'])) + if (!count(User::find(array("where" => array("login" => $_POST['login']))))) + Flash::warning(__("There is no user with that login name.")); + else + Flash::warning(__("Password incorrect.")); + + if (!Flash::exists("warning")) { + $user = new User(array("login" => $_POST['login'])); + $_SESSION['user_id'] = $user->id; + + $redirect = $_SESSION['redirect_to']; + unset($_SESSION['redirect_to']); + + Flash::notice(__("Logged in."), oneof($redirect, "/")); + } + } + + $this->display("forms/user/login", array(), __("Log In")); + } + + /** + * Function: logout + * Logs the current user out. If they are not logged in, it will error. + */ + public function logout() { + if (!logged_in()) + error(__("Error"), __("You aren't logged in.")); + + session_destroy(); + + session(); + + Flash::notice(__("Logged out."), "/"); + } + + /** + * Function: controls + * Updates the current user when the form is submitted. Shows an error if they aren't logged in. + */ + public function controls() { + if (!logged_in()) + error(__("Error"), __("You must be logged in to access this area.")); + + if (!empty($_POST)) { + $visitor = Visitor::current(); + + $password = (!empty($_POST['new_password1']) and $_POST['new_password1'] == $_POST['new_password2']) ? + User::hashPassword($_POST['new_password1']) : + $visitor->password ; + + $visitor->update($visitor->login, + $password, + $_POST['email'], + $_POST['full_name'], + $_POST['website'], + $visitor->group->id); + + Flash::notice(__("Your profile has been updated."), "/"); + } + + $this->display("forms/user/controls", array(), __("Controls")); + } + + /** + * Function: lost_password + * Handles e-mailing lost passwords to a user's email address. + */ + public function lost_password() { + if (!empty($_POST)) { + $user = new User(array("login" => $_POST['login'])); + if ($user->no_results) { + Flash::warning(__("Invalid user specified.")); + return $this->display("forms/user/lost_password", array(), __("Lost Password")); + } + + $new_password = random(16); + + $user->update($user->login, + User::hashPassword($new_password), + $user->email, + $user->full_name, + $user->website, + $user->group_id); + + $sent = email($user->email, + __("Lost Password Request"), + _f("%s,\n\nWe have received a request for a new password for your account at %s.\n\nPlease log in with the following password, and feel free to change it once you've successfully logged in:\n\t%s", + array($user->login, Config::current()->name, $new_password))); + + if ($sent) + Flash::notice(_f("An e-mail has been sent to your e-mail address that contains a new password. Once you have logged in, you can change it at User Controls.", + array(url("controls")))); + else { + # Set their password back to what it was originally. + $user->update($user->login, + $user->password, + $user->email, + $user->full_name, + $user->website, + $user->group_id); + + Flash::warning(__("E-Mail could not be sent. Password change cancelled.")); + } + } + + $this->display("forms/user/lost_password", array(), __("Lost Password")); + } + + /** + * Function: feed + * Grabs posts for the feed. + */ + public function feed($posts = null) { + fallback($posts, Post::find(array("limit" => Config::current()->feed_items))); + + header("Content-Type: application/atom+xml; charset=UTF-8"); + + if (!is_array($posts)) + $posts = $posts->paginated; + + $latest_timestamp = 0; + foreach ($posts as $post) + if (strtotime($post->created_at) > $latest_timestamp) + $latest_timestamp = strtotime($post->created_at); + + require INCLUDES_DIR."/feed.php"; + } + + /** + * Function: display + * Display the page. + * + * If "posts" is in the context and the visitor requested a feed, they will be served. + * + * Parameters: + * $file - The theme file to display. + * $context - The context for the file. + * $title - The title for the page. + */ + public function display($file, $context = array(), $title = "") { + if (is_array($file)) + for ($i = 0; $i < count($file); $i++) { + $check = ($file[$i][0] == "/" or preg_match("/[a-zA-Z]:\\\/", $file[$i])) ? + $file[$i] : + THEME_DIR."/".$file[$i] ; + + if (file_exists($check.".twig") or ($i + 1) == count($file)) + return $this->display($file[$i], $context, $title); + } + + $this->displayed = true; + + $route = Route::current(); + $trigger = Trigger::current(); + + # Serve feeds. + if ($this->feed) { + if ($trigger->exists($route->action."_feed")) + return $trigger->call($route->action."_feed", $context); + + if (isset($context["posts"])) + return $this->feed($context["posts"]); + } + + $this->context = array_merge($context, $this->context); + + $visitor = Visitor::current(); + $config = Config::current(); + $theme = Theme::current(); + + $theme->title = $title; + + $this->context["theme"] = $theme; + $this->context["flash"] = Flash::current(); + $this->context["trigger"] = $trigger; + $this->context["modules"] = Modules::$instances; + $this->context["feathers"] = Feathers::$instances; + $this->context["title"] = $title; + $this->context["site"] = $config; + $this->context["visitor"] = $visitor; + $this->context["route"] = Route::current(); + $this->context["hide_admin"] = isset($_COOKIE["hide_admin"]); + $this->context["version"] = CHYRP_VERSION; + $this->context["now"] = time(); + $this->context["debug"] = DEBUG; + $this->context["POST"] = $_POST; + $this->context["GET"] = $_GET; + $this->context["sql_queries"] =& SQL::current()->queries; + + $this->context["visitor"]->logged_in = logged_in(); + + $this->context["enabled_modules"] = array(); + foreach ($config->enabled_modules as $module) + $this->context["enabled_modules"][$module] = true; + + $context["enabled_feathers"] = array(); + foreach ($config->enabled_feathers as $feather) + $this->context["enabled_feathers"][$feather] = true; + + $this->context["sql_debug"] =& SQL::current()->debug; + + $trigger->filter($this->context, array("main_context", "main_context_".str_replace("/", "_", $file))); + + $file = ($file[0] == "/" or preg_match("/[a-zA-Z]:\\\/", $file)) ? $file : THEME_DIR."/".$file ; + if (!file_exists($file.".twig")) + error(__("Template Missing"), _f("Couldn't load template: %s", array($file.".twig"))); + + try { + return $this->twig->getTemplate($file.".twig")->display($this->context); + } catch (Exception $e) { + $prettify = preg_replace("/([^:]+): (.+)/", "\\1: \\2", $e->getMessage()); + $trace = debug_backtrace(); + $twig = array("file" => $e->filename, "line" => $e->lineno); + array_unshift($trace, $twig); + error(__("Error"), $prettify, $trace); + } + } + + /** + * Function: resort + * Queue a failpage in the event that none of the routes are successful. + */ + public function resort($file, $context, $title = null) { + $this->fallback = array($file, $context, $title); + return false; + } + + /** + * Function: current + * Returns a singleton reference to the current class. + */ + public static function & current() { + static $instance = null; + return $instance = (empty($instance)) ? new self() : $instance ; + } + } + diff --git a/News/includes/error.php b/News/includes/error.php new file mode 100755 index 0000000..5391939 --- /dev/null +++ b/News/includes/error.php @@ -0,0 +1,154 @@ +url."/includes/lib/gz.php?file=jquery.js" : + "http://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js" ; + + if (!class_exists("MainController")) + require INCLUDES_DIR."/controller/Main.php"; + + if (class_exists("Route")) + Route::current(MainController::current()); +?> + + + + + Chyrp: <?php echo $title; ?> + + + + + +
+

+
+ + +

+
    + +
  1. + +
+ +
+ + " class="big login"> + +
+
+
+ + + + + diff --git a/News/includes/feed.php b/News/includes/feed.php new file mode 100755 index 0000000..91eb8dc --- /dev/null +++ b/News/includes/feed.php @@ -0,0 +1,57 @@ +\r"; +?> + + <?php echo $config->name.$title; ?> +description)): ?> + description); ?> + + + + + + Chyrp +updated) ? $post->updated_at : $post->created_at ; + + $tagged = substr(strstr(url("id/".$post->id), "//"), 2); + $tagged = str_replace("#", "/", $tagged); + $tagged = preg_replace("/(".preg_quote(parse_url($post->url(), PHP_URL_HOST)).")/", + "\\1,".when("Y-m-d", $updated).":", + $tagged, + 1); + + $url = $post->url(); + $title = $post->title(); + + $trigger->filter($url, "feed_url", $post); + + if (!$post->user->no_results) + $author = oneof($post->user->full_name, $post->user->login); + else + $author = __("Guest"); +?> + + <?php echo fix(oneof($title, ucfirst($post->feather))); ?> + tag: + + created_at); ?> + + + +user->website)): ?> + user->website); ?> + + + feed_content()); ?> +call("feed_item", $post); ?> + + diff --git a/News/includes/helpers.php b/News/includes/helpers.php new file mode 100755 index 0000000..61e7285 --- /dev/null +++ b/News/includes/helpers.php @@ -0,0 +1,1632 @@ + &$trace) + if (!isset($trace["file"]) or !isset($trace["line"])) + unset($backtrace[$index]); + else + $trace["file"] = str_replace(MAIN_DIR."/", "", $trace["file"]); + # $trace["file"] = isset($trace["file"]) ? + # : + # (isset($trace["function"]) ? + # (isset($trace["class"]) ? + # $trace["class"].$trace["type"] : + # "").$trace["function"] : + # "[internal]"); + + # Clear all output sent before this error. + if (($buffer = ob_get_contents()) !== false) { + ob_end_clean(); + + # Since the header might already be set to gzip, start output buffering again. + if (extension_loaded("zlib") and !ini_get("zlib.output_compression") and + isset($_SERVER['HTTP_ACCEPT_ENCODING']) and + substr_count($_SERVER['HTTP_ACCEPT_ENCODING'], "gzip") and + USE_ZLIB) { + ob_start("ob_gzhandler"); + header("Content-Encoding: gzip"); + } else + ob_start(); + } elseif (!UPGRADING) { + # If output buffering is not started, assume this + # is sent from the Session class or somewhere deep. + error_log($title.": ".$body); + + foreach ($backtrace as $index => $trace) + error_log(" ".($index + 1).": "._f("%s on line %d", array($trace["file"], $trace["line"]))); + + exit; + } + + if (TESTER) + exit("ERROR: ".$body); + + if ($title == __("Access Denied")) + $_SESSION['redirect_to'] = self_url(); + + # Display the error. + if (defined('THEME_DIR') and class_exists("Theme") and Theme::current()->file_exists("pages/error")) + MainController::current()->display("pages/error", + array("title" => $title, + "body" => $body, + "backtrace" => $backtrace)); + else + require INCLUDES_DIR."/error.php"; + + if ($buffer !== false) + ob_end_flush(); + + exit; + } + + /** + * Function: show_403 + * Shows an error message with a 403 HTTP header. + * + * Parameters: + * $title - The title for the error dialog. + * $body - The message for the error dialog. + */ + function show_403($title, $body) { + header("Status: 403"); + error($title, $body); + } + + /** + * Function: logged_in + * Returns whether or not they are logged in by returning the (which defaults to 0). + */ + function logged_in() { + return (class_exists("Visitor") and isset(Visitor::current()->id) and Visitor::current()->id != 0); + } + + /** + * Function: load_translator + * Loads a .mo file for gettext translation. + * + * Parameters: + * $domain - The name for this translation domain. + * $mofile - The .mo file to read from. + */ + function load_translator($domain, $mofile) { + global $l10n; + + if (isset($l10n[$domain])) + return; + + if (is_readable($mofile)) + $input = new CachedFileReader($mofile); + else + return; + + $l10n[$domain] = new gettext_reader($input); + } + + /** + * Function: __ + * Returns a translated string. + * + * Parameters: + * $text - The string to translate. + * $domain - The translation domain to read from. + */ + function __($text, $domain = "chyrp") { + global $l10n; + return (isset($l10n[$domain])) ? $l10n[$domain]->translate($text) : $text ; + } + + /** + * Function: _p + * Returns a plural (or not) form of a translated string. + * + * Parameters: + * $single - Singular string. + * $plural - Pluralized string. + * $number - The number to judge by. + * $domain - The translation domain to read from. + */ + function _p($single, $plural, $number, $domain = "chyrp") { + global $l10n; + return isset($l10n[$domain]) ? + $l10n[$domain]->ngettext($single, $plural, $number) : + (($number != 1) ? $plural : $single) ; + } + + /** + * Function: _f + * Returns a formatted translated string. + * + * Parameters: + * $string - String to translate and format. + * $args - One arg or an array of arguments to format with. + * $domain - The translation domain to read from. + */ + function _f($string, $args = array(), $domain = "chyrp") { + $args = (array) $args; + array_unshift($args, __($string, $domain)); + return call_user_func_array("sprintf", $args); + } + + /** + * Function: redirect + * Redirects to the given URL and exits immediately. + * + * Parameters: + * $url - The URL to redirect to. If it begins with @/@ it will be relative to the @Config.chyrp_url@. + * $use_chyrp_url - Use the @Config.chyrp_url@ instead of @Config.url@ for $urls beginning with @/@? + */ + function redirect($url, $use_chyrp_url = false) { + # Handle URIs without domain + if ($url[0] == "/") + $url = (ADMIN or $use_chyrp_url) ? + Config::current()->chyrp_url.$url : + Config::current()->url.$url ; + elseif (file_exists(INCLUDES_DIR."/config.yaml.php") and class_exists("Route") and !substr_count($url, "://")) + $url = url($url); + + header("Location: ".html_entity_decode($url)); + exit; + } + + /** + * Function: url + * Mask for Route->url(). + */ + function url($url, $controller = null) { + return Route::current()->url($url, $controller); + } + + /** + * Function: pluralize + * Returns a pluralized string. This is a port of Rails's pluralizer. + * + * Parameters: + * $string - The string to pluralize. + * $number - If passed, and this number is 1, it will not pluralize. + */ + function pluralize($string, $number = null) { + $uncountable = array("moose", "sheep", "fish", "series", "species", + "rice", "money", "information", "equipment", "piss"); + + if (in_array($string, $uncountable) or $number == 1) + return $string; + + $replacements = array("/person/i" => "people", + "/man/i" => "men", + "/child/i" => "children", + "/cow/i" => "kine", + "/goose/i" => "geese", + "/(penis)$/i" => "\\1es", # Take that, Rails! + "/(ax|test)is$/i" => "\\1es", + "/(octop|vir)us$/i" => "\\1ii", + "/(cact)us$/i" => "\\1i", + "/(alias|status)$/i" => "\\1es", + "/(bu)s$/i" => "\\1ses", + "/(buffal|tomat)o$/i" => "\\1oes", + "/([ti])um$/i" => "\\1a", + "/sis$/i" => "ses", + "/(hive)$/i" => "\\1s", + "/([^aeiouy]|qu)y$/i" => "\\1ies", + "/^(ox)$/i" => "\\1en", + "/(matr|vert|ind)(?:ix|ex)$/i" => "\\1ices", + "/(x|ch|ss|sh)$/i" => "\\1es", + "/([m|l])ouse$/i" => "\\1ice", + "/(quiz)$/i" => "\\1zes"); + + $replaced = preg_replace(array_keys($replacements), array_values($replacements), $string, 1); + + if ($replaced == $string) + return $string."s"; + else + return $replaced; + } + + /** + * Function: depluralize + * Returns a depluralized string. This is the inverse of . + * + * Parameters: + * $string - The string to depluralize. + * $number - If passed, and this number is not 1, it will not depluralize. + */ + function depluralize($string, $number = null) { + if (isset($number) and $number != 1) + return $string; + + $replacements = array("/people/i" => "person", + "/^men/i" => "man", + "/children/i" => "child", + "/kine/i" => "cow", + "/geese/i" => "goose", + "/(penis)es$/i" => "\\1", + "/(ax|test)es$/i" => "\\1is", + "/(octopi|viri|cact)i$/i" => "\\1us", + "/(alias|status)es$/i" => "\\1", + "/(bu)ses$/i" => "\\1s", + "/(buffal|tomat)oes$/i" => "\\1o", + "/([ti])a$/i" => "\\1um", + "/ses$/i" => "sis", + "/(hive)s$/i" => "\\1", + "/([^aeiouy]|qu)ies$/i" => "\\1y", + "/^(ox)en$/i" => "\\1", + "/(vert|ind)ices$/i" => "\\1ex", + "/(matr)ices$/i" => "\\1ix", + "/(x|ch|ss|sh)es$/i" => "\\1", + "/([ml])ice$/i" => "\\1ouse", + "/(quiz)zes$/i" => "\\1"); + + $replaced = preg_replace(array_keys($replacements), array_values($replacements), $string, 1); + + if ($replaced == $string and substr($string, -1) == "s") + return substr($string, 0, -1); + else + return $replaced; + } + + /** + * Function: truncate + * Truncates a string to the given length, optionally taking into account HTML tags, and/or keeping words in tact. + * + * Parameters: + * $text - String to shorten. + * $length - Length to truncate to. + * $ending - What to place at the end, e.g. "...". + * $exact - Break words? + * $html - Auto-close cut-off HTML tags? + * + * Author: + * CakePHP team, code style modified. + */ + function truncate($text, $length = 100, $ending = "...", $exact = false, $html = false) { + if (is_array($ending)) + extract($ending); + + if ($html) { + if (strlen(preg_replace("/<[^>]+>/", "", $text)) <= $length) + return $text; + + $totalLength = strlen($ending); + $openTags = array(); + $truncate = ""; + preg_match_all("/(<\/?([\w+]+)[^>]*>)?([^<>]*)/", $text, $tags, PREG_SET_ORDER); + foreach ($tags as $tag) { + if (!preg_match('/img|br|input|hr|area|base|basefont|col|frame|isindex|link|meta|param/s', $tag[2]) + and preg_match('/<[\w]+[^>]*>/s', $tag[0])) + array_unshift($openTags, $tag[2]); + elseif (preg_match('/<\/([\w]+)[^>]*>/s', $tag[0], $closeTag)) { + $pos = array_search($closeTag[1], $openTags); + if ($pos !== false) + array_splice($openTags, $pos, 1); + } + + $truncate .= $tag[1]; + + $contentLength = strlen(preg_replace("/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i", " ", $tag[3])); + if ($contentLength + $totalLength > $length) { + $left = $length - $totalLength; + $entitiesLength = 0; + if (preg_match_all("/&[0-9a-z]{2,8};|&#[0-9]{1,7};|&#x[0-9a-f]{1,6};/i", $tag[3], $entities, PREG_OFFSET_CAPTURE)) + foreach ($entities[0] as $entity) + if ($entity[1] + 1 - $entitiesLength <= $left) { + $left--; + $entitiesLength += strlen($entity[0]); + } else + break; + + $truncate .= substr($tag[3], 0 , $left + $entitiesLength); + + break; + } else { + $truncate .= $tag[3]; + $totalLength += $contentLength; + } + + if ($totalLength >= $length) + break; + } + } else { + if (strlen($text) <= $length) + return $text; + else + $truncate = substr($text, 0, $length - strlen($ending)); + } + + if (!$exact) { + $spacepos = strrpos($truncate, " "); + + if (isset($spacepos)) { + if ($html) { + $bits = substr($truncate, $spacepos); + preg_match_all('/<\/([a-z]+)>/', $bits, $droppedTags, PREG_SET_ORDER); + if (!empty($droppedTags)) + foreach ($droppedTags as $closingTag) + if (!in_array($closingTag[1], $openTags)) + array_unshift($openTags, $closingTag[1]); + } + + $truncate = substr($truncate, 0, $spacepos); + } + } + + $truncate .= $ending; + + if ($html) + foreach ($openTags as $tag) + $truncate .= ''; + + return $truncate; + } + + /** + * Function: when + * Returns date formatting for a string that isn't a regular time() value + * + * Parameters: + * $formatting - The formatting for date(). + * $when - Time to base on. If it is not numeric it will be run through strtotime. + * $strftime - Use @strftime@ instead of @date@? + */ + function when($formatting, $when, $strftime = false) { + $time = (is_numeric($when)) ? $when : strtotime($when) ; + + if ($strftime) + return strftime($formatting, $time); + else + return date($formatting, $time); + } + + /** + * Function: datetime + * Returns a standard datetime string based on either the passed timestamp or their time offset, usually for MySQL inserts. + * + * Parameters: + * $when - An optional timestamp. + */ + function datetime($when = null) { + fallback($when, time()); + + $time = (is_numeric($when)) ? $when : strtotime($when) ; + + return date("Y-m-d H:i:s", $time); + } + + /** + * Function: fix + * Returns a HTML-sanitized version of a string. + * + * Parameters: + * $string - String to fix. + * $quotes - Encode quotes? + */ + function fix($string, $quotes = false) { + $quotes = ($quotes) ? ENT_QUOTES : ENT_NOQUOTES ; + return htmlspecialchars($string, $quotes, "utf-8"); + } + + /** + * Function: unfix + * Returns the reverse of fix(). + * + * Parameters: + * $string - String to unfix. + */ + function unfix($string) { + return htmlspecialchars_decode($string, ENT_QUOTES, "utf-8"); + } + + /** + * Function: lang_code + * Returns the passed language code (e.g. en_US) to the human-readable text (e.g. English (US)) + * + * Parameters: + * $code - The language code to convert + * + * Author: + * TextPattern devs, modified to fit with Chyrp. + */ + function lang_code($code) { + $langs = array("ar_DZ" => "جزائري عربي", + "ca_ES" => "Català", + "cs_CZ" => "ÄŒeÅ¡tina", + "da_DK" => "Dansk", + "de_DE" => "Deutsch", + "el_GR" => "Ελληνικά", + "en_GB" => "English (GB)", + "en_US" => "English (US)", + "es_ES" => "Español", + "et_EE" => "Eesti", + "fi_FI" => "Suomi", + "fr_FR" => "Français", + "gl_GZ" => "Galego (Galiza)", + "he_IL" => "עברית", + "hu_HU" => "Magyar", + "id_ID" => "Bahasa Indonesia", + "is_IS" => "Ãslenska", + "it_IT" => "Italiano", + "ja_JP" => "日本語", + "lv_LV" => "LatvieÅ¡u", + "nl_NL" => "Nederlands", + "no_NO" => "Norsk", + "pl_PL" => "Polski", + "pt_PT" => "Português", + "ro_RO" => "Română", + "ru_RU" => "РуÑÑкий", + "sk_SK" => "SlovenÄina", + "sv_SE" => "Svenska", + "th_TH" => "ไทย", + "uk_UA" => "УкраїнÑька", + "vi_VN" => "Tiếng Việt", + "zh_CN" => "中文(简体)", + "zh_TW" => "中文(ç¹é«”)", + "bg_BG" => "БългарÑки"); + return (isset($langs[$code])) ? str_replace(array_keys($langs), array_values($langs), $code) : $code ; + } + + /** + * Function: sanitize + * Returns a sanitized string, typically for URLs. + * + * Parameters: + * $string - The string to sanitize. + * $force_lowercase - Force the string to lowercase? + * $anal - If set to *true*, will remove all non-alphanumeric characters. + * $trunc - Number of characters to truncate to (default 100, 0 to disable). + */ + function sanitize($string, $force_lowercase = true, $anal = false, $trunc = 100) { + $strip = array("~", "`", "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "_", "=", "+", "[", "{", "]", + "}", "\\", "|", ";", ":", "\"", "'", "‘", "’", "“", "”", "–", "—", + "—", "–", ",", "<", ".", ">", "/", "?"); + $clean = trim(str_replace($strip, "", strip_tags($string))); + $clean = preg_replace('/\s+/', "-", $clean); + $clean = ($anal ? preg_replace("/[^a-zA-Z0-9]/", "", $clean) : $clean); + $clean = ($trunc ? substr($clean, 0, $trunc) : $clean); + return ($force_lowercase) ? + (function_exists('mb_strtolower')) ? + mb_strtolower($clean, 'UTF-8') : + strtolower($clean) : + $clean; + } + + /** + * Function: trackback_respond + * Responds to a trackback request. + * + * Parameters: + * $error - Is this an error? + * $message - Message to return. + */ + function trackback_respond($error = false, $message = "") { + header("Content-Type: text/xml; charset=utf-8"); + + if ($error) { + echo '\n"; + echo "\n"; + echo "1\n"; + echo "".$message."\n"; + echo ""; + exit; + } else { + echo '\n"; + echo "\n"; + echo "0\n"; + echo ""; + } + + exit; + } + + /** + * Function: trackback_send + * Sends a trackback request. + * + * Parameters: + * $post - The post we're sending from. + * $target - The URL we're sending to. + */ + function trackback_send($post, $target) { + if (empty($target)) return false; + + $target = parse_url($target); + $title = $post->title(); + fallback($title, ucfirst($post->feather)." Post #".$post->id); + $excerpt = strip_tags(truncate($post->excerpt(), 255)); + + if (!empty($target["query"])) $target["query"] = "?".$target["query"]; + if (empty($target["port"])) $target["port"] = 80; + + $connect = fsockopen($target["host"], $target["port"]); + if (!$connect) return false; + + $config = Config::current(); + $query = "url=".rawurlencode($post->url())."&". + "title=".rawurlencode($title)."&". + "blog_name=".rawurlencode($config->name)."&". + "excerpt=".rawurlencode($excerpt); + + fwrite($connect, "POST ".$target["path"].$target["query"]." HTTP/1.1\n"); + fwrite($connect, "Host: ".$target["host"]."\n"); + fwrite($connect, "Content-type: application/x-www-form-urlencoded\n"); + fwrite($connect, "Content-length: ". strlen($query)."\n"); + fwrite($connect, "Connection: close\n\n"); + fwrite($connect, $query); + + fclose($connect); + + return true; + } + + /** + * Function: send_pingbacks + * Sends pingback requests to the URLs in a string. + * + * Parameters: + * $string - The string to crawl for pingback URLs. + * $post - The post we're sending from. + */ + function send_pingbacks($string, $post) { + foreach (grab_urls($string) as $url) + if ($ping_url = pingback_url($url)) { + require_once INCLUDES_DIR."/lib/ixr.php"; + + $client = new IXR_Client($ping_url); + $client->timeout = 3; + $client->useragent.= " -- Chyrp/".CHYRP_VERSION; + $client->query("pingback.ping", $post->url(), $url); + } + } + + /** + * Function: grab_urls + * Crawls a string for links. + * + * Parameters: + * $string - The string to crawl. + * + * Returns: + * An array of all URLs found in the string. + */ + function grab_urls($string) { + $regexp = "/]+href=[\"|']([^\"]+)[\"|']>[^<]+<\/a>/"; + preg_match_all(Trigger::current()->filter($regexp, "link_regexp"), stripslashes($string), $matches); + $matches = $matches[1]; + return $matches; + } + + /** + * Function: pingback_url + * Checks if a URL is pingback-capable. + * + * Parameters: + * $url - The URL to check. + * + * Returns: + * The pingback target, if the URL is pingback-capable. + */ + function pingback_url($url) { + extract(parse_url($url), EXTR_SKIP); + if (!isset($host)) return false; + + $path = (!isset($path)) ? '/' : $path ; + if (isset($query)) $path.= '?'.$query; + $port = (isset($port)) ? $port : 80 ; + + # Connect + $connect = @fsockopen($host, $port, $errno, $errstr, 2); + if (!$connect) return false; + + # Send the GET headers + fwrite($connect, "GET $path HTTP/1.1\r\n"); + fwrite($connect, "Host: $host\r\n"); + fwrite($connect, "User-Agent: Chyrp/".CHYRP_VERSION."\r\n\r\n"); + + # Check for X-Pingback header + $headers = ""; + while (!feof($connect)) { + $line = fgets($connect, 512); + if (trim($line) == "") break; + $headers.= trim($line)."\n"; + + if (preg_match("/X-Pingback: (.+)/i", $line, $matches)) + return trim($matches[1]); + + # Nothing's found so far, so grab the content-type + # for the search afterwards + if (preg_match("/Content-Type: (.+)/i", $headers, $matches)) + $content_type = trim($matches[1]); + } + + # No header found, check for + if (preg_match('/(image|audio|video|model)/i', $content_type)) return false; + $size = 0; + while (!feof($connect)) { + $line = fgets($connect, 1024); + if (preg_match("//i", $line, $link)) + return $link[1]; + $size += strlen($line); + if ($size > 2048) return false; + } + + fclose($connect); + + return false; + } + + /** + * Function: camelize + * Converts a given string to camel-case. + * + * Parameters: + * $string - The string to camelize. + * $keep_spaces - Whether or not to convert underscores to spaces or remove them. + * + * Returns: + * A CamelCased string. + * + * See Also: + * + */ + function camelize($string, $keep_spaces = false) { + $lower = strtolower($string); + $deunderscore = str_replace("_", " ", $lower); + $dehyphen = str_replace("-", " ", $deunderscore); + $final = ucwords($dehyphen); + + if (!$keep_spaces) + $final = str_replace(" ", "", $final); + + return $final; + } + + /** + * Function: decamelize + * Decamelizes a string. + * + * Parameters: + * $string - The string to decamelize. + * + * Returns: + * A de_camel_cased string. + * + * See Also: + * + */ + function decamelize($string) { + return strtolower(preg_replace("/([a-z])([A-Z])/", "\\1_\\2", $string)); + } + + /** + * Function: selected + * If $val1 == $val2, outputs or returns @ selected="selected"@ + * + * Parameters: + * $val1 - First value. + * $val2 - Second value. + * $return - Return @ selected="selected"@ instead of outputting it + */ + function selected($val1, $val2, $return = false) { + if ($val1 == $val2) + if ($return) + return ' selected="selected"'; + else + echo ' selected="selected"'; + } + + /** + * Function: checked + * If $val == 1 (true), outputs ' checked="checked"' + * + * Parameters: + * $val - Value to check. + */ + function checked($val) { + if ($val == 1) echo ' checked="checked"'; + } + + /** + * Function: module_enabled + * Returns whether the given module is enabled or not. + * + * Parameters: + * $name - The folder name of the module. + * + * Returns: + * Whether or not the requested module is enabled. + */ + function module_enabled($name) { + $config = Config::current(); + return in_array($name, $config->enabled_modules); + } + + /** + * Function: feather_enabled + * Returns whether the given feather is enabled or not. + * + * Parameters: + * $name - The folder name of the feather. + * + * Returns: + * Whether or not the requested feather is enabled. + */ + function feather_enabled($name) { + $config = Config::current(); + return in_array($name, $config->enabled_feathers); + } + + /** + * Function: fallback + * Sets a given variable if it is not set. + * + * The last of the arguments or the first non-empty value will be used. + * + * Parameters: + * &$variable - The variable to return or set. + * + * Returns: + * The value of whatever was chosen. + */ + function fallback(&$variable) { + if (is_bool($variable)) + return $variable; + + $set = (!isset($variable) or (is_string($variable) and trim($variable) === "") or $variable === array()); + + $args = func_get_args(); + array_shift($args); + if (count($args) > 1) { + foreach ($args as $arg) { + $fallback = $arg; + + if (isset($arg) and (!is_string($arg) or (is_string($arg) and trim($arg) !== "")) and $arg !== array()) + break; + } + } else + $fallback = isset($args[0]) ? $args[0] : null ; + + if ($set) + $variable = $fallback; + + return $set ? $fallback : $variable ; + } + + /** + * Function: oneof + * Returns the first argument that is set and non-empty. + * + * It will guess where to stop based on the types of the arguments, e.g. "" has priority over array() but not 1. + */ + function oneof() { + $last = null; + $args = func_get_args(); + foreach ($args as $index => $arg) { + if (!isset($arg) or (is_string($arg) and trim($arg) === "") or $arg === array() or (is_object($arg) and empty($arg)) or ($arg === "0000-00-00 00:00:00")) + $last = $arg; + else + return $arg; + + if ($index + 1 == count($args)) + break; + + $next = $args[$index + 1]; + + $incomparable = ((is_array($arg) and !is_array($next)) or # This is a big check but it should cover most "incomparable" cases. + (!is_array($arg) and is_array($next)) or # Using simple type comparison wouldn't work too well, for example + (is_object($arg) and !is_object($next)) or # when "" would take priority over 1 in oneof("", 1) because they're + (!is_object($arg) and is_object($next)) or # different types. + (is_resource($arg) and !is_resource($next)) or + (!is_resource($arg) and is_resource($next))); + + if (isset($arg) and isset($next) and $incomparable) + return $arg; + } + + return $last; + } + + /** + * Function: random + * Returns a random string. + * + * Parameters: + * $length - How long the string should be. + * $specialchars - Use special characters in the resulting string? + */ + function random($length, $specialchars = false) { + $pattern = "1234567890abcdefghijklmnopqrstuvwxyz"; + + if ($specialchars) + $pattern.= "!@#$%^&*()?~"; + + $len = strlen($pattern) - 1; + + $key = ""; + for($i = 0; $i < $length; $i++) + $key.= $pattern[rand(0, $len)]; + + return $key; + } + + /** + * Function: unique_filename + * Makes a given filename unique for the uploads directory. + * + * Parameters: + * $name - The name to check. + * $path - Path to check in. + * $num - Number suffix from which to start increasing if the filename exists. + * + * Returns: + * A unique version of the given $name. + */ + function unique_filename($name, $path = "", $num = 2) { + $path = rtrim($path, "/"); + if (!file_exists(MAIN_DIR.Config::current()->uploads_path.$path."/".$name)) + return $name; + + $name = explode(".", $name); + + # Handle common double extensions + foreach (array("tar.gz", "tar.bz", "tar.bz2") as $extension) { + list($first, $second) = explode(".", $extension); + $file_first =& $name[count($name) - 2]; + if ($file_first == $first and end($name) == $second) { + $file_first = $first.".".$second; + array_pop($name); + } + } + + $ext = ".".array_pop($name); + + $try = implode(".", $name)."-".$num.$ext; + if (!file_exists(MAIN_DIR.Config::current()->uploads_path.$path."/".$try)) + return $try; + + return unique_filename(implode(".", $name).$ext, $path, $num + 1); + } + + /** + * Function: upload + * Moves an uploaded file to the uploads directory. + * + * Parameters: + * $file - The $_FILES value. + * $extension - An array of valid extensions (case-insensitive). + * $path - A sub-folder in the uploads directory (optional). + * $put - Use copy() instead of move_uploaded_file()? + * + * Returns: + * The resulting filename from the upload. + */ + function upload($file, $extension = null, $path = "", $put = false) { + $file_split = explode(".", $file['name']); + $path = rtrim($path, "/"); + $dir = MAIN_DIR.Config::current()->uploads_path.$path; + + if (!file_exists($dir)) + mkdir($dir, 0777, true); + + $original_ext = end($file_split); + + # Handle common double extensions + foreach (array("tar.gz", "tar.bz", "tar.bz2") as $ext) { + list($first, $second) = explode(".", $ext); + $file_first =& $file_split[count($file_split) - 2]; + if ($file_first == $first and end($file_split) == $second) { + $file_first = $first.".".$second; + array_pop($file_split); + } + } + + $file_ext = end($file_split); + + if (is_array($extension)) { + if (!in_array(strtolower($file_ext), $extension) and !in_array(strtolower($original_ext), $extension)) { + $list = ""; + for ($i = 0; $i < count($extension); $i++) { + $comma = ""; + if (($i + 1) != count($extension)) $comma = ", "; + if (($i + 2) == count($extension)) $comma = ", and "; + $list.= "*.".$extension[$i]."".$comma; + } + error(__("Invalid Extension"), _f("Only %s files are accepted.", array($list))); + } + } elseif (isset($extension) and + strtolower($file_ext) != strtolower($extension) and + strtolower($original_ext) != strtolower($extension)) + error(__("Invalid Extension"), _f("Only %s files are supported.", array("*.".$extension))); + + array_pop($file_split); + $file_clean = implode(".", $file_split); + $file_clean = sanitize($file_clean, false).".".$file_ext; + $filename = unique_filename($file_clean, $path); + + $message = __("Couldn't upload file. CHMOD ".$dir." to 777 and try again. If this problem persists, it's probably timing out; in which case, you must contact your system administrator to increase the maximum POST and upload sizes."); + + if ($put) { + if (!@copy($file['tmp_name'], $dir."/".$filename)) + error(__("Error"), $message); + } elseif (!@move_uploaded_file($file['tmp_name'], $dir."/".$filename)) + error(__("Error"), $message); + + return ($path ? $path."/".$filename : $filename); + } + + /** + * Function: upload_from_url + * Copy a file from a specified URL to their upload directory. + * + * Parameters: + * $url - The URL to copy. + * $extension - An array of valid extensions (case-insensitive). + * $path - A sub-folder in the uploads directory (optional). + * + * See Also: + * + */ + function upload_from_url($url, $extension = null, $path = "") { + $file = tempnam(null, "chyrp"); + file_put_contents($file, get_remote($url)); + + $fake_file = array("name" => basename(parse_url($url, PHP_URL_PATH)), + "tmp_name" => $file); + + return upload($fake_file, $extension, $path, true); + } + + /** + * Function: uploaded + * Returns a URL to an uploaded file. + * + * Parameters: + * $file - Filename relative to the uploads directory. + */ + function uploaded($file, $url = true) { + if (empty($file)) + return ""; + + $config = Config::current(); + return ($url ? $config->chyrp_url.$config->uploads_path.$file : MAIN_DIR.$config->uploads_path.$file); + } + + /** + * Function: timer_start + * Starts the timer. + */ + function timer_start() { + global $time_start; + $mtime = explode(" ", microtime()); + $mtime = $mtime[1] + $mtime[0]; + $time_start = $mtime; + } + + /** + * Function: timer_stop + * Stops the timer and returns the total time. + * + * Parameters: + * $precision - Number of decimals places to round to. + * + * Returns: + * A formatted number with the given $precision. + */ + function timer_stop($precision = 3) { + global $time_start; + $mtime = microtime(); + $mtime = explode(" ", $mtime); + $mtime = $mtime[1] + $mtime[0]; + $time_end = $mtime; + $time_total = $time_end - $time_start; + return number_format($time_total, $precision); + } + + /** + * Function: normalize + * Attempts to normalize all newlines and whitespace into single spaces. + * + * Returns: + * The normalized string. + */ + function normalize($string) { + $trimmed = trim($string); + $newlines = str_replace("\n\n", " ", $trimmed); + $newlines = str_replace("\n", "", $newlines); + $normalized = preg_replace("/[\s\n\r\t]+/", " ", $newlines); + return $normalized; + } + + /** + * Function: get_remote + * Grabs the contents of a website/location. + * + * Parameters: + * $url - The URL of the location to grab. + * + * Returns: + * The response from the remote URL. + */ + function get_remote($url) { + extract(parse_url($url), EXTR_SKIP); + + if (ini_get("allow_url_fopen")) { + $content = @file_get_contents($url); + if ($http_response_header[0] != "HTTP/1.1 200 OK") + $content = "Server returned a message: $http_response_header[0]"; + } elseif (function_exists("curl_init")) { + $handle = curl_init(); + curl_setopt($handle, CURLOPT_URL, $url); + curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 1); + curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($handle, CURLOPT_TIMEOUT, 60); + $content = curl_exec($handle); + $status = curl_getinfo($handle, CURLINFO_HTTP_CODE); + curl_close($handle); + if ($status != 200) + $content = "Server returned a message: $status"; + } else { + $path = (!isset($path)) ? '/' : $path ; + if (isset($query)) $path.= '?'.$query; + $port = (isset($port)) ? $port : 80 ; + + $connect = @fsockopen($host, $port, $errno, $errstr, 2); + if (!$connect) return false; + + # Send the GET headers + fwrite($connect, "GET ".$path." HTTP/1.1\r\n"); + fwrite($connect, "Host: ".$host."\r\n"); + fwrite($connect, "User-Agent: Chyrp/".CHYRP_VERSION."\r\n\r\n"); + + $content = ""; + while (!feof($connect)) { + $line = fgets($connect, 128); + if (preg_match("/\r\n/", $line)) continue; + + $content.= $line; + } + + fclose($connect); + } + + return $content; + } + + /** + * Function: self_url + * Returns the current URL. + */ + function self_url() { + $split = explode("/", $_SERVER['SERVER_PROTOCOL']); + $protocol = strtolower($split[0]); + $default_port = ($protocol == "http") ? 80 : 443 ; + $port = ($_SERVER['SERVER_PORT'] == $default_port) ? "" : ":".$_SERVER['SERVER_PORT'] ; + return $protocol."://".$_SERVER['SERVER_NAME'].$port.$_SERVER['REQUEST_URI']; + } + + /** + * Function: show_404 + * Shows a 404 error message and immediately exits. + * + * Parameters: + * $scope - An array of values to extract into the scope. + */ + function show_404() { + header("HTTP/1.1 404 Not Found"); + + if (!defined('CHYRP_VERSION')) + exit("404 Not Found"); + + $theme = Theme::current(); + $main = MainController::current(); + + Trigger::current()->call("not_found"); + + if ($theme->file_exists("pages/404")) + $main->display("pages/404", array(), "404"); + else + error(__("404 Not Found"), __("The requested page could not be located.")); + + exit; + } + + /** + * Function: set_locale + * Set locale in a platform-independent way + * + * Parameters: + * $locale - the locale name (@en_US@, @uk_UA@, @fr_FR@ etc.) + * + * Returns: + * The encoding name used by locale-aware functions. + */ + function set_locale($locale) { # originally via http://www.onphp5.com/article/22; heavily modified + if ($locale == "en_US") return; # en_US is the default in Chyrp; their system may have + # its own locale setting and no Chyrp translation available + # for their locale, so let's just leave it alone. + + list($lang, $cty) = explode("_", $locale); + $locales = array($locale.".UTF-8", $lang, "en_US.UTF-8", "en"); + $result = setlocale(LC_ALL, $locales); + + return (!strpos($result, 'UTF-8')) ? "CP".preg_replace('~\.(\d+)$~', "\\1", $result) : "UTF-8" ; + } + + /** + * Function: sanitize_input + * Makes sure no inherently broken ideas such as magic_quotes break our application + * + * Parameters: + * $data - The array to be sanitized, usually one of @$_GET@, @$_POST@, @$_COOKIE@, or @$_REQUEST@ + */ + function sanitize_input(&$data) { + foreach ($data as &$value) + if (is_array($value)) + sanitize_input($value); + else + $value = get_magic_quotes_gpc() ? stripslashes($value) : $value ; + } + + /** + * Function: match + * Try to match a string against an array of regular expressions. + * + * Parameters: + * $try - An array of regular expressions, or a single regular expression. + * $haystack - The string to test. + * + * Returns: + * Whether or not the match succeeded. + */ + function match($try, $haystack) { + if (is_string($try)) + return (bool) preg_match($try, $haystack); + + foreach ($try as $needle) + if (preg_match($needle, $haystack)) + return true; + + return false; + } + + /** + * Function: cancel_module + * Temporarily removes a module from $config->enabled_modules. + * + * Parameters: + * $target - Module name to disable. + */ + function cancel_module($target) { + $this_disabled = array(); + + if (isset(Modules::$instances[$target])) + Modules::$instances[$target]->cancelled = true; + + $config = Config::current(); + foreach ($config->enabled_modules as $module) + if ($module != $target) + $this_disabled[] = $module; + + return $config->enabled_modules = $this_disabled; + } + + /** + * Function: time_in_timezone + * Returns the appropriate time() for representing a timezone. + */ + function time_in_timezone($timezone) { + $orig = get_timezone(); + set_timezone($timezone); + $time = date("F jS, Y, g:i A"); + set_timezone($orig); + return strtotime($time); + } + + /** + * Function: timezones + * Returns an array of timezones that have unique offsets. Doesn't count deprecated timezones. + */ + function timezones() { + $zones = array(); + + $deprecated = array("Brazil/Acre", "Brazil/DeNoronha", "Brazil/East", "Brazil/West", "Canada/Atlantic", "Canada/Central", "Canada/East-Saskatchewan", "Canada/Eastern", "Canada/Mountain", "Canada/Newfoundland", "Canada/Pacific", "Canada/Saskatchewan", "Canada/Yukon", "CET", "Chile/Continental", "Chile/EasterIsland", "CST6CDT", "Cuba", "EET", "Egypt", "Eire", "EST", "EST5EDT", "Etc/GMT", "Etc/GMT+0", "Etc/GMT+1", "Etc/GMT+10", "Etc/GMT+11", "Etc/GMT+12", "Etc/GMT+2", "Etc/GMT+3", "Etc/GMT+4", "Etc/GMT+5", "Etc/GMT+6", "Etc/GMT+7", "Etc/GMT+8", "Etc/GMT+9", "Etc/GMT-0", "Etc/GMT-1", "Etc/GMT-10", "Etc/GMT-11", "Etc/GMT-12", "Etc/GMT-13", "Etc/GMT-14", "Etc/GMT-2", "Etc/GMT-3", "Etc/GMT-4", "Etc/GMT-5", "Etc/GMT-6", "Etc/GMT-7", "Etc/GMT-8", "Etc/GMT-9", "Etc/GMT0", "Etc/Greenwich", "Etc/UCT", "Etc/Universal", "Etc/UTC", "Etc/Zulu", "Factory", "GB", "GB-Eire", "GMT", "GMT+0", "GMT-0", "GMT0", "Greenwich", "Hongkong", "HST", "Iceland", "Iran", "Israel", "Jamaica", "Japan", "Kwajalein", "Libya", "MET", "Mexico/BajaNorte", "Mexico/BajaSur", "Mexico/General", "MST", "MST7MDT", "Navajo", "NZ", "NZ-CHAT", "Poland", "Portugal", "PRC", "PST8PDT", "ROC", "ROK", "Singapore", "Turkey", "UCT", "Universal", "US/Alaska", "US/Aleutian", "US/Arizona", "US/Central", "US/East-Indiana", "US/Eastern", "US/Hawaii", "US/Indiana-Starke", "US/Michigan", "US/Mountain", "US/Pacific", "US/Pacific-New", "US/Samoa", "UTC", "W-SU", "WET", "Zulu"); + + foreach (timezone_identifiers_list() as $zone) + if (!in_array($zone, $deprecated)) + $zones[] = array("name" => $zone, + "now" => time_in_timezone($zone)); + + function by_time($a, $b) { + return (int) ($a["now"] > $b["now"]); + } + + usort($zones, "by_time"); + + return $zones; + } + + /** + * Function: set_timezone + * Sets the timezone. + * + * Parameters: + * $timezone - The timezone to set. + */ + function set_timezone($timezone) { + if (function_exists("date_default_timezone_set")) + date_default_timezone_set($timezone); + else + ini_set("date.timezone", $timezone); + } + + /** + * Function: get_timezone() + * Returns the current timezone. + */ + function get_timezone() { + if (function_exists("date_default_timezone_set")) + return date_default_timezone_get(); + else + return ini_get("date.timezone"); + } + + /** + * Function: error_panicker + * Exits and states where the error occurred. + */ + function error_panicker($errno, $message, $file, $line) { + if (error_reporting() === 0) + return; # Suppressed error. + + exit("ERROR: ".$message." (".$file." on line ".$line.")"); + } + + /** + * Function: keywords + * Handle keyword-searching. + * + * Parameters: + * $query - The query to parse. + * $plain - WHERE syntax to search for non-keyword queries. + * $table - If specified, the keywords will be checked against this table's columns for validity. + * + * Returns: + * An array containing the "WHERE" queries and the corresponding parameters. + */ + function keywords($query, $plain, $table = null) { + if (!trim($query)) + return array(array(), array()); + + $search = array(); + $matches = array(); + $where = array(); + $params = array(); + + if ($table) + $columns = SQL::current()->select($table)->fetch(); + + $queries = explode(" ", $query); + foreach ($queries as $query) + if (!preg_match("/([a-z0-9_]+):(.+)/", $query)) + $search[] = $query; + else + $matches[] = $query; + + $times = array("year", "month", "day", "hour", "minute", "second"); + + foreach ($matches as $match) { + list($test, $equals,) = explode(":", $match); + + if ($equals[0] == '"') { + if (substr($equals, -1) != '"') + foreach ($search as $index => $part) { + $equals.= " ".$part; + + unset($search[$index]); + + if (substr($part, -1) == '"') + break; + } + + $equals = ltrim(trim($equals, '"'), '"'); + } + + if (in_array($test, $times)) { + if ($equals == "today") + $where["created_at like"] = date("%Y-m-d %"); + elseif ($equals == "yesterday") + $where["created_at like"] = date("%Y-m-d %", now("-1 day")); + elseif ($equals == "tomorrow") + error(__("Error"), "Unfortunately our flux capacitor is currently having issues. Try again yesterday."); + else + $where[strtoupper($test)."(created_at)"] = $equals; + } elseif ($test == "author") { + $user = new User(array("login" => $equals)); + if ($user->no_results and $equals == "me") { + !($table == "users") ? $where["user_id"] = Visitor::current()->id : $where["id"] = Visitor::current()->id; + } else + !($table == "users") ? $where["user_id"] = $user->id : $where["id"] = $user->id; + } elseif ($test == "group") { + $group = new Group(array("name" => $equals)); + $where["group_id"] = $equals = ($group->no_results) ? 0 : $group->id; + } else + $where[$test] = $equals; + } + + if ($table) + foreach ($where as $col => $val) + if (!isset($where[$col])) { + if ($table == "posts") { + $where["post_attributes.name"] = $col; + $where["post_attributes.value like"] = "%".$val."%"; + } + + unset($where[$col]); + } + + if (!empty($search)) { + $where[] = $plain; + $params[":query"] = "%".join(" ", $search)."%"; + } + + $keywords = array($where, $params); + + Trigger::current()->filter($keywords, "keyword_search", $query, $plain); + + return $keywords; + } + + /** + * Function: init_extensions + * Initialize all Modules and Feathers. + */ + function init_extensions() { + $config = Config::current(); + + # Instantiate all Modules. + foreach ($config->enabled_modules as $index => $module) { + if (!file_exists(MODULES_DIR."/".$module."/".$module.".php")) { + unset($config->enabled_modules[$index]); + continue; + } + + if (file_exists(MODULES_DIR."/".$module."/locale/".$config->locale.".mo")) + load_translator($module, MODULES_DIR."/".$module."/locale/".$config->locale.".mo"); + + require MODULES_DIR."/".$module."/".$module.".php"; + + $camelized = camelize($module); + if (!class_exists($camelized)) + continue; + + Modules::$instances[$module] = new $camelized; + Modules::$instances[$module]->safename = $module; + + foreach (YAML::load(MODULES_DIR."/".$module."/info.yaml") as $key => $val) + Modules::$instances[$module]->$key = (is_string($val)) ? __($val, $module) : $val ; + } + + # Instantiate all Feathers. + foreach ($config->enabled_feathers as $index => $feather) { + if (!file_exists(FEATHERS_DIR."/".$feather."/".$feather.".php")) { + unset($config->enabled_feathers[$index]); + continue; + } + + if (file_exists(FEATHERS_DIR."/".$feather."/locale/".$config->locale.".mo")) + load_translator($feather, FEATHERS_DIR."/".$feather."/locale/".$config->locale.".mo"); + + require FEATHERS_DIR."/".$feather."/".$feather.".php"; + + $camelized = camelize($feather); + if (!class_exists($camelized)) + continue; + + Feathers::$instances[$feather] = new $camelized; + Feathers::$instances[$feather]->safename = $feather; + + foreach (YAML::load(FEATHERS_DIR."/".$feather."/info.yaml") as $key => $val) + Feathers::$instances[$feather]->$key = (is_string($val)) ? __($val, $feather) : $val ; + } + + # Initialize all modules. + foreach (Feathers::$instances as $feather) + if (method_exists($feather, "__init")) + $feather->__init(); + + foreach (Modules::$instances as $module) + if (method_exists($module, "__init")) + $module->__init(); + } + + /** + * Function: xml2arr + * Recursively converts a SimpleXML object (and children) to an array. + * + * Parameters: + * $parse - The SimpleXML object to convert into an array. + */ + function xml2arr($parse) { + if (empty($parse)) + return ""; + + $parse = (array) $parse; + + foreach ($parse as &$val) + if (get_class($val) == "SimpleXMLElement") + $val = xml2arr($val); + + return $parse; + } + + /** + * Function: arr2xml + * Recursively adds an array (or object I guess) to a SimpleXML object. + * + * Parameters: + * &$object - The SimpleXML object to modify. + * $data - The data to add to the SimpleXML object. + */ + function arr2xml(&$object, $data) { + foreach ($data as $key => $val) { + if (is_int($key) and (empty($val) or (is_string($val) and trim($val) == ""))) { + unset($data[$key]); + continue; + } + + if (is_array($val)) { + if (in_array(0, array_keys($val))) { # Numeric-indexed things need to be added as duplicates + foreach ($val as $dup) { + $xml = $object->addChild($key); + arr2xml($xml, $dup); + } + } else { + $xml = $object->addChild($key); + arr2xml($xml, $val); + } + } else + $object->addChild($key, fix($val, false, false)); + } + } + + /** + * Function: relative_time + * Returns the difference between the given timestamps or now. + * + * Parameters: + * $time - Timestamp to compare to. + * $from - Timestamp to compare from. If not specified, defaults to now. + * + * Returns: + * A string formatted like "3 days ago" or "3 days from now". + */ + function relative_time($when, $from = null) { + fallback($from, time()); + + $time = (is_numeric($when)) ? $when : strtotime($when) ; + + $difference = $from - $time; + + if ($difference < 0) { + $word = "from now"; + $difference = -$difference; + } elseif ($difference > 0) + $word = "ago"; + else + return "just now"; + + $units = array("second" => 1, + "minute" => 60, + "hour" => 60 * 60, + "day" => 60 * 60 * 24, + "week" => 60 * 60 * 24 * 7, + "month" => 60 * 60 * 24 * 30, + "year" => 60 * 60 * 24 * 365, + "decade" => 60 * 60 * 24 * 365 * 10, + "century" => 60 * 60 * 24 * 365 * 100, + "millennium" => 60 * 60 * 24 * 365 * 1000); + + $possible_units = array(); + foreach ($units as $name => $val) + if (($name == "week" and $difference >= ($val * 2)) or # Only say "weeks" after two have passed. + ($name != "week" and $difference >= $val)) + $unit = $possible_units[] = $name; + + $precision = (int) in_array("year", $possible_units); + $amount = round($difference / $units[$unit], $precision); + + return $amount." ".pluralize($unit, $amount)." ".$word; + } + + /** + * Function: list_notate + * Notates an array as a list of things. + * + * Parameters: + * $array - An array of things to notate. + * $quotes - Wrap quotes around strings? + * + * Returns: + * A string like "foo, bar, and baz". + */ + function list_notate($array, $quotes = false) { + $count = 0; + $items = array(); + foreach ($array as $item) { + $string = (is_string($item) and $quotes) ? "“".$item."”" : $item ; + if (count($array) == ++$count and $count !== 1) + $items[] = __("and ").$string; + else + $items[] = $string; + } + + return (count($array) == 2) ? implode(" ", $items) : implode(", ", $items) ; + } + + /** + * Function: email + * Send an email. Function arguments are exactly the same as the PHP mail() function. + * + * This is intended so that modules can provide an email method if the server cannot use mail(). + */ + function email() { + $function = "mail"; + Trigger::current()->filter($function, "send_mail"); + $args = func_get_args(); # Looks redundant, but it must be so in order to meet PHP's retardation requirements. + return call_user_func_array($function, $args); + } + + /** + * Function: now + * Alias to strtotime, for prettiness like now("+1 day"). + */ + function now($when) { + return strtotime($when); + } + + /** + * Function: comma_sep + * Convert a comma-seperated string into an array of the listed values. + */ + function comma_sep($string) { + $commas = explode(",", $string); + $trimmed = array_map("trim", $commas); + $cleaned = array_diff(array_unique($trimmed), array("")); + return $cleaned; + } diff --git a/News/includes/interface/Feather.php b/News/includes/interface/Feather.php new file mode 100644 index 0000000..48a4068 --- /dev/null +++ b/News/includes/interface/Feather.php @@ -0,0 +1,40 @@ + object created. + */ + public function submit(); + + /** + * Function: update + * Handles updating a post. + */ + public function update($post); + + /** + * Function: title + * Returns the appropriate source to be treated as a "title" of a post. + * If there is no immediate solution, you may use . + */ + public function title($post); + + /** + * Function: excerpt + * Returns the appropriate source, unmodified, to be used as an excerpt of a post. + */ + public function excerpt($post); + + /** + * Function: feed_content + * Returns the appropriate content for a feed. + */ + public function feed_content($post); + } diff --git a/News/includes/javascript.php b/News/includes/javascript.php new file mode 100755 index 0000000..5d1518b --- /dev/null +++ b/News/includes/javascript.php @@ -0,0 +1,143 @@ + + diff --git a/News/includes/lib/PasswordHash.php b/News/includes/lib/PasswordHash.php new file mode 100755 index 0000000..6b52212 --- /dev/null +++ b/News/includes/lib/PasswordHash.php @@ -0,0 +1,236 @@ + in 2004-2006 and placed in +# the public domain. Revised in subsequent years, still public domain. +# +# The homepage URL for this framework is: http://www.openwall.com/phpass/ +# +class PasswordHash { + var $itoa64; + var $iteration_count_log2; + var $portable_hashes; + var $random_state; + + function PasswordHash($iteration_count_log2, $portable_hashes) + { + $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) + $iteration_count_log2 = 8; + $this->iteration_count_log2 = $iteration_count_log2; + + $this->portable_hashes = $portable_hashes; + + $this->random_state = microtime() . uniqid(rand(), TRUE); // removed getmypid() for compability reasons; + } + + function get_random_bytes($count) + { + $output = ''; + if (@is_readable('/dev/urandom') && + ($fh = @fopen('/dev/urandom', 'rb'))) { + $output = fread($fh, $count); + fclose($fh); + } + + if (strlen($output) < $count) { + $output = ''; + for ($i = 0; $i < $count; $i += 16) { + $this->random_state = + md5(microtime() . $this->random_state); + $output .= + pack('H*', md5($this->random_state)); + } + $output = substr($output, 0, $count); + } + + return $output; + } + + function encode64($input, $count) + { + $output = ''; + $i = 0; + do { + $value = ord($input[$i++]); + $output .= $this->itoa64[$value & 0x3f]; + if ($i < $count) + $value |= ord($input[$i]) << 8; + $output .= $this->itoa64[($value >> 6) & 0x3f]; + if ($i++ >= $count) + break; + if ($i < $count) + $value |= ord($input[$i]) << 16; + $output .= $this->itoa64[($value >> 12) & 0x3f]; + if ($i++ >= $count) + break; + $output .= $this->itoa64[($value >> 18) & 0x3f]; + } while ($i < $count); + + return $output; + } + + function gensalt_private($input) + { + $output = '$P$'; + $output .= $this->itoa64[min($this->iteration_count_log2 + + ((PHP_VERSION >= '5') ? 5 : 3), 30)]; + $output .= $this->encode64($input, 6); + + return $output; + } + + function crypt_private($password, $setting) + { + $output = '*0'; + if (substr($setting, 0, 2) == $output) + $output = '*1'; + + $id = substr($setting, 0, 3); + # We use "$P$", phpBB3 uses "$H$" for the same thing + if ($id != '$P$' && $id != '$H$') + return $output; + + $count_log2 = strpos($this->itoa64, $setting[3]); + if ($count_log2 < 7 || $count_log2 > 30) + return $output; + + $count = 1 << $count_log2; + + $salt = substr($setting, 4, 8); + if (strlen($salt) != 8) + return $output; + + # We're kind of forced to use MD5 here since it's the only + # cryptographic primitive available in all versions of PHP + # currently in use. To implement our own low-level crypto + # in PHP would result in much worse performance and + # consequently in lower iteration counts and hashes that are + # quicker to crack (by non-PHP code). + if (PHP_VERSION >= '5') { + $hash = md5($salt . $password, TRUE); + do { + $hash = md5($hash . $password, TRUE); + } while (--$count); + } else { + $hash = pack('H*', md5($salt . $password)); + do { + $hash = pack('H*', md5($hash . $password)); + } while (--$count); + } + + $output = substr($setting, 0, 12); + $output .= $this->encode64($hash, 16); + + return $output; + } + + function gensalt_extended($input) + { + $count_log2 = min($this->iteration_count_log2 + 8, 24); + # This should be odd to not reveal weak DES keys, and the + # maximum valid value is (2**24 - 1) which is odd anyway. + $count = (1 << $count_log2) - 1; + + $output = '_'; + $output .= $this->itoa64[$count & 0x3f]; + $output .= $this->itoa64[($count >> 6) & 0x3f]; + $output .= $this->itoa64[($count >> 12) & 0x3f]; + $output .= $this->itoa64[($count >> 18) & 0x3f]; + + $output .= $this->encode64($input, 3); + + return $output; + } + + function gensalt_blowfish($input) + { + # This one needs to use a different order of characters and a + # different encoding scheme from the one in encode64() above. + # We care because the last character in our encoded string will + # only represent 2 bits. While two known implementations of + # bcrypt will happily accept and correct a salt string which + # has the 4 unused bits set to non-zero, we do not want to take + # chances and we also do not want to waste an additional byte + # of entropy. + $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $output = '$2a$'; + $output .= chr(ord('0') + $this->iteration_count_log2 / 10); + $output .= chr(ord('0') + $this->iteration_count_log2 % 10); + $output .= '$'; + + $i = 0; + do { + $c1 = ord($input[$i++]); + $output .= $itoa64[$c1 >> 2]; + $c1 = ($c1 & 0x03) << 4; + if ($i >= 16) { + $output .= $itoa64[$c1]; + break; + } + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 4; + $output .= $itoa64[$c1]; + $c1 = ($c2 & 0x0f) << 2; + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 6; + $output .= $itoa64[$c1]; + $output .= $itoa64[$c2 & 0x3f]; + } while (1); + + return $output; + } + + function HashPassword($password) + { + $random = ''; + + if (CRYPT_BLOWFISH == 1 && !$this->portable_hashes) { + $random = $this->get_random_bytes(16); + $hash = + crypt($password, $this->gensalt_blowfish($random)); + if (strlen($hash) == 60) + return $hash; + } + + if (CRYPT_EXT_DES == 1 && !$this->portable_hashes) { + if (strlen($random) < 3) + $random = $this->get_random_bytes(3); + $hash = + crypt($password, $this->gensalt_extended($random)); + if (strlen($hash) == 20) + return $hash; + } + + if (strlen($random) < 6) + $random = $this->get_random_bytes(6); + $hash = + $this->crypt_private($password, + $this->gensalt_private($random)); + if (strlen($hash) == 34) + return $hash; + + # Returning '*' on error is safe here, but would _not_ be safe + # in a crypt(3)-like function used _both_ for generating new + # hashes and for validating passwords against existing hashes. + return '*'; + } + + function CheckPassword($password, $stored_hash) + { + $hash = $this->crypt_private($password, $stored_hash); + if ($hash[0] == '*') + $hash = crypt($password, $stored_hash); + + return $hash == $stored_hash; + } +} + +?> diff --git a/News/includes/lib/YAML.php b/News/includes/lib/YAML.php new file mode 100644 index 0000000..2727d89 --- /dev/null +++ b/News/includes/lib/YAML.php @@ -0,0 +1,154 @@ +parse($line); + } + + return $loader->toArray(); + } + + /** + * Load a file containing YAML and parse it into a PHP array. + * + * If the file cannot be opened, an exception is thrown. If the + * file is read but parsing fails, an empty array is returned. + * + * @param string $filename Filename to load + * @return array PHP array representation of YAML content + * @throws IllegalArgumentException If $filename is invalid + * @throws Horde_Yaml_Exception If the file cannot be opened. + */ + public static function loadFile($filename) + { + if (!is_string($filename) || !strlen($filename)) { + $msg = 'Filename must be a string and cannot be empty'; + throw new InvalidArgumentException($msg); + } + + $stream = @fopen($filename, 'rb'); + if (!$stream) { + throw new Horde_Yaml_Exception('Failed to open file: ', error_get_last()); + } + + return self::loadStream($stream); + } + + /** + * Load YAML from a PHP stream resource. + * + * @param resource $stream PHP stream resource + * @return array PHP array representation of YAML content + */ + public static function loadStream($stream) + { + if (! is_resource($stream) || get_resource_type($stream) != 'stream') { + throw new InvalidArgumentException('Stream must be a stream resource'); + } + + if (is_callable(self::$loadfunc)) { + $array = call_user_func(self::$loadfunc, stream_get_contents($stream)); + return is_array($array) ? $array : array(); + } + + $loader = new Horde_Yaml_Loader; + while (!feof($stream)) { + $loader->parse(stream_get_line($stream, 100000, "\n")); + } + + return $loader->toArray(); + } + + /** + * Dump a PHP array to YAML. + * + * The dump method, when supplied with an array, will do its best + * to convert the array into friendly YAML. + * + * @param array|Traversable $array PHP array or traversable object + * @param integer $options Options to pass to dumper + * @return string YAML representation of $value + */ + public static function dump($value, $options = array()) + { + $dumper = new Horde_Yaml_Dumper; + return $dumper->dump($value, $options); + } + +} diff --git a/News/includes/lib/YAML/Dumper.php b/News/includes/lib/YAML/Dumper.php new file mode 100644 index 0000000..43d07f4 --- /dev/null +++ b/News/includes/lib/YAML/Dumper.php @@ -0,0 +1,213 @@ + 2, + 'wordwrap' => 0); + $this->_options = array_merge($defaults, $options); + + if (! is_int($this->_options['indent'])) { + throw new InvalidArgumentException('Indent must be an integer'); + } + + if (! is_int($this->_options['wordwrap'])) { + throw new InvalidArgumentException('Wordwrap column must be an integer'); + } + + // new YAML document + $dump = "---\n"; + + // iterate through array and yamlize it + foreach ($value as $key => $val) { + $dump .= $this->_yamlize($key, $val, 0, ($value === array_values($value))); + } + return $dump; + } + + /** + * Attempts to convert a key / value array item to YAML + * + * @param string $key The name of the key + * @param string|array $value The value of the item + * @param integer $indent The indent of the current node + * @param boolean $seq Is the item part of a sequence? + * @return string + */ + protected function _yamlize($key, $value, $indent, $seq = false) + { + if ($value instanceof Serializable) { + // Dump serializable objects as !php/object::classname serialize_data + $data = '!php/object::' . get_class($value) . ' ' . $value->serialize(); + $string = $this->_dumpNode($key, $data, $indent); + } elseif (is_array($value) || $value instanceof Traversable) { + // It has children. Make it the right kind of item. + $string = $this->_dumpNode($key, null, $indent); + + // Add the indent. + $indent += $this->_options['indent']; + + // Yamlize the array. + $string .= $this->_yamlizeArray($value, $indent); + } elseif (!is_array($value)) { + // No children. + $string = $this->_dumpNode($key, $value, $indent, $seq); + } + + return $string; + } + + /** + * Attempts to convert an array to YAML + * + * @param array $array The array you want to convert + * @param integer $indent The indent of the current level + * @return string + */ + protected function _yamlizeArray($array, $indent) + { + if (!is_array($array)) { + return false; + } + + $seq = ($array === array_values($array)); + + $string = ''; + foreach ($array as $key => $value) { + $string .= $this->_yamlize($key, $value, $indent, $seq); + } + return $string; + } + + /** + * Returns YAML from a key and a value + * + * @param string $key The name of the key + * @param string $value The value of the item + * @param integer $indent The indent of the current node + * @param boolean $seq Is the item part of a sequence? + * @return string + */ + protected function _dumpNode($key, $value, $indent, $seq = false) + { + // Do some folding here, for blocks. + if (strpos($value, "\n") !== false + || strpos($value, ': ') !== false + || strpos($value, '- ') !== false) { + $value = $this->_doLiteralBlock($value, $indent); + } else { + $value = $this->_fold($value, $indent); + } + + if (is_bool($value)) { + $value = ($value) ? 'true' : 'false'; + } elseif (is_float($value)) { + if (is_nan($value)) { + $value = '.NAN'; + } elseif ($value === INF) { + $value = '.INF'; + } elseif ($value === -INF) { + $value = '-.INF'; + } + } + + $spaces = str_repeat(' ', $indent); + + if ($seq) { + // It's a sequence. + $string = $spaces . '- ' . $value . "\n"; + } else { + // It's mapped. + $string = $spaces . $key . ': ' . $value . "\n"; + } + + return $string; + } + + /** + * Creates a literal block for dumping + * + * @param string $value + * @param integer $indent The value of the indent. + * @return string + */ + protected function _doLiteralBlock($value, $indent) + { + $exploded = explode("\n", $value); + $newValue = '|'; + $indent += $this->_options['indent']; + $spaces = str_repeat(' ', $indent); + foreach ($exploded as $line) { + $newValue .= "\n" . $spaces . trim($line); + } + return $newValue; + } + + /** + * Folds a string of text, if necessary + * + * @param $value The string you wish to fold + * @return string + */ + protected function _fold($value, $indent) + { + // Don't do anything if wordwrap is set to 0 + if (! $this->_options['wordwrap']) { + return (is_string($value) and !is_numeric($value) and !empty($value)) ? '"'.str_replace("\"", "\\\"", $value).'"' : $value ; + } + + if (strlen($value) > $this->_options['wordwrap']) { + $indent += $this->_options['indent']; + $indent = str_repeat(' ', $indent); + $wrapped = wordwrap($value, $this->_options['wordwrap'], "\n$indent"); + $value = ">\n" . $indent . $wrapped; + } + + return $value; + } + +} diff --git a/News/includes/lib/YAML/Exception.php b/News/includes/lib/YAML/Exception.php new file mode 100644 index 0000000..3f232ae --- /dev/null +++ b/News/includes/lib/YAML/Exception.php @@ -0,0 +1,45 @@ +file = $code_or_lasterror['file']; + $this->line = $code_or_lasterror['line']; + $code = $code_or_lasterror['type']; + } else { + $code = $code_or_lasterror; + } + + parent::__construct($message, $code); + } + +} diff --git a/News/includes/lib/YAML/Loader.php b/News/includes/lib/YAML/Loader.php new file mode 100644 index 0000000..15cd08d --- /dev/null +++ b/News/includes/lib/YAML/Loader.php @@ -0,0 +1,751 @@ +_nodeId++); + $base->indent = 0; + $this->_lastNode = $base->id; + } + + /** + * Return the PHP built from all YAML parsed so far. + * + * @return array PHP version of parsed YAML + */ + public function toArray() + { + // Here we travel through node-space and pick out references + // (& and *). + $this->_linkReferences(); + + // Build the PHP array out of node-space. + return $this->_buildArray(); + } + + /** + * Parse a line of a YAML file. + * + * @param string $line The line of YAML to parse. + * @return Horde_Yaml_Node YAML Node + */ + public function parse($line) + { + // Keep track of how many lines we've parsed for friendlier + // error messages. + ++$this->_lineNumber; + + $trimmed = trim($line); + + // If the line starts with a tab (instead of a space), throw a fit. + if (preg_match('/^ *(\t) *[^\t ]/', $line)) { + $msg = "Line {$this->_lineNumber} indent contains a tab. " + . 'YAML only allows spaces for indentation.'; + throw new Horde_Yaml_Exception($msg); + } + + if (!$this->_inBlock && empty($trimmed)) { + return; + } elseif ($this->_inBlock && empty($trimmed)) { + $last =& $this->_allNodes[$this->_lastNode]; + $last->data[key($last->data)] .= "\n"; + } elseif ($trimmed[0] != '#' && substr($trimmed, 0, 3) != '---') { + // Create a new node and get its indent + $node = new Horde_Yaml_Node($this->_nodeId++); + $node->indent = $this->_getIndent($line); + + // Check where the node lies in the hierarchy + if ($this->_lastIndent == $node->indent) { + // If we're in a block, add the text to the parent's data + if ($this->_inBlock) { + $parent =& $this->_allNodes[$this->_lastNode]; + $parent->data[key($parent->data)] .= trim($line) . $this->_blockEnd; + } else { + // The current node's parent is the same as the previous node's + if (isset($this->_allNodes[$this->_lastNode])) { + $node->parent = $this->_allNodes[$this->_lastNode]->parent; + } + } + } elseif ($this->_lastIndent < $node->indent) { + if ($this->_inBlock) { + $parent =& $this->_allNodes[$this->_lastNode]; + $parent->data[key($parent->data)] .= trim($line) . $this->_blockEnd; + } elseif (!$this->_inBlock) { + // The current node's parent is the previous node + $node->parent = $this->_lastNode; + + // If the value of the last node's data was > or | + // we need to start blocking i.e. taking in all + // lines as a text value until we drop our indent. + $parent =& $this->_allNodes[$node->parent]; + $this->_allNodes[$node->parent]->children = true; + if (is_array($parent->data)) { + if (isset($parent->data[key($parent->data)])) { + $chk = $parent->data[key($parent->data)]; + if ($chk === '>') { + $this->_inBlock = true; + $this->_blockEnd = ''; + $parent->data[key($parent->data)] = + str_replace('>', '', $parent->data[key($parent->data)]); + $parent->data[key($parent->data)] .= trim($line) . ' '; + $this->_allNodes[$node->parent]->children = false; + $this->_lastIndent = $node->indent; + } elseif ($chk === '|') { + $this->_inBlock = true; + $this->_blockEnd = "\n"; + $parent->data[key($parent->data)] = + str_replace('|', '', $parent->data[key($parent->data)]); + $parent->data[key($parent->data)] .= trim($line) . "\n"; + $this->_allNodes[$node->parent]->children = false; + $this->_lastIndent = $node->indent; + } + } + } + } + } elseif ($this->_lastIndent > $node->indent) { + // Any block we had going is dead now + if ($this->_inBlock) { + $this->_inBlock = false; + if ($this->_blockEnd == "\n") { + $last =& $this->_allNodes[$this->_lastNode]; + $last->data[key($last->data)] = + trim($last->data[key($last->data)]); + } + } + + // We don't know the parent of the node so we have to + // find it + foreach ($this->_indentSort[$node->indent] as $n) { + if ($n->indent == $node->indent) { + $node->parent = $n->parent; + } + } + } + + if (!$this->_inBlock) { + // Set these properties with information from our + // current node + $this->_lastIndent = $node->indent; + + // Set the last node + $this->_lastNode = $node->id; + + // Parse the YAML line and return its data + $node->data = $this->_parseLine($line); + + // Add the node to the master list + $this->_allNodes[$node->id] = $node; + + // Add a reference to the parent list + $this->_allParent[intval($node->parent)][] = $node->id; + + // Add a reference to the node in an indent array + $this->_indentSort[$node->indent][] =& $this->_allNodes[$node->id]; + + // Add a reference to the node in a References array + // if this node has a YAML reference in it. + $is_array = is_array($node->data); + $key = key($node->data); + $isset = isset($node->data[$key]); + if ($isset) { + $nodeval = $node->data[$key]; + } + if (($is_array && $isset && !is_array($nodeval) && !is_object($nodeval)) + && (strlen($nodeval) && (false))) { # $nodeval[0] == '&' || $nodeval[0] == '*') && $nodeval[1] != ' ')) { + $this->_haveRefs[] =& $this->_allNodes[$node->id]; + } elseif ($is_array && $isset && is_array($nodeval)) { + // Incomplete reference making code. Needs to be + // cleaned up. + foreach ($node->data[$key] as $d) { + if (!is_array($d) && strlen($d) && (false)) { # ($d[0] == '&' || $d[0] == '*') && $d[1] != ' ')) { + $this->_haveRefs[] =& $this->_allNodes[$node->id]; + } + } + } + } + } + } + + /** + * Finds and returns the indentation of a YAML line + * + * @param string $line A line from the YAML file + * @return int Indentation level + */ + protected function _getIndent($line) + { + if (preg_match('/^\s+/', $line, $match)) { + return strlen($match[0]); + } else { + return 0; + } + } + + /** + * Parses YAML code and returns an array for a node + * + * @param string $line A line from the YAML file + * @return array + */ + protected function _parseLine($line) + { + $array = array(); + + $line = trim($line); + if (preg_match('/^-(.*):$/', $line)) { + // It's a mapped sequence + $key = trim(substr(substr($line, 1), 0, -1)); + $array[$key] = ''; + } elseif ($line[0] == '-' && substr($line, 0, 3) != '---') { + // It's a list item but not a new stream + if (strlen($line) > 1) { + // Set the type of the value. Int, string, etc + $array[] = $this->_toType(trim(substr($line, 1))); + } else { + $array[] = array(); + } + } elseif (preg_match('/^(.+):/', $line, $key)) { + // It's a key/value pair most likely + // If the key is in double quotes pull it out + if (preg_match('/^(["\'](.*)["\'](\s)*:)/', $line, $matches)) { + $value = trim(str_replace($matches[1], '', $line)); + $key = $matches[2]; + } else { + // Do some guesswork as to the key and the value + $explode = explode(':', $line); + $key = trim(array_shift($explode)); + $value = trim(implode(':', $explode)); + } + + // Set the type of the value. Int, string, etc + $value = $this->_toType($value); + if (empty($key)) { + $array[] = $value; + } else { + $array[$key] = $value; + } + } + + return $array; + } + + /** + * Finds the type of the passed value, returns the value as the new type. + * + * @param string $value + * @return mixed + */ + protected function _toType($value) + { + // Check for PHP specials + self::_unserialize($value); + if (!is_scalar($value)) { + return $value; + } + + // Used in a lot of cases. + $lower_value = strtolower($value); + + if (preg_match('/^("(.*)"|\'(.*)\')/', $value, $matches)) { + $value = (string)str_replace(array('\'\'', '\\\''), "'", end($matches)); + $value = str_replace('\\"', '"', $value); + } elseif (preg_match('/^\\[(\s*)\\]$/', $value)) { + // empty inline mapping + $value = array(); + } elseif (preg_match('/^\\[(.+)\\]$/', $value, $matches)) { + // Inline Sequence + + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($matches[1]); + + // Propogate value array + $value = array(); + foreach ($explode as $v) { + $value[] = $this->_toType($v); + } + } elseif (preg_match('/^\\{(\s*)\\}$/', $value)) { + // empty inline mapping + $value = array(); + } elseif (strpos($value, ': ') !== false && !preg_match('/^{(.+)/', $value)) { + // inline mapping + $array = explode(': ', $value); + $key = trim($array[0]); + array_shift($array); + $value = trim(implode(': ', $array)); + $value = $this->_toType($value); + $value = array($key => $value); + } elseif (preg_match("/{(.+)}$/", $value, $matches)) { + // Inline Mapping + + // Take out strings sequences and mappings + $explode = $this->_inlineEscape($matches[1]); + + // Propogate value array + $array = array(); + foreach ($explode as $v) { + $array = $array + $this->_toType($v); + } + $value = $array; + } elseif ($lower_value == 'null' || $value == '' || $value == '~') { + $value = null; + } elseif ($lower_value == '.nan') { + $value = NAN; + } elseif ($lower_value == '.inf') { + $value = INF; + } elseif ($lower_value == '-.inf') { + $value = -INF; + } elseif (is_numeric($value) and !substr_count($value, ".")) { + $value = (int)$value; + } elseif (in_array($lower_value, + array('true', 'on', '+', 'yes', 'y'))) { + $value = true; + } elseif (in_array($lower_value, + array('false', 'off', '-', 'no', 'n'))) { + $value = false; + } elseif (is_numeric($value)) { + $value = (float)$value; + } else { + // Just a normal string, right? + if (($pos = strpos($value, '#')) !== false) { + $value = substr($value, 0, $pos); + } + $value = trim($value); + } + + return $value; + } + + /** + * Handle PHP serialized data. + * + * @param string &$data Data to check for serialized PHP types. + */ + protected function _unserialize(&$data) + { + if (substr($data, 0, 5) != '!php/') { + return; + } + + $first_space = strpos($data, ' '); + $type = substr($data, 5, $first_space - 5); + $class = null; + if (strpos($type, '::') !== false) { + list($type, $class) = explode('::', $type); + + if (!in_array($class, Horde_Yaml::$allowedClasses)) { + throw new Horde_Yaml_Exception("$class is not in the list of allowed classes"); + } + } + + switch ($type) { + case 'object': + if (!class_exists($class)) { + throw new Horde_Yaml_Exception("$class is not defined"); + } + + $reflector = new ReflectionClass($class); + if (!$reflector->implementsInterface('Serializable')) { + throw new Horde_Yaml_Exception("$class does not implement Serializable"); + } + + $class_data = substr($data, $first_space + 1); + $serialized = 'C:' . strlen($class) . ':"' . $class . '":' . strlen($class_data) . ':{' . $class_data . '}'; + $data = unserialize($serialized); + break; + + case 'array': + case 'hash': + $array_data = substr($data, $first_space + 1); + $array_data = Horde_Yaml::load('a: ' . $array_data); + + if (is_null($class)) { + $data = $array_data['a']; + } else { + if (!class_exists($class)) { + throw new Horde_Yaml_Exception("$class is not defined"); + } + + $array = new $class; + if (!$array instanceof ArrayAccess) { + throw new Horde_Yaml_Exception("$class does not implement ArrayAccess"); + } + + foreach ($array_data['a'] as $key => $val) { + $array[$key] = $val; + } + + $data = $array; + } + break; + } + } + + /** + * Used in inlines to check for more inlines or quoted strings + * + * @todo There should be a cleaner way to do this. While + * pure sequences seem to be nesting just fine, + * pure mappings and mappings with sequences inside + * can't go very deep. This needs to be fixed. + * + * @param string $inline Inline data + * @return array + */ + protected function _inlineEscape($inline) + { + $saved_strings = array(); + + // Check for strings + $regex = '/(?:(")|(?:\'))((?(1)[^"]+|[^\']+))(?(1)"|\')/'; + if (preg_match_all($regex, $inline, $strings)) { + $saved_strings = $strings[0]; + $inline = preg_replace($regex, 'YAMLString', $inline); + } + + // Check for sequences + if (preg_match_all('/\[(.+)\]/U', $inline, $seqs)) { + $inline = preg_replace('/\[(.+)\]/U', 'YAMLSeq', $inline); + $seqs = $seqs[0]; + } + + // Check for mappings + if (preg_match_all('/{(.+)}/U', $inline, $maps)) { + $inline = preg_replace('/{(.+)}/U', 'YAMLMap', $inline); + $maps = $maps[0]; + } + + $explode = explode(', ', $inline); + + // Re-add the sequences + if (!empty($seqs)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value, 'YAMLSeq') !== false) { + $explode[$key] = str_replace('YAMLSeq', $seqs[$i], $value); + ++$i; + } + } + } + + // Re-add the mappings + if (!empty($maps)) { + $i = 0; + foreach ($explode as $key => $value) { + if (strpos($value, 'YAMLMap') !== false) { + $explode[$key] = str_replace('YAMLMap', $maps[$i], $value); + ++$i; + } + } + } + + // Re-add the strings + if (!empty($saved_strings)) { + $i = 0; + foreach ($explode as $key => $value) { + while (strpos($value, 'YAMLString') !== false) { + $explode[$key] = preg_replace('/YAMLString/', $saved_strings[$i], $value, 1); + ++$i; + $value = $explode[$key]; + } + } + } + + return $explode; + } + + /** + * Builds the PHP array from all the YAML nodes we've gathered + * + * @return array + */ + protected function _buildArray() + { + $trunk = array(); + if (!isset($this->_indentSort[0])) { + return $trunk; + } + + foreach ($this->_indentSort[0] as $n) { + if (empty($n->parent)) { + $this->_nodeArrayizeData($n); + + // Check for references and copy the needed data to complete them. + $this->_makeReferences($n); + + // Merge our data with the big array we're building + $trunk = $this->_array_kmerge($trunk, $n->data); + } + } + + return $trunk; + } + + /** + * Traverses node-space and sets references (& and *) accordingly + * + * @return bool + */ + protected function _linkReferences() + { + if (is_array($this->_haveRefs)) { + foreach ($this->_haveRefs as $node) { + if (!empty($node->data)) { + $key = key($node->data); + // If it's an array, don't check. + if (is_array($node->data[$key])) { + foreach ($node->data[$key] as $k => $v) { + $this->_linkRef($node, $key, $k, $v); + } + } else { + $this->_linkRef($node, $key); + } + } + } + } + + return true; + } + + /** + * Helper for _linkReferences() + * + * @param Horde_Yaml_Node $n Node + * @param string $k Key + * @param mixed $v Value + * @return void + */ + function _linkRef(&$n, $key, $k = null, $v = null) + { + if (empty($k) && empty($v)) { + // Look for &refs + if (preg_match('/^&([^ ]+)/', $n->data[$key], $matches)) { + // Flag the node so we know it's a reference + $this->_allNodes[$n->id]->ref = substr($matches[0], 1); + $this->_allNodes[$n->id]->data[$key] = + substr($n->data[$key], strlen($matches[0]) + 1); + // Look for *refs + } elseif (preg_match('/^\*([^ ]+)/', $n->data[$key], $matches)) { + $ref = substr($matches[0], 1); + // Flag the node as having a reference + $this->_allNodes[$n->id]->refKey = $ref; + } + } elseif (!empty($k) && !empty($v)) { + if (preg_match('/^&([^ ]+)/', $v, $matches)) { + // Flag the node so we know it's a reference + $this->_allNodes[$n->id]->ref = substr($matches[0], 1); + $this->_allNodes[$n->id]->data[$key][$k] = + substr($v, strlen($matches[0]) + 1); + // Look for *refs + } elseif (preg_match('/^\*([^ ]+)/', $v, $matches)) { + $ref = substr($matches[0], 1); + // Flag the node as having a reference + $this->_allNodes[$n->id]->refKey = $ref; + } + } + } + + /** + * Finds the children of a node and aids in the building of the PHP array + * + * @param int $nid The id of the node whose children we're gathering + * @return array + */ + protected function _gatherChildren($nid) + { + $return = array(); + $node =& $this->_allNodes[$nid]; + if (is_array ($this->_allParent[$node->id])) { + foreach ($this->_allParent[$node->id] as $nodeZ) { + $z =& $this->_allNodes[$nodeZ]; + // We found a child + $this->_nodeArrayizeData($z); + + // Check for references + $this->_makeReferences($z); + + // Merge with the big array we're returning, the big + // array being all the data of the children of our + // parent node + $return = $this->_array_kmerge($return, $z->data); + } + } + return $return; + } + + /** + * Turns a node's data and its children's data into a PHP array + * + * @param array $node The node which you want to arrayize + * @return boolean + */ + protected function _nodeArrayizeData(&$node) + { + if ($node->children == true) { + if (is_array($node->data)) { + // This node has children, so we need to find them + $children = $this->_gatherChildren($node->id); + + // We've gathered all our children's data and are ready to use it + $key = key($node->data); + $key = empty($key) ? 0 : $key; + // If it's an array, add to it of course + if (isset($node->data[$key])) { + if (is_array($node->data[$key])) { + $node->data[$key] = $this->_array_kmerge($node->data[$key], $children); + } else { + $node->data[$key] = $children; + } + } else { + $node->data[$key] = $children; + } + } else { + // Same as above, find the children of this node + $children = $this->_gatherChildren($node->id); + $node->data = array(); + $node->data[] = $children; + } + } else { + // The node is a single string. See if we need to unserialize it. + if (is_array($node->data)) { + $key = key($node->data); + $key = empty($key) ? 0 : $key; + + if (!isset($node->data[$key]) || is_array($node->data[$key]) || is_object($node->data[$key])) { + return true; + } + + self::_unserialize($node->data[$key]); + } elseif (is_string($node->data)) { + self::_unserialize($node->data); + } + } + + // We edited $node by reference, so just return true + return true; + } + + /** + * Traverses node-space and copies references to / from this object. + * + * @param Horde_Yaml_Node $z A node whose references we wish to make real + * @return bool + */ + protected function _makeReferences(&$z) + { + // It is a reference + if (isset($z->ref)) { + $key = key($z->data); + // Copy the data to this object for easy retrieval later + $this->ref[$z->ref] =& $z->data[$key]; + // It has a reference + } elseif (isset($z->refKey)) { + if (isset($this->ref[$z->refKey])) { + $key = key($z->data); + // Copy the data from this object to make the node a real reference + $z->data[$key] =& $this->ref[$z->refKey]; + } + } + + return true; + } + + /** + * Merges two arrays, maintaining numeric keys. If two numeric + * keys clash, the second one will be appended to the resulting + * array. If string keys clash, the last one wins. + * + * @param array $arr1 + * @param array $arr2 + * @return array + */ + protected function _array_kmerge($arr1, $arr2) + { + while (list($key, $val) = each($arr2)) { + if (isset($arr1[$key]) && is_int($key)) { + $arr1[] = $val; + } else { + $arr1[$key] = $val; + } + } + + return $arr1; + } + +} diff --git a/News/includes/lib/YAML/Node.php b/News/includes/lib/YAML/Node.php new file mode 100644 index 0000000..f6d8085 --- /dev/null +++ b/News/includes/lib/YAML/Node.php @@ -0,0 +1,58 @@ +id = $nodeId; + } + +} diff --git a/News/includes/lib/gettext/gettext.php b/News/includes/lib/gettext/gettext.php new file mode 100755 index 0000000..5064047 --- /dev/null +++ b/News/includes/lib/gettext/gettext.php @@ -0,0 +1,432 @@ +. + Copyright (c) 2005 Nico Kaiser + + This file is part of PHP-gettext. + + PHP-gettext is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + PHP-gettext is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with PHP-gettext; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +*/ + +/** + * Provides a simple gettext replacement that works independently from + * the system's gettext abilities. + * It can read MO files and use them for translating strings. + * The files are passed to gettext_reader as a Stream (see streams.php) + * + * This version has the ability to cache all strings and translations to + * speed up the string lookup. + * While the cache is enabled by default, it can be switched off with the + * second parameter in the constructor (e.g. whenusing very large MO files + * that you don't want to keep in memory) + */ +class gettext_reader { + //public: + var $error = 0; // public variable that holds error code (0 if no error) + + //private: + var $BYTEORDER = 0; // 0: low endian, 1: big endian + var $STREAM = NULL; + var $short_circuit = false; + var $enable_cache = false; + var $originals = NULL; // offset of original table + var $translations = NULL; // offset of translation table + var $pluralheader = NULL; // cache header field for plural forms + var $total = 0; // total string count + var $table_originals = NULL; // table for original strings (offsets) + var $table_translations = NULL; // table for translated strings (offsets) + var $cache_translations = NULL; // original -> translation mapping + + + /* Methods */ + + + /** + * Reads a 32bit Integer from the Stream + * + * @access private + * @return Integer from the Stream + */ + function readint() { + if ($this->BYTEORDER == 0) { + // low endian + $input=unpack('V', $this->STREAM->read(4)); + return array_shift($input); + } else { + // big endian + $input=unpack('N', $this->STREAM->read(4)); + return array_shift($input); + } + } + + function read($bytes) { + return $this->STREAM->read($bytes); + } + + /** + * Reads an array of Integers from the Stream + * + * @param int count How many elements should be read + * @return Array of Integers + */ + function readintarray($count) { + if ($this->BYTEORDER == 0) { + // low endian + return unpack('V'.$count, $this->STREAM->read(4 * $count)); + } else { + // big endian + return unpack('N'.$count, $this->STREAM->read(4 * $count)); + } + } + + /** + * Constructor + * + * @param object Reader the StreamReader object + * @param boolean enable_cache Enable or disable caching of strings (default on) + */ + function gettext_reader($Reader, $enable_cache = true) { + // If there isn't a StreamReader, turn on short circuit mode. + if (! $Reader || isset($Reader->error) ) { + $this->short_circuit = true; + return; + } + + // Caching can be turned off + $this->enable_cache = $enable_cache; + + $MAGIC1 = "\x95\x04\x12\xde"; + $MAGIC2 = "\xde\x12\x04\x95"; + + $this->STREAM = $Reader; + $magic = $this->read(4); + if ($magic == $MAGIC1) { + $this->BYTEORDER = 1; + } elseif ($magic == $MAGIC2) { + $this->BYTEORDER = 0; + } else { + $this->error = 1; // not MO file + return false; + } + + // FIXME: Do we care about revision? We should. + $revision = $this->readint(); + + $this->total = $this->readint(); + $this->originals = $this->readint(); + $this->translations = $this->readint(); + } + + /** + * Loads the translation tables from the MO file into the cache + * If caching is enabled, also loads all strings into a cache + * to speed up translation lookups + * + * @access private + */ + function load_tables() { + if (is_array($this->cache_translations) && + is_array($this->table_originals) && + is_array($this->table_translations)) + return; + + /* get original and translations tables */ + if (!is_array($this->table_originals)) { + $this->STREAM->seekto($this->originals); + $this->table_originals = $this->readintarray($this->total * 2); + } + if (!is_array($this->table_translations)) { + $this->STREAM->seekto($this->translations); + $this->table_translations = $this->readintarray($this->total * 2); + } + + if ($this->enable_cache) { + $this->cache_translations = array (); + /* read all strings in the cache */ + for ($i = 0; $i < $this->total; $i++) { + $this->STREAM->seekto($this->table_originals[$i * 2 + 2]); + $original = $this->STREAM->read($this->table_originals[$i * 2 + 1]); + $this->STREAM->seekto($this->table_translations[$i * 2 + 2]); + $translation = $this->STREAM->read($this->table_translations[$i * 2 + 1]); + $this->cache_translations[$original] = $translation; + } + } + } + + /** + * Returns a string from the "originals" table + * + * @access private + * @param int num Offset number of original string + * @return string Requested string if found, otherwise '' + */ + function get_original_string($num) { + $length = $this->table_originals[$num * 2 + 1]; + $offset = $this->table_originals[$num * 2 + 2]; + if (! $length) + return ''; + $this->STREAM->seekto($offset); + $data = $this->STREAM->read($length); + return (string)$data; + } + + /** + * Returns a string from the "translations" table + * + * @access private + * @param int num Offset number of original string + * @return string Requested string if found, otherwise '' + */ + function get_translation_string($num) { + $length = $this->table_translations[$num * 2 + 1]; + $offset = $this->table_translations[$num * 2 + 2]; + if (! $length) + return ''; + $this->STREAM->seekto($offset); + $data = $this->STREAM->read($length); + return (string)$data; + } + + /** + * Binary search for string + * + * @access private + * @param string string + * @param int start (internally used in recursive function) + * @param int end (internally used in recursive function) + * @return int string number (offset in originals table) + */ + function find_string($string, $start = -1, $end = -1) { + if (($start == -1) or ($end == -1)) { + // find_string is called with only one parameter, set start end end + $start = 0; + $end = $this->total; + } + if (abs($start - $end) <= 1) { + // We're done, now we either found the string, or it doesn't exist + $txt = $this->get_original_string($start); + if ($string == $txt) + return $start; + else + return -1; + } else if ($start > $end) { + // start > end -> turn around and start over + return $this->find_string($string, $end, $start); + } else { + // Divide table in two parts + $half = (int)(($start + $end) / 2); + $cmp = strcmp($string, $this->get_original_string($half)); + if ($cmp == 0) + // string is exactly in the middle => return it + return $half; + else if ($cmp < 0) + // The string is in the upper half + return $this->find_string($string, $start, $half); + else + // The string is in the lower half + return $this->find_string($string, $half, $end); + } + } + + /** + * Translates a string + * + * @access public + * @param string string to be translated + * @return string translated string (or original, if not found) + */ + function translate($string) { + if ($this->short_circuit) + return $string; + $this->load_tables(); + + if ($this->enable_cache) { + // Caching enabled, get translated string from cache + if (array_key_exists($string, $this->cache_translations)) + return $this->cache_translations[$string]; + else + return $string; + } else { + // Caching not enabled, try to find string + $num = $this->find_string($string); + if ($num == -1) + return $string; + else + return $this->get_translation_string($num); + } + } + + /** + * Sanitize plural form expression for use in PHP eval call. + * + * @access private + * @return string sanitized plural form expression + */ + function sanitize_plural_expression($expr) { + // Get rid of disallowed characters. + $expr = preg_replace('@[^a-zA-Z0-9_:;\(\)\?\|\&=!<>+*/\%-]@', '', $expr); + + // Add parenthesis for tertiary '?' operator. + $expr .= ';'; + $res = ''; + $p = 0; + for ($i = 0; $i < strlen($expr); $i++) { + $ch = $expr[$i]; + switch ($ch) { + case '?': + $res .= ' ? ('; + $p++; + break; + case ':': + $res .= ') : ('; + break; + case ';': + $res .= str_repeat( ')', $p) . ';'; + $p = 0; + break; + default: + $res .= $ch; + } + } + return $res; + } + + /** + * Parse full PO header and extract only plural forms line. + * + * @access private + * @return string verbatim plural form header field + */ + function extract_plural_forms_header_from_po_header($header) { + if (preg_match("/(^|\n)plural-forms: ([^\n]*)\n/i", $header, $regs)) + $expr = $regs[2]; + else + $expr = "nplurals=2; plural=n == 1 ? 0 : 1;"; + return $expr; + } + + /** + * Get possible plural forms from MO header + * + * @access private + * @return string plural form header + */ + function get_plural_forms() { + // lets assume message number 0 is header + // this is true, right? + $this->load_tables(); + + // cache header field for plural forms + if (! is_string($this->pluralheader)) { + if ($this->enable_cache) { + $header = $this->cache_translations[""]; + } else { + $header = $this->get_translation_string(0); + } + $expr = $this->extract_plural_forms_header_from_po_header($header); + $this->pluralheader = $this->sanitize_plural_expression($expr); + } + return $this->pluralheader; + } + + /** + * Detects which plural form to take + * + * @access private + * @param n count + * @return int array index of the right plural form + */ + function select_string($n) { + $string = $this->get_plural_forms(); + $string = str_replace('nplurals',"\$total",$string); + $string = str_replace("n",$n,$string); + $string = str_replace('plural',"\$plural",$string); + + $total = 0; + $plural = 0; + + eval("$string"); + if ($plural >= $total) $plural = $total - 1; + return $plural; + } + + /** + * Plural version of gettext + * + * @access public + * @param string single + * @param string plural + * @param string number + * @return translated plural form + */ + function ngettext($single, $plural, $number) { + if ($this->short_circuit) { + if ($number != 1) + return $plural; + else + return $single; + } + + // find out the appropriate form + $select = $this->select_string($number); + + // this should contains all strings separated by NULLs + $key = $single . chr(0) . $plural; + + + if ($this->enable_cache) { + if (! array_key_exists($key, $this->cache_translations)) { + return ($number != 1) ? $plural : $single; + } else { + $result = $this->cache_translations[$key]; + $list = explode(chr(0), $result); + return $list[$select]; + } + } else { + $num = $this->find_string($key); + if ($num == -1) { + return ($number != 1) ? $plural : $single; + } else { + $result = $this->get_translation_string($num); + $list = explode(chr(0), $result); + return $list[$select]; + } + } + } + + function pgettext($context, $msgid) { + $key = $context . chr(4) . $msgid; + $ret = $this->translate($key); + if (strpos($ret, "\004") !== FALSE) { + return $msgid; + } else { + return $ret; + } + } + + function npgettext($context, $singular, $plural, $number) { + $key = $context . chr(4) . $singular; + $ret = $this->ngettext($key, $plural, $number); + if (strpos($ret, "\004") !== FALSE) { + return $singular; + } else { + return $ret; + } + + } +} + +?> diff --git a/News/includes/lib/gettext/streams.php b/News/includes/lib/gettext/streams.php new file mode 100755 index 0000000..3cdc158 --- /dev/null +++ b/News/includes/lib/gettext/streams.php @@ -0,0 +1,167 @@ +. + + This file is part of PHP-gettext. + + PHP-gettext is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + PHP-gettext is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with PHP-gettext; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +*/ + + + // Simple class to wrap file streams, string streams, etc. + // seek is essential, and it should be byte stream +class StreamReader { + // should return a string [FIXME: perhaps return array of bytes?] + function read($bytes) { + return false; + } + + // should return new position + function seekto($position) { + return false; + } + + // returns current position + function currentpos() { + return false; + } + + // returns length of entire stream (limit for seekto()s) + function length() { + return false; + } +}; + +class StringReader { + var $_pos; + var $_str; + + function StringReader($str='') { + $this->_str = $str; + $this->_pos = 0; + } + + function read($bytes) { + $data = substr($this->_str, $this->_pos, $bytes); + $this->_pos += $bytes; + if (strlen($this->_str)<$this->_pos) + $this->_pos = strlen($this->_str); + + return $data; + } + + function seekto($pos) { + $this->_pos = $pos; + if (strlen($this->_str)<$this->_pos) + $this->_pos = strlen($this->_str); + return $this->_pos; + } + + function currentpos() { + return $this->_pos; + } + + function length() { + return strlen($this->_str); + } + +}; + + +class FileReader { + var $_pos; + var $_fd; + var $_length; + + function FileReader($filename) { + if (file_exists($filename)) { + + $this->_length=filesize($filename); + $this->_pos = 0; + $this->_fd = fopen($filename,'rb'); + if (!$this->_fd) { + $this->error = 3; // Cannot read file, probably permissions + return false; + } + } else { + $this->error = 2; // File doesn't exist + return false; + } + } + + function read($bytes) { + if ($bytes) { + fseek($this->_fd, $this->_pos); + + // PHP 5.1.1 does not read more than 8192 bytes in one fread() + // the discussions at PHP Bugs suggest it's the intended behaviour + $data = ''; + while ($bytes > 0) { + $chunk = fread($this->_fd, $bytes); + $data .= $chunk; + $bytes -= strlen($chunk); + } + $this->_pos = ftell($this->_fd); + + return $data; + } else return ''; + } + + function seekto($pos) { + fseek($this->_fd, $pos); + $this->_pos = ftell($this->_fd); + return $this->_pos; + } + + function currentpos() { + return $this->_pos; + } + + function length() { + return $this->_length; + } + + function close() { + fclose($this->_fd); + } + +}; + +// Preloads entire file in memory first, then creates a StringReader +// over it (it assumes knowledge of StringReader internals) +class CachedFileReader extends StringReader { + function CachedFileReader($filename) { + if (file_exists($filename)) { + + $length=filesize($filename); + $fd = fopen($filename,'rb'); + + if (!$fd) { + $this->error = 3; // Cannot read file, probably permissions + return false; + } + $this->_str = fread($fd, $length); + fclose($fd); + + } else { + $this->error = 2; // File doesn't exist + return false; + } + } +}; + + +?> diff --git a/News/includes/lib/gz.php b/News/includes/lib/gz.php new file mode 100755 index 0000000..111cf68 --- /dev/null +++ b/News/includes/lib/gz.php @@ -0,0 +1,39 @@ + 0 ) + exit("GTFO."); + + if (extension_loaded('zlib') and USE_ZLIB and ini_get('zlib.output_compression') === 'On') { + @ini_set('zlib.output_compression', 'Off'); + ob_start("ob_gzhandler"); + header("Content-Encoding: gzip"); + } else + ob_start(); + + header("Content-Type: application/x-javascript"); + + if (strpos($_GET['file'], "/themes/") === 0) { + # Constant: MAIN_DIR + # Absolute path to the Chyrp root + define('MAIN_DIR', dirname(dirname(dirname(__FILE__)))); + + header("Last-Modified: ".@date("r", filemtime(MAIN_DIR.$_GET['file']))); + + if (file_exists(MAIN_DIR.$_GET['file'])) + readfile(MAIN_DIR.$_GET['file']); + else + echo "alert('File not found: ".addslashes($_GET['file'])."')"; + } elseif (file_exists($_GET['file'])) { + header("Last-Modified: ".@date("r", filemtime($_GET['file']))); + readfile($_GET['file']); + } else + echo "alert('File not found: ".addslashes($_GET['file'])."')"; + + ob_end_flush(); diff --git a/News/includes/lib/ixr.php b/News/includes/lib/ixr.php new file mode 100755 index 0000000..8dc1608 --- /dev/null +++ b/News/includes/lib/ixr.php @@ -0,0 +1,1343 @@ +data = $data; + if (!$type) { + $type = $this->calculateType(); + } + $this->type = $type; + if ($type == 'struct') { + // Turn all the values in the array in to new IXR_Value objects + foreach ($this->data as $key => $value) { + $this->data[$key] = new IXR_Value($value); + } + } + if ($type == 'array') { + for ($i = 0, $j = count($this->data); $i < $j; $i++) { + $this->data[$i] = new IXR_Value($this->data[$i]); + } + } + } + + function calculateType() + { + if ($this->data === true || $this->data === false) { + return 'boolean'; + } + if (is_integer($this->data)) { + return 'int'; + } + if (is_double($this->data)) { + return 'double'; + } + + // Deal with IXR object types base64 and date + if (is_object($this->data) && is_a($this->data, 'IXR_Date')) { + return 'date'; + } + if (is_object($this->data) && is_a($this->data, 'IXR_Base64')) { + return 'base64'; + } + + // If it is a normal PHP object convert it in to a struct + if (is_object($this->data)) { + $this->data = get_object_vars($this->data); + return 'struct'; + } + if (!is_array($this->data)) { + return 'string'; + } + + // We have an array - is it an array or a struct? + if ($this->isStruct($this->data)) { + return 'struct'; + } else { + return 'array'; + } + } + + function getXml() + { + // Return XML for this value + switch ($this->type) { + case 'boolean': + return ''.(($this->data) ? '1' : '0').''; + break; + case 'int': + return ''.$this->data.''; + break; + case 'double': + return ''.$this->data.''; + break; + case 'string': + return ''.htmlspecialchars($this->data).''; + break; + case 'array': + $return = ''."\n"; + foreach ($this->data as $item) { + $return .= ' '.$item->getXml()."\n"; + } + $return .= ''; + return $return; + break; + case 'struct': + $return = ''."\n"; + foreach ($this->data as $name => $value) { + $return .= " $name"; + $return .= $value->getXml()."\n"; + } + $return .= ''; + return $return; + break; + case 'date': + case 'base64': + return $this->data->getXml(); + break; + } + return false; + } + + /** + * Checks whether or not the supplied array is a struct or not + * + * @param unknown_type $array + * @return boolean + */ + function isStruct($array) + { + $expected = 0; + foreach ($array as $key => $value) { + if ((string)$key != (string)$expected) { + return true; + } + $expected++; + } + return false; + } +} + +/** + * IXR_MESSAGE + * + * @package IXR + * @since 1.5 + * + */ +class IXR_Message +{ + var $message; + var $messageType; // methodCall / methodResponse / fault + var $faultCode; + var $faultString; + var $methodName; + var $params; + + // Current variable stacks + var $_arraystructs = array(); // The stack used to keep track of the current array/struct + var $_arraystructstypes = array(); // Stack keeping track of if things are structs or array + var $_currentStructName = array(); // A stack as well + var $_param; + var $_value; + var $_currentTag; + var $_currentTagContents; + // The XML parser + var $_parser; + + function IXR_Message($message) + { + $this->message =& $message; + } + + function parse() + { + // first remove the XML declaration + // merged from WP #10698 - this method avoids the RAM usage of preg_replace on very large messages + $header = preg_replace( '/<\?xml.*?\?'.'>/', '', substr($this->message, 0, 100), 1); + $this->message = substr_replace($this->message, $header, 0, 100); + if (trim($this->message) == '') { + return false; + } + $this->_parser = xml_parser_create(); + // Set XML parser to take the case of tags in to account + xml_parser_set_option($this->_parser, XML_OPTION_CASE_FOLDING, false); + // Set XML parser callback functions + xml_set_object($this->_parser, $this); + xml_set_element_handler($this->_parser, 'tag_open', 'tag_close'); + xml_set_character_data_handler($this->_parser, 'cdata'); + $chunk_size = 262144; // 256Kb, parse in chunks to avoid the RAM usage on very large messages + do { + if (strlen($this->message) <= $chunk_size) { + $final = true; + } + $part = substr($this->message, 0, $chunk_size); + $this->message = substr($this->message, $chunk_size); + if (!xml_parse($this->_parser, $part, $final)) { + return false; + } + if ($final) { + break; + } + } while (true); + xml_parser_free($this->_parser); + + // Grab the error messages, if any + if ($this->messageType == 'fault') { + $this->faultCode = $this->params[0]['faultCode']; + $this->faultString = $this->params[0]['faultString']; + } + return true; + } + + function tag_open($parser, $tag, $attr) + { + $this->_currentTagContents = ''; + $this->currentTag = $tag; + switch($tag) { + case 'methodCall': + case 'methodResponse': + case 'fault': + $this->messageType = $tag; + break; + /* Deal with stacks of arrays and structs */ + case 'data': // data is to all intents and puposes more interesting than array + $this->_arraystructstypes[] = 'array'; + $this->_arraystructs[] = array(); + break; + case 'struct': + $this->_arraystructstypes[] = 'struct'; + $this->_arraystructs[] = array(); + break; + } + } + + function cdata($parser, $cdata) + { + $this->_currentTagContents .= $cdata; + } + + function tag_close($parser, $tag) + { + $valueFlag = false; + switch($tag) { + case 'int': + case 'i4': + $value = (int)trim($this->_currentTagContents); + $valueFlag = true; + break; + case 'double': + $value = (double)trim($this->_currentTagContents); + $valueFlag = true; + break; + case 'string': + $value = (string)trim($this->_currentTagContents); + $valueFlag = true; + break; + case 'dateTime.iso8601': + $value = new IXR_Date(trim($this->_currentTagContents)); + $valueFlag = true; + break; + case 'value': + // "If no type is indicated, the type is string." + if (trim($this->_currentTagContents) != '') { + $value = (string)$this->_currentTagContents; + $valueFlag = true; + } + break; + case 'boolean': + $value = (boolean)trim($this->_currentTagContents); + $valueFlag = true; + break; + case 'base64': + $value = base64_decode($this->_currentTagContents); + $valueFlag = true; + break; + /* Deal with stacks of arrays and structs */ + case 'data': + case 'struct': + $value = array_pop($this->_arraystructs); + array_pop($this->_arraystructstypes); + $valueFlag = true; + break; + case 'member': + array_pop($this->_currentStructName); + break; + case 'name': + $this->_currentStructName[] = trim($this->_currentTagContents); + break; + case 'methodName': + $this->methodName = trim($this->_currentTagContents); + break; + } + + if ($valueFlag) { + if (count($this->_arraystructs) > 0) { + // Add value to struct or array + if ($this->_arraystructstypes[count($this->_arraystructstypes)-1] == 'struct') { + // Add to struct + $this->_arraystructs[count($this->_arraystructs)-1][$this->_currentStructName[count($this->_currentStructName)-1]] = $value; + } else { + // Add to array + $this->_arraystructs[count($this->_arraystructs)-1][] = $value; + } + } else { + // Just add as a paramater + $this->params[] = $value; + } + } + $this->_currentTagContents = ''; + } +} + +/** + * IXR_Server + * + * @package IXR + * @since 1.5 + */ +class IXR_Server +{ + var $data; + var $callbacks = array(); + var $message; + var $capabilities; + + function IXR_Server($callbacks = false, $data = false, $wait = false) + { + $this->setCapabilities(); + if ($callbacks) { + $this->callbacks = $callbacks; + } + $this->setCallbacks(); + if (!$wait) { + $this->serve($data); + } + } + + function serve($data = false) + { + if (!$data) { + if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] !== 'POST') { + header('Content-Type: text/plain'); // merged from WP #9093 + die('XML-RPC server accepts POST requests only.'); + } + + global $HTTP_RAW_POST_DATA; + if (empty($HTTP_RAW_POST_DATA)) { + // workaround for a bug in PHP 5.2.2 - http://bugs.php.net/bug.php?id=41293 + $data = file_get_contents('php://input'); + } else { + $data =& $HTTP_RAW_POST_DATA; + } + } + $this->message = new IXR_Message($data); + if (!$this->message->parse()) { + $this->error(-32700, 'parse error. not well formed'); + } + if ($this->message->messageType != 'methodCall') { + $this->error(-32600, 'server error. invalid xml-rpc. not conforming to spec. Request must be a methodCall'); + } + $result = $this->call($this->message->methodName, $this->message->params); + + // Is the result an error? + if (is_a($result, 'IXR_Error')) { + $this->error($result); + } + + // Encode the result + $r = new IXR_Value($result); + $resultxml = $r->getXml(); + + // Create the XML + $xml = << + + + + $resultxml + + + + + +EOD; + // Send it + $this->output($xml); + } + + function call($methodname, $args) + { + if (!$this->hasMethod($methodname)) { + return new IXR_Error(-32601, 'server error. requested method '.$methodname.' does not exist.'); + } + $method = $this->callbacks[$methodname]; + + // Perform the callback and send the response + if (count($args) == 1) { + // If only one paramater just send that instead of the whole array + $args = $args[0]; + } + + // Are we dealing with a function or a method? + if (is_string($method) && substr($method, 0, 5) == 'this:') { + // It's a class method - check it exists + $method = substr($method, 5); + if (!method_exists($this, $method)) { + return new IXR_Error(-32601, 'server error. requested class method "'.$method.'" does not exist.'); + } + + //Call the method + $result = $this->$method($args); + } else { + // It's a function - does it exist? + if (is_array($method)) { + if (!method_exists($method[0], $method[1])) { + return new IXR_Error(-32601, 'server error. requested object method "'.$method[1].'" does not exist.'); + } + } else if (!function_exists($method)) { + return new IXR_Error(-32601, 'server error. requested function "'.$method.'" does not exist.'); + } + + // Call the function + $result = call_user_func($method, $args); + } + return $result; + } + + function error($error, $message = false) + { + // Accepts either an error object or an error code and message + if ($message && !is_object($error)) { + $error = new IXR_Error($error, $message); + } + $this->output($error->getXml()); + } + + function output($xml) + { + $xml = ''."\n".$xml; + $length = strlen($xml); + header('Connection: close'); + header('Content-Length: '.$length); + header('Content-Type: text/xml'); + header('Date: '.date('r')); + echo $xml; + exit; + } + + function hasMethod($method) + { + return in_array($method, array_keys($this->callbacks)); + } + + function setCapabilities() + { + // Initialises capabilities array + $this->capabilities = array( + 'xmlrpc' => array( + 'specUrl' => 'http://www.xmlrpc.com/spec', + 'specVersion' => 1 + ), + 'faults_interop' => array( + 'specUrl' => 'http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php', + 'specVersion' => 20010516 + ), + 'system.multicall' => array( + 'specUrl' => 'http://www.xmlrpc.com/discuss/msgReader$1208', + 'specVersion' => 1 + ), + ); + } + + function getCapabilities($args) + { + return $this->capabilities; + } + + function setCallbacks() + { + $this->callbacks['system.getCapabilities'] = 'this:getCapabilities'; + $this->callbacks['system.listMethods'] = 'this:listMethods'; + $this->callbacks['system.multicall'] = 'this:multiCall'; + } + + function listMethods($args) + { + // Returns a list of methods - uses array_reverse to ensure user defined + // methods are listed before server defined methods + return array_reverse(array_keys($this->callbacks)); + } + + function multiCall($methodcalls) + { + // See http://www.xmlrpc.com/discuss/msgReader$1208 + $return = array(); + foreach ($methodcalls as $call) { + $method = $call['methodName']; + $params = $call['params']; + if ($method == 'system.multicall') { + $result = new IXR_Error(-32600, 'Recursive calls to system.multicall are forbidden'); + } else { + $result = $this->call($method, $params); + } + if (is_a($result, 'IXR_Error')) { + $return[] = array( + 'faultCode' => $result->code, + 'faultString' => $result->message + ); + } else { + $return[] = array($result); + } + } + return $return; + } +} + +/** + * IXR_Request + * + * @package IXR + * @since 1.5 + */ +class IXR_Request +{ + var $method; + var $args; + var $xml; + + function IXR_Request($method, $args) + { + $this->method = $method; + $this->args = $args; + $this->xml = << + +{$this->method} + + +EOD; + foreach ($this->args as $arg) { + $this->xml .= ''; + $v = new IXR_Value($arg); + $this->xml .= $v->getXml(); + $this->xml .= "\n"; + } + $this->xml .= ''; + } + + function getLength() + { + return strlen($this->xml); + } + + function getXml() + { + return $this->xml; + } +} + +/** + * IXR_Client + * + * @package IXR + * @since 1.5 + * + */ +class IXR_Client +{ + var $server; + var $port; + var $path; + var $useragent; + var $response; + var $message = false; + var $debug = false; + var $timeout; + + // Storage place for an error message + var $error = false; + + function IXR_Client($server, $path = false, $port = 80, $timeout = 15) + { + if (!$path) { + // Assume we have been given a URL instead + $bits = parse_url($server); + $this->server = $bits['host']; + $this->port = isset($bits['port']) ? $bits['port'] : 80; + $this->path = isset($bits['path']) ? $bits['path'] : '/'; + + // Make absolutely sure we have a path + if (!$this->path) { + $this->path = '/'; + } + } else { + $this->server = $server; + $this->path = $path; + $this->port = $port; + } + $this->useragent = 'The Incutio XML-RPC PHP Library'; + $this->timeout = $timeout; + } + + function query() + { + $args = func_get_args(); + $method = array_shift($args); + $request = new IXR_Request($method, $args); + $length = $request->getLength(); + $xml = $request->getXml(); + $r = "\r\n"; + $request = "POST {$this->path} HTTP/1.0$r"; + + // Merged from WP #8145 - allow custom headers + $this->headers['Host'] = $this->server; + $this->headers['Content-Type'] = 'text/xml'; + $this->headers['User-Agent'] = $this->useragent; + $this->headers['Content-Length']= $length; + + foreach( $this->headers as $header => $value ) { + $request .= "{$header}: {$value}{$r}"; + } + $request .= $r; + + $request .= $xml; + + // Now send the request + if ($this->debug) { + echo '
'.htmlspecialchars($request)."\n
\n\n"; + } + + if ($this->timeout) { + $fp = @fsockopen($this->server, $this->port, $errno, $errstr, $this->timeout); + } else { + $fp = @fsockopen($this->server, $this->port, $errno, $errstr); + } + if (!$fp) { + $this->error = new IXR_Error(-32300, 'transport error - could not open socket'); + return false; + } + fputs($fp, $request); + $contents = ''; + $debugContents = ''; + $gotFirstLine = false; + $gettingHeaders = true; + while (!feof($fp)) { + $line = fgets($fp, 4096); + if (!$gotFirstLine) { + // Check line for '200' + if (strstr($line, '200') === false) { + $this->error = new IXR_Error(-32300, 'transport error - HTTP status code was not 200'); + return false; + } + $gotFirstLine = true; + } + if (trim($line) == '') { + $gettingHeaders = false; + } + if (!$gettingHeaders) { + // merged from WP #12559 - remove trim + $contents .= $line; + } + if ($this->debug) { + $debugContents .= $line; + } + } + if ($this->debug) { + echo '
'.htmlspecialchars($debugContents)."\n
\n\n"; + } + + // Now parse what we've got back + $this->message = new IXR_Message($contents); + if (!$this->message->parse()) { + // XML error + $this->error = new IXR_Error(-32700, 'parse error. not well formed'); + return false; + } + + // Is the message a fault? + if ($this->message->messageType == 'fault') { + $this->error = new IXR_Error($this->message->faultCode, $this->message->faultString); + return false; + } + + // Message must be OK + return true; + } + + function getResponse() + { + // methodResponses can only have one param - return that + return $this->message->params[0]; + } + + function isError() + { + return (is_object($this->error)); + } + + function getErrorCode() + { + return $this->error->code; + } + + function getErrorMessage() + { + return $this->error->message; + } +} + + +/** + * IXR_Error + * + * @package IXR + * @since 1.5 + */ +class IXR_Error +{ + var $code; + var $message; + + function IXR_Error($code, $message) + { + $this->code = $code; + $this->message = htmlspecialchars($message); + } + + function getXml() + { + $xml = << + + + + + faultCode + {$this->code} + + + faultString + {$this->message} + + + + + + +EOD; + return $xml; + } +} + +/** + * IXR_Date + * + * @package IXR + * @since 1.5 + */ +class IXR_Date { + var $year; + var $month; + var $day; + var $hour; + var $minute; + var $second; + var $timezone; + + function IXR_Date($time) + { + // $time can be a PHP timestamp or an ISO one + if (is_numeric($time)) { + $this->parseTimestamp($time); + } else { + $this->parseIso($time); + } + } + + function parseTimestamp($timestamp) + { + $this->year = date('Y', $timestamp); + $this->month = date('m', $timestamp); + $this->day = date('d', $timestamp); + $this->hour = date('H', $timestamp); + $this->minute = date('i', $timestamp); + $this->second = date('s', $timestamp); + $this->timezone = ''; + } + + function parseIso($iso) + { + $this->year = substr($iso, 0, 4); + $this->month = substr($iso, 4, 2); + $this->day = substr($iso, 6, 2); + $this->hour = substr($iso, 9, 2); + $this->minute = substr($iso, 12, 2); + $this->second = substr($iso, 15, 2); + $this->timezone = substr($iso, 17); + } + + function getIso() + { + return $this->year.$this->month.$this->day.'T'.$this->hour.':'.$this->minute.':'.$this->second.$this->timezone; + } + + function getXml() + { + return ''.$this->getIso().''; + } + + function getTimestamp() + { + return mktime($this->hour, $this->minute, $this->second, $this->month, $this->day, $this->year); + } +} + +/** + * IXR_Base64 + * + * @package IXR + * @since 1.5 + */ +class IXR_Base64 +{ + var $data; + + function IXR_Base64($data) + { + $this->data = $data; + } + + function getXml() + { + return ''.base64_encode($this->data).''; + } +} + +/** + * IXR_IntrospectionServer + * + * @package IXR + * @since 1.5 + */ +class IXR_IntrospectionServer extends IXR_Server +{ + var $signatures; + var $help; + + function IXR_IntrospectionServer() + { + $this->setCallbacks(); + $this->setCapabilities(); + $this->capabilities['introspection'] = array( + 'specUrl' => 'http://xmlrpc.usefulinc.com/doc/reserved.html', + 'specVersion' => 1 + ); + $this->addCallback( + 'system.methodSignature', + 'this:methodSignature', + array('array', 'string'), + 'Returns an array describing the return type and required parameters of a method' + ); + $this->addCallback( + 'system.getCapabilities', + 'this:getCapabilities', + array('struct'), + 'Returns a struct describing the XML-RPC specifications supported by this server' + ); + $this->addCallback( + 'system.listMethods', + 'this:listMethods', + array('array'), + 'Returns an array of available methods on this server' + ); + $this->addCallback( + 'system.methodHelp', + 'this:methodHelp', + array('string', 'string'), + 'Returns a documentation string for the specified method' + ); + } + + function addCallback($method, $callback, $args, $help) + { + $this->callbacks[$method] = $callback; + $this->signatures[$method] = $args; + $this->help[$method] = $help; + } + + function call($methodname, $args) + { + // Make sure it's in an array + if ($args && !is_array($args)) { + $args = array($args); + } + + // Over-rides default call method, adds signature check + if (!$this->hasMethod($methodname)) { + return new IXR_Error(-32601, 'server error. requested method "'.$this->message->methodName.'" not specified.'); + } + $method = $this->callbacks[$methodname]; + $signature = $this->signatures[$methodname]; + $returnType = array_shift($signature); + + // Check the number of arguments + if (count($args) != count($signature)) { + return new IXR_Error(-32602, 'server error. wrong number of method parameters'); + } + + // Check the argument types + $ok = true; + $argsbackup = $args; + for ($i = 0, $j = count($args); $i < $j; $i++) { + $arg = array_shift($args); + $type = array_shift($signature); + switch ($type) { + case 'int': + case 'i4': + if (is_array($arg) || !is_int($arg)) { + $ok = false; + } + break; + case 'base64': + case 'string': + if (!is_string($arg)) { + $ok = false; + } + break; + case 'boolean': + if ($arg !== false && $arg !== true) { + $ok = false; + } + break; + case 'float': + case 'double': + if (!is_float($arg)) { + $ok = false; + } + break; + case 'date': + case 'dateTime.iso8601': + if (!is_a($arg, 'IXR_Date')) { + $ok = false; + } + break; + } + if (!$ok) { + return new IXR_Error(-32602, 'server error. invalid method parameters'); + } + } + // It passed the test - run the "real" method call + return parent::call($methodname, $argsbackup); + } + + function methodSignature($method) + { + if (!$this->hasMethod($method)) { + return new IXR_Error(-32601, 'server error. requested method "'.$method.'" not specified.'); + } + // We should be returning an array of types + $types = $this->signatures[$method]; + $return = array(); + foreach ($types as $type) { + switch ($type) { + case 'string': + $return[] = 'string'; + break; + case 'int': + case 'i4': + $return[] = 42; + break; + case 'double': + $return[] = 3.1415; + break; + case 'dateTime.iso8601': + $return[] = new IXR_Date(time()); + break; + case 'boolean': + $return[] = true; + break; + case 'base64': + $return[] = new IXR_Base64('base64'); + break; + case 'array': + $return[] = array('array'); + break; + case 'struct': + $return[] = array('struct' => 'struct'); + break; + } + } + return $return; + } + + function methodHelp($method) + { + return $this->help[$method]; + } +} + +/** + * IXR_ClientMulticall + * + * @package IXR + * @since 1.5 + */ +class IXR_ClientMulticall extends IXR_Client +{ + var $calls = array(); + + function IXR_ClientMulticall($server, $path = false, $port = 80) + { + parent::IXR_Client($server, $path, $port); + $this->useragent = 'The Incutio XML-RPC PHP Library (multicall client)'; + } + + function addCall() + { + $args = func_get_args(); + $methodName = array_shift($args); + $struct = array( + 'methodName' => $methodName, + 'params' => $args + ); + $this->calls[] = $struct; + } + + function query() + { + // Prepare multicall, then call the parent::query() method + return parent::query('system.multicall', $this->calls); + } +} + +/** + * Client for communicating with a XML-RPC Server over HTTPS. + * + * @author Jason Stirk (@link http://blog.griffin.homelinux.org/projects/xmlrpc/) + * @version 0.2.0 26May2005 08:34 +0800 + * @copyright (c) 2004-2005 Jason Stirk + * @package IXR + */ +class IXR_ClientSSL extends IXR_Client +{ + /** + * Filename of the SSL Client Certificate + * @access private + * @since 0.1.0 + * @var string + */ + var $_certFile; + + /** + * Filename of the SSL CA Certificate + * @access private + * @since 0.1.0 + * @var string + */ + var $_caFile; + + /** + * Filename of the SSL Client Private Key + * @access private + * @since 0.1.0 + * @var string + */ + var $_keyFile; + + /** + * Passphrase to unlock the private key + * @access private + * @since 0.1.0 + * @var string + */ + var $_passphrase; + + /** + * Constructor + * @param string $server URL of the Server to connect to + * @since 0.1.0 + */ + function IXR_ClientSSL($server, $path = false, $port = 443, $timeout = false) + { + parent::IXR_Client($server, $path, $port, $timeout); + $this->useragent = 'The Incutio XML-RPC PHP Library for SSL'; + + // Set class fields + $this->_certFile=false; + $this->_caFile=false; + $this->_keyFile=false; + $this->_passphrase=''; + } + + /** + * Set the client side certificates to communicate with the server. + * + * @since 0.1.0 + * @param string $certificateFile Filename of the client side certificate to use + * @param string $keyFile Filename of the client side certificate's private key + * @param string $keyPhrase Passphrase to unlock the private key + */ + function setCertificate($certificateFile, $keyFile, $keyPhrase='') + { + // Check the files all exist + if (is_file($certificateFile)) { + $this->_certFile = $certificateFile; + } else { + die('Could not open certificate: ' . $certificateFile); + } + + if (is_file($keyFile)) { + $this->_keyFile = $keyFile; + } else { + die('Could not open private key: ' . $keyFile); + } + + $this->_passphrase=(string)$keyPhrase; + } + + function setCACertificate($caFile) + { + if (is_file($caFile)) { + $this->_caFile = $caFile; + } else { + die('Could not open CA certificate: ' . $caFile); + } + } + + /** + * Sets the connection timeout (in seconds) + * @param int $newTimeOut Timeout in seconds + * @returns void + * @since 0.1.2 + */ + function setTimeOut($newTimeOut) + { + $this->timeout = (int)$newTimeOut; + } + + /** + * Returns the connection timeout (in seconds) + * @returns int + * @since 0.1.2 + */ + function getTimeOut() + { + return $this->timeout; + } + + /** + * Set the query to send to the XML-RPC Server + * @since 0.1.0 + */ + function query() + { + $args = func_get_args(); + $method = array_shift($args); + $request = new IXR_Request($method, $args); + $length = $request->getLength(); + $xml = $request->getXml(); + + if ($this->debug) { + echo '
'.htmlspecialchars($xml)."\n
\n\n"; + } + + //This is where we deviate from the normal query() + //Rather than open a normal sock, we will actually use the cURL + //extensions to make the calls, and handle the SSL stuff. + + //Since 04Aug2004 (0.1.3) - Need to include the port (duh...) + //Since 06Oct2004 (0.1.4) - Need to include the colon!!! + // (I swear I've fixed this before... ESP in live... But anyhu...) + $curl=curl_init('https://' . $this->server . ':' . $this->port . $this->path); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1); + + //Since 23Jun2004 (0.1.2) - Made timeout a class field + curl_setopt($curl, CURLOPT_TIMEOUT, $this->timeout); + + if ($this->debug) { + curl_setopt($curl, CURLOPT_VERBOSE, 1); + } + + curl_setopt($curl, CURLOPT_HEADER, 1); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_POSTFIELDS, $xml); + curl_setopt($curl, CURLOPT_PORT, $this->port); + curl_setopt($curl, CURLOPT_HTTPHEADER, array( + "Content-Type: text/xml", + "Content-length: {$length}")); + + // Process the SSL certificates, etc. to use + if (!($this->_certFile === false)) { + // We have a certificate file set, so add these to the cURL handler + curl_setopt($curl, CURLOPT_SSLCERT, $this->_certFile); + curl_setopt($curl, CURLOPT_SSLKEY, $this->_keyFile); + + if ($this->debug) { + echo "SSL Cert at : " . $this->_certFile . "\n"; + echo "SSL Key at : " . $this->_keyFile . "\n"; + } + + // See if we need to give a passphrase + if (!($this->_passphrase === '')) { + curl_setopt($curl, CURLOPT_SSLCERTPASSWD, $this->_passphrase); + } + + if ($this->_caFile === false) { + // Don't verify their certificate, as we don't have a CA to verify against + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 0); + } else { + // Verify against a CA + curl_setopt($curl, CURLOPT_CAINFO, $this->_caFile); + } + } + + // Call cURL to do it's stuff and return us the content + $contents = curl_exec($curl); + curl_close($curl); + + // Check for 200 Code in $contents + if (!strstr($contents, '200 OK')) { + //There was no "200 OK" returned - we failed + $this->error = new IXR_Error(-32300, 'transport error - HTTP status code was not 200'); + return false; + } + + if ($this->debug) { + echo '
'.htmlspecialchars($contents)."\n
\n\n"; + } + // Now parse what we've got back + // Since 20Jun2004 (0.1.1) - We need to remove the headers first + // Why I have only just found this, I will never know... + // So, remove everything before the first < + $contents = substr($contents,strpos($contents, '<')); + + $this->message = new IXR_Message($contents); + if (!$this->message->parse()) { + // XML error + $this->error = new IXR_Error(-32700, 'parse error. not well formed'); + return false; + } + // Is the message a fault? + if ($this->message->messageType == 'fault') { + $this->error = new IXR_Error($this->message->faultCode, $this->message->faultString); + return false; + } + + // Message must be OK + return true; + } +} + +/** + * Extension of the {@link IXR_Server} class to easily wrap objects. + * + * Class is designed to extend the existing XML-RPC server to allow the + * presentation of methods from a variety of different objects via an + * XML-RPC server. + * It is intended to assist in organization of your XML-RPC methods by allowing + * you to "write once" in your existing model classes and present them. + * + * @author Jason Stirk + * @version 1.0.1 19Apr2005 17:40 +0800 + * @copyright Copyright (c) 2005 Jason Stirk + * @package IXR + */ +class IXR_ClassServer extends IXR_Server +{ + var $_objects; + var $_delim; + + function IXR_ClassServer($delim = '.', $wait = false) + { + $this->IXR_Server(array(), false, $wait); + $this->_delimiter = $delim; + $this->_objects = array(); + } + + function addMethod($rpcName, $functionName) + { + $this->callbacks[$rpcName] = $functionName; + } + + function registerObject($object, $methods, $prefix=null) + { + if (is_null($prefix)) + { + $prefix = get_class($object); + } + $this->_objects[$prefix] = $object; + + // Add to our callbacks array + foreach($methods as $method) + { + if (is_array($method)) + { + $targetMethod = $method[0]; + $method = $method[1]; + } + else + { + $targetMethod = $method; + } + $this->callbacks[$prefix . $this->_delimiter . $method]=array($prefix, $targetMethod); + } + } + + function call($methodname, $args) + { + if (!$this->hasMethod($methodname)) { + return new IXR_Error(-32601, 'server error. requested method '.$methodname.' does not exist.'); + } + $method = $this->callbacks[$methodname]; + + // Perform the callback and send the response + if (count($args) == 1) { + // If only one paramater just send that instead of the whole array + $args = $args[0]; + } + + // See if this method comes from one of our objects or maybe self + if (is_array($method) || (substr($method, 0, 5) == 'this:')) { + if (is_array($method)) { + $object=$this->_objects[$method[0]]; + $method=$method[1]; + } else { + $object=$this; + $method = substr($method, 5); + } + + // It's a class method - check it exists + if (!method_exists($object, $method)) { + return new IXR_Error(-32601, 'server error. requested class method "'.$method.'" does not exist.'); + } + + // Call the method + $result = $object->$method($args); + } else { + // It's a function - does it exist? + if (!function_exists($method)) { + return new IXR_Error(-32601, 'server error. requested function "'.$method.'" does not exist.'); + } + + // Call the function + $result = $method($args); + } + return $result; + } +} diff --git a/News/includes/lib/jquery.js b/News/includes/lib/jquery.js new file mode 100644 index 0000000..8f3ca2e --- /dev/null +++ b/News/includes/lib/jquery.js @@ -0,0 +1,167 @@ +/*! + * jQuery JavaScript Library v1.4.4 + * http://jquery.com/ + * + * Copyright 2010, John Resig + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * Includes Sizzle.js + * http://sizzlejs.com/ + * Copyright 2010, The Dojo Foundation + * Released under the MIT, BSD, and GPL Licenses. + * + * Date: Thu Nov 11 19:04:53 2010 -0500 + */ +(function(E,B){function ka(a,b,d){if(d===B&&a.nodeType===1){d=a.getAttribute("data-"+b);if(typeof d==="string"){try{d=d==="true"?true:d==="false"?false:d==="null"?null:!c.isNaN(d)?parseFloat(d):Ja.test(d)?c.parseJSON(d):d}catch(e){}c.data(a,b,d)}else d=B}return d}function U(){return false}function ca(){return true}function la(a,b,d){d[0].type=a;return c.event.handle.apply(b,d)}function Ka(a){var b,d,e,f,h,l,k,o,x,r,A,C=[];f=[];h=c.data(this,this.nodeType?"events":"__events__");if(typeof h==="function")h= +h.events;if(!(a.liveFired===this||!h||!h.live||a.button&&a.type==="click")){if(a.namespace)A=RegExp("(^|\\.)"+a.namespace.split(".").join("\\.(?:.*\\.)?")+"(\\.|$)");a.liveFired=this;var J=h.live.slice(0);for(k=0;kd)break;a.currentTarget=f.elem;a.data=f.handleObj.data;a.handleObj=f.handleObj;A=f.handleObj.origHandler.apply(f.elem,arguments);if(A===false||a.isPropagationStopped()){d=f.level;if(A===false)b=false;if(a.isImmediatePropagationStopped())break}}return b}}function Y(a,b){return(a&&a!=="*"?a+".":"")+b.replace(La, +"`").replace(Ma,"&")}function ma(a,b,d){if(c.isFunction(b))return c.grep(a,function(f,h){return!!b.call(f,h,f)===d});else if(b.nodeType)return c.grep(a,function(f){return f===b===d});else if(typeof b==="string"){var e=c.grep(a,function(f){return f.nodeType===1});if(Na.test(b))return c.filter(b,e,!d);else b=c.filter(b,e)}return c.grep(a,function(f){return c.inArray(f,b)>=0===d})}function na(a,b){var d=0;b.each(function(){if(this.nodeName===(a[d]&&a[d].nodeName)){var e=c.data(a[d++]),f=c.data(this, +e);if(e=e&&e.events){delete f.handle;f.events={};for(var h in e)for(var l in e[h])c.event.add(this,h,e[h][l],e[h][l].data)}}})}function Oa(a,b){b.src?c.ajax({url:b.src,async:false,dataType:"script"}):c.globalEval(b.text||b.textContent||b.innerHTML||"");b.parentNode&&b.parentNode.removeChild(b)}function oa(a,b,d){var e=b==="width"?a.offsetWidth:a.offsetHeight;if(d==="border")return e;c.each(b==="width"?Pa:Qa,function(){d||(e-=parseFloat(c.css(a,"padding"+this))||0);if(d==="margin")e+=parseFloat(c.css(a, +"margin"+this))||0;else e-=parseFloat(c.css(a,"border"+this+"Width"))||0});return e}function da(a,b,d,e){if(c.isArray(b)&&b.length)c.each(b,function(f,h){d||Ra.test(a)?e(a,h):da(a+"["+(typeof h==="object"||c.isArray(h)?f:"")+"]",h,d,e)});else if(!d&&b!=null&&typeof b==="object")c.isEmptyObject(b)?e(a,""):c.each(b,function(f,h){da(a+"["+f+"]",h,d,e)});else e(a,b)}function S(a,b){var d={};c.each(pa.concat.apply([],pa.slice(0,b)),function(){d[this]=a});return d}function qa(a){if(!ea[a]){var b=c("<"+ +a+">").appendTo("body"),d=b.css("display");b.remove();if(d==="none"||d==="")d="block";ea[a]=d}return ea[a]}function fa(a){return c.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:false}var t=E.document,c=function(){function a(){if(!b.isReady){try{t.documentElement.doScroll("left")}catch(j){setTimeout(a,1);return}b.ready()}}var b=function(j,s){return new b.fn.init(j,s)},d=E.jQuery,e=E.$,f,h=/^(?:[^<]*(<[\w\W]+>)[^>]*$|#([\w\-]+)$)/,l=/\S/,k=/^\s+/,o=/\s+$/,x=/\W/,r=/\d/,A=/^<(\w+)\s*\/?>(?:<\/\1>)?$/, +C=/^[\],:{}\s]*$/,J=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,w=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,I=/(?:^|:|,)(?:\s*\[)+/g,L=/(webkit)[ \/]([\w.]+)/,g=/(opera)(?:.*version)?[ \/]([\w.]+)/,i=/(msie) ([\w.]+)/,n=/(mozilla)(?:.*? rv:([\w.]+))?/,m=navigator.userAgent,p=false,q=[],u,y=Object.prototype.toString,F=Object.prototype.hasOwnProperty,M=Array.prototype.push,N=Array.prototype.slice,O=String.prototype.trim,D=Array.prototype.indexOf,R={};b.fn=b.prototype={init:function(j, +s){var v,z,H;if(!j)return this;if(j.nodeType){this.context=this[0]=j;this.length=1;return this}if(j==="body"&&!s&&t.body){this.context=t;this[0]=t.body;this.selector="body";this.length=1;return this}if(typeof j==="string")if((v=h.exec(j))&&(v[1]||!s))if(v[1]){H=s?s.ownerDocument||s:t;if(z=A.exec(j))if(b.isPlainObject(s)){j=[t.createElement(z[1])];b.fn.attr.call(j,s,true)}else j=[H.createElement(z[1])];else{z=b.buildFragment([v[1]],[H]);j=(z.cacheable?z.fragment.cloneNode(true):z.fragment).childNodes}return b.merge(this, +j)}else{if((z=t.getElementById(v[2]))&&z.parentNode){if(z.id!==v[2])return f.find(j);this.length=1;this[0]=z}this.context=t;this.selector=j;return this}else if(!s&&!x.test(j)){this.selector=j;this.context=t;j=t.getElementsByTagName(j);return b.merge(this,j)}else return!s||s.jquery?(s||f).find(j):b(s).find(j);else if(b.isFunction(j))return f.ready(j);if(j.selector!==B){this.selector=j.selector;this.context=j.context}return b.makeArray(j,this)},selector:"",jquery:"1.4.4",length:0,size:function(){return this.length}, +toArray:function(){return N.call(this,0)},get:function(j){return j==null?this.toArray():j<0?this.slice(j)[0]:this[j]},pushStack:function(j,s,v){var z=b();b.isArray(j)?M.apply(z,j):b.merge(z,j);z.prevObject=this;z.context=this.context;if(s==="find")z.selector=this.selector+(this.selector?" ":"")+v;else if(s)z.selector=this.selector+"."+s+"("+v+")";return z},each:function(j,s){return b.each(this,j,s)},ready:function(j){b.bindReady();if(b.isReady)j.call(t,b);else q&&q.push(j);return this},eq:function(j){return j=== +-1?this.slice(j):this.slice(j,+j+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(N.apply(this,arguments),"slice",N.call(arguments).join(","))},map:function(j){return this.pushStack(b.map(this,function(s,v){return j.call(s,v,s)}))},end:function(){return this.prevObject||b(null)},push:M,sort:[].sort,splice:[].splice};b.fn.init.prototype=b.fn;b.extend=b.fn.extend=function(){var j,s,v,z,H,G=arguments[0]||{},K=1,Q=arguments.length,ga=false; +if(typeof G==="boolean"){ga=G;G=arguments[1]||{};K=2}if(typeof G!=="object"&&!b.isFunction(G))G={};if(Q===K){G=this;--K}for(;K0))if(q){var s=0,v=q;for(q=null;j=v[s++];)j.call(t,b);b.fn.trigger&&b(t).trigger("ready").unbind("ready")}}},bindReady:function(){if(!p){p=true;if(t.readyState==="complete")return setTimeout(b.ready,1);if(t.addEventListener){t.addEventListener("DOMContentLoaded",u,false);E.addEventListener("load",b.ready,false)}else if(t.attachEvent){t.attachEvent("onreadystatechange",u);E.attachEvent("onload", +b.ready);var j=false;try{j=E.frameElement==null}catch(s){}t.documentElement.doScroll&&j&&a()}}},isFunction:function(j){return b.type(j)==="function"},isArray:Array.isArray||function(j){return b.type(j)==="array"},isWindow:function(j){return j&&typeof j==="object"&&"setInterval"in j},isNaN:function(j){return j==null||!r.test(j)||isNaN(j)},type:function(j){return j==null?String(j):R[y.call(j)]||"object"},isPlainObject:function(j){if(!j||b.type(j)!=="object"||j.nodeType||b.isWindow(j))return false;if(j.constructor&& +!F.call(j,"constructor")&&!F.call(j.constructor.prototype,"isPrototypeOf"))return false;for(var s in j);return s===B||F.call(j,s)},isEmptyObject:function(j){for(var s in j)return false;return true},error:function(j){throw j;},parseJSON:function(j){if(typeof j!=="string"||!j)return null;j=b.trim(j);if(C.test(j.replace(J,"@").replace(w,"]").replace(I,"")))return E.JSON&&E.JSON.parse?E.JSON.parse(j):(new Function("return "+j))();else b.error("Invalid JSON: "+j)},noop:function(){},globalEval:function(j){if(j&& +l.test(j)){var s=t.getElementsByTagName("head")[0]||t.documentElement,v=t.createElement("script");v.type="text/javascript";if(b.support.scriptEval)v.appendChild(t.createTextNode(j));else v.text=j;s.insertBefore(v,s.firstChild);s.removeChild(v)}},nodeName:function(j,s){return j.nodeName&&j.nodeName.toUpperCase()===s.toUpperCase()},each:function(j,s,v){var z,H=0,G=j.length,K=G===B||b.isFunction(j);if(v)if(K)for(z in j){if(s.apply(j[z],v)===false)break}else for(;H
a";var f=d.getElementsByTagName("*"),h=d.getElementsByTagName("a")[0],l=t.createElement("select"), +k=l.appendChild(t.createElement("option"));if(!(!f||!f.length||!h)){c.support={leadingWhitespace:d.firstChild.nodeType===3,tbody:!d.getElementsByTagName("tbody").length,htmlSerialize:!!d.getElementsByTagName("link").length,style:/red/.test(h.getAttribute("style")),hrefNormalized:h.getAttribute("href")==="/a",opacity:/^0.55$/.test(h.style.opacity),cssFloat:!!h.style.cssFloat,checkOn:d.getElementsByTagName("input")[0].value==="on",optSelected:k.selected,deleteExpando:true,optDisabled:false,checkClone:false, +scriptEval:false,noCloneEvent:true,boxModel:null,inlineBlockNeedsLayout:false,shrinkWrapBlocks:false,reliableHiddenOffsets:true};l.disabled=true;c.support.optDisabled=!k.disabled;b.type="text/javascript";try{b.appendChild(t.createTextNode("window."+e+"=1;"))}catch(o){}a.insertBefore(b,a.firstChild);if(E[e]){c.support.scriptEval=true;delete E[e]}try{delete b.test}catch(x){c.support.deleteExpando=false}a.removeChild(b);if(d.attachEvent&&d.fireEvent){d.attachEvent("onclick",function r(){c.support.noCloneEvent= +false;d.detachEvent("onclick",r)});d.cloneNode(true).fireEvent("onclick")}d=t.createElement("div");d.innerHTML="";a=t.createDocumentFragment();a.appendChild(d.firstChild);c.support.checkClone=a.cloneNode(true).cloneNode(true).lastChild.checked;c(function(){var r=t.createElement("div");r.style.width=r.style.paddingLeft="1px";t.body.appendChild(r);c.boxModel=c.support.boxModel=r.offsetWidth===2;if("zoom"in r.style){r.style.display="inline";r.style.zoom= +1;c.support.inlineBlockNeedsLayout=r.offsetWidth===2;r.style.display="";r.innerHTML="
";c.support.shrinkWrapBlocks=r.offsetWidth!==2}r.innerHTML="
t
";var A=r.getElementsByTagName("td");c.support.reliableHiddenOffsets=A[0].offsetHeight===0;A[0].style.display="";A[1].style.display="none";c.support.reliableHiddenOffsets=c.support.reliableHiddenOffsets&&A[0].offsetHeight===0;r.innerHTML="";t.body.removeChild(r).style.display= +"none"});a=function(r){var A=t.createElement("div");r="on"+r;var C=r in A;if(!C){A.setAttribute(r,"return;");C=typeof A[r]==="function"}return C};c.support.submitBubbles=a("submit");c.support.changeBubbles=a("change");a=b=d=f=h=null}})();var ra={},Ja=/^(?:\{.*\}|\[.*\])$/;c.extend({cache:{},uuid:0,expando:"jQuery"+c.now(),noData:{embed:true,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:true},data:function(a,b,d){if(c.acceptData(a)){a=a==E?ra:a;var e=a.nodeType,f=e?a[c.expando]:null,h= +c.cache;if(!(e&&!f&&typeof b==="string"&&d===B)){if(e)f||(a[c.expando]=f=++c.uuid);else h=a;if(typeof b==="object")if(e)h[f]=c.extend(h[f],b);else c.extend(h,b);else if(e&&!h[f])h[f]={};a=e?h[f]:h;if(d!==B)a[b]=d;return typeof b==="string"?a[b]:a}}},removeData:function(a,b){if(c.acceptData(a)){a=a==E?ra:a;var d=a.nodeType,e=d?a[c.expando]:a,f=c.cache,h=d?f[e]:e;if(b){if(h){delete h[b];d&&c.isEmptyObject(h)&&c.removeData(a)}}else if(d&&c.support.deleteExpando)delete a[c.expando];else if(a.removeAttribute)a.removeAttribute(c.expando); +else if(d)delete f[e];else for(var l in a)delete a[l]}},acceptData:function(a){if(a.nodeName){var b=c.noData[a.nodeName.toLowerCase()];if(b)return!(b===true||a.getAttribute("classid")!==b)}return true}});c.fn.extend({data:function(a,b){var d=null;if(typeof a==="undefined"){if(this.length){var e=this[0].attributes,f;d=c.data(this[0]);for(var h=0,l=e.length;h-1)return true;return false},val:function(a){if(!arguments.length){var b=this[0];if(b){if(c.nodeName(b,"option")){var d=b.attributes.value;return!d||d.specified?b.value:b.text}if(c.nodeName(b,"select")){var e=b.selectedIndex;d=[];var f=b.options;b=b.type==="select-one"; +if(e<0)return null;var h=b?e:0;for(e=b?e+1:f.length;h=0;else if(c.nodeName(this,"select")){var A=c.makeArray(r);c("option",this).each(function(){this.selected=c.inArray(c(this).val(),A)>=0});if(!A.length)this.selectedIndex=-1}else this.value=r}})}});c.extend({attrFn:{val:true,css:true,html:true,text:true,data:true,width:true,height:true,offset:true}, +attr:function(a,b,d,e){if(!a||a.nodeType===3||a.nodeType===8)return B;if(e&&b in c.attrFn)return c(a)[b](d);e=a.nodeType!==1||!c.isXMLDoc(a);var f=d!==B;b=e&&c.props[b]||b;var h=Ta.test(b);if((b in a||a[b]!==B)&&e&&!h){if(f){b==="type"&&Ua.test(a.nodeName)&&a.parentNode&&c.error("type property can't be changed");if(d===null)a.nodeType===1&&a.removeAttribute(b);else a[b]=d}if(c.nodeName(a,"form")&&a.getAttributeNode(b))return a.getAttributeNode(b).nodeValue;if(b==="tabIndex")return(b=a.getAttributeNode("tabIndex"))&& +b.specified?b.value:Va.test(a.nodeName)||Wa.test(a.nodeName)&&a.href?0:B;return a[b]}if(!c.support.style&&e&&b==="style"){if(f)a.style.cssText=""+d;return a.style.cssText}f&&a.setAttribute(b,""+d);if(!a.attributes[b]&&a.hasAttribute&&!a.hasAttribute(b))return B;a=!c.support.hrefNormalized&&e&&h?a.getAttribute(b,2):a.getAttribute(b);return a===null?B:a}});var X=/\.(.*)$/,ia=/^(?:textarea|input|select)$/i,La=/\./g,Ma=/ /g,Xa=/[^\w\s.|`]/g,Ya=function(a){return a.replace(Xa,"\\$&")},ua={focusin:0,focusout:0}; +c.event={add:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(c.isWindow(a)&&a!==E&&!a.frameElement)a=E;if(d===false)d=U;else if(!d)return;var f,h;if(d.handler){f=d;d=f.handler}if(!d.guid)d.guid=c.guid++;if(h=c.data(a)){var l=a.nodeType?"events":"__events__",k=h[l],o=h.handle;if(typeof k==="function"){o=k.handle;k=k.events}else if(!k){a.nodeType||(h[l]=h=function(){});h.events=k={}}if(!o)h.handle=o=function(){return typeof c!=="undefined"&&!c.event.triggered?c.event.handle.apply(o.elem, +arguments):B};o.elem=a;b=b.split(" ");for(var x=0,r;l=b[x++];){h=f?c.extend({},f):{handler:d,data:e};if(l.indexOf(".")>-1){r=l.split(".");l=r.shift();h.namespace=r.slice(0).sort().join(".")}else{r=[];h.namespace=""}h.type=l;if(!h.guid)h.guid=d.guid;var A=k[l],C=c.event.special[l]||{};if(!A){A=k[l]=[];if(!C.setup||C.setup.call(a,e,r,o)===false)if(a.addEventListener)a.addEventListener(l,o,false);else a.attachEvent&&a.attachEvent("on"+l,o)}if(C.add){C.add.call(a,h);if(!h.handler.guid)h.handler.guid= +d.guid}A.push(h);c.event.global[l]=true}a=null}}},global:{},remove:function(a,b,d,e){if(!(a.nodeType===3||a.nodeType===8)){if(d===false)d=U;var f,h,l=0,k,o,x,r,A,C,J=a.nodeType?"events":"__events__",w=c.data(a),I=w&&w[J];if(w&&I){if(typeof I==="function"){w=I;I=I.events}if(b&&b.type){d=b.handler;b=b.type}if(!b||typeof b==="string"&&b.charAt(0)==="."){b=b||"";for(f in I)c.event.remove(a,f+b)}else{for(b=b.split(" ");f=b[l++];){r=f;k=f.indexOf(".")<0;o=[];if(!k){o=f.split(".");f=o.shift();x=RegExp("(^|\\.)"+ +c.map(o.slice(0).sort(),Ya).join("\\.(?:.*\\.)?")+"(\\.|$)")}if(A=I[f])if(d){r=c.event.special[f]||{};for(h=e||0;h=0){a.type=f=f.slice(0,-1);a.exclusive=true}if(!d){a.stopPropagation();c.event.global[f]&&c.each(c.cache,function(){this.events&&this.events[f]&&c.event.trigger(a,b,this.handle.elem)})}if(!d||d.nodeType===3||d.nodeType=== +8)return B;a.result=B;a.target=d;b=c.makeArray(b);b.unshift(a)}a.currentTarget=d;(e=d.nodeType?c.data(d,"handle"):(c.data(d,"__events__")||{}).handle)&&e.apply(d,b);e=d.parentNode||d.ownerDocument;try{if(!(d&&d.nodeName&&c.noData[d.nodeName.toLowerCase()]))if(d["on"+f]&&d["on"+f].apply(d,b)===false){a.result=false;a.preventDefault()}}catch(h){}if(!a.isPropagationStopped()&&e)c.event.trigger(a,b,e,true);else if(!a.isDefaultPrevented()){var l;e=a.target;var k=f.replace(X,""),o=c.nodeName(e,"a")&&k=== +"click",x=c.event.special[k]||{};if((!x._default||x._default.call(d,a)===false)&&!o&&!(e&&e.nodeName&&c.noData[e.nodeName.toLowerCase()])){try{if(e[k]){if(l=e["on"+k])e["on"+k]=null;c.event.triggered=true;e[k]()}}catch(r){}if(l)e["on"+k]=l;c.event.triggered=false}}},handle:function(a){var b,d,e,f;d=[];var h=c.makeArray(arguments);a=h[0]=c.event.fix(a||E.event);a.currentTarget=this;b=a.type.indexOf(".")<0&&!a.exclusive;if(!b){e=a.type.split(".");a.type=e.shift();d=e.slice(0).sort();e=RegExp("(^|\\.)"+ +d.join("\\.(?:.*\\.)?")+"(\\.|$)")}a.namespace=a.namespace||d.join(".");f=c.data(this,this.nodeType?"events":"__events__");if(typeof f==="function")f=f.events;d=(f||{})[a.type];if(f&&d){d=d.slice(0);f=0;for(var l=d.length;f-1?c.map(a.options,function(e){return e.selected}).join("-"):"";else if(a.nodeName.toLowerCase()==="select")d=a.selectedIndex;return d},Z=function(a,b){var d=a.target,e,f;if(!(!ia.test(d.nodeName)||d.readOnly)){e=c.data(d,"_change_data");f=xa(d);if(a.type!=="focusout"||d.type!=="radio")c.data(d,"_change_data",f);if(!(e===B||f===e))if(e!=null||f){a.type="change";a.liveFired= +B;return c.event.trigger(a,b,d)}}};c.event.special.change={filters:{focusout:Z,beforedeactivate:Z,click:function(a){var b=a.target,d=b.type;if(d==="radio"||d==="checkbox"||b.nodeName.toLowerCase()==="select")return Z.call(this,a)},keydown:function(a){var b=a.target,d=b.type;if(a.keyCode===13&&b.nodeName.toLowerCase()!=="textarea"||a.keyCode===32&&(d==="checkbox"||d==="radio")||d==="select-multiple")return Z.call(this,a)},beforeactivate:function(a){a=a.target;c.data(a,"_change_data",xa(a))}},setup:function(){if(this.type=== +"file")return false;for(var a in V)c.event.add(this,a+".specialChange",V[a]);return ia.test(this.nodeName)},teardown:function(){c.event.remove(this,".specialChange");return ia.test(this.nodeName)}};V=c.event.special.change.filters;V.focus=V.beforeactivate}t.addEventListener&&c.each({focus:"focusin",blur:"focusout"},function(a,b){function d(e){e=c.event.fix(e);e.type=b;return c.event.trigger(e,null,e.target)}c.event.special[b]={setup:function(){ua[b]++===0&&t.addEventListener(a,d,true)},teardown:function(){--ua[b]=== +0&&t.removeEventListener(a,d,true)}}});c.each(["bind","one"],function(a,b){c.fn[b]=function(d,e,f){if(typeof d==="object"){for(var h in d)this[b](h,e,d[h],f);return this}if(c.isFunction(e)||e===false){f=e;e=B}var l=b==="one"?c.proxy(f,function(o){c(this).unbind(o,l);return f.apply(this,arguments)}):f;if(d==="unload"&&b!=="one")this.one(d,e,f);else{h=0;for(var k=this.length;h0?this.bind(b,d,e):this.trigger(b)};if(c.attrFn)c.attrFn[b]=true});E.attachEvent&&!E.addEventListener&&c(E).bind("unload",function(){for(var a in c.cache)if(c.cache[a].handle)try{c.event.remove(c.cache[a].handle.elem)}catch(b){}}); +(function(){function a(g,i,n,m,p,q){p=0;for(var u=m.length;p0){F=y;break}}y=y[g]}m[p]=F}}}var d=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,e=0,f=Object.prototype.toString,h=false,l=true;[0,0].sort(function(){l=false;return 0});var k=function(g,i,n,m){n=n||[];var p=i=i||t;if(i.nodeType!==1&&i.nodeType!==9)return[];if(!g||typeof g!=="string")return n;var q,u,y,F,M,N=true,O=k.isXML(i),D=[],R=g;do{d.exec("");if(q=d.exec(R)){R=q[3];D.push(q[1]);if(q[2]){F=q[3]; +break}}}while(q);if(D.length>1&&x.exec(g))if(D.length===2&&o.relative[D[0]])u=L(D[0]+D[1],i);else for(u=o.relative[D[0]]?[i]:k(D.shift(),i);D.length;){g=D.shift();if(o.relative[g])g+=D.shift();u=L(g,u)}else{if(!m&&D.length>1&&i.nodeType===9&&!O&&o.match.ID.test(D[0])&&!o.match.ID.test(D[D.length-1])){q=k.find(D.shift(),i,O);i=q.expr?k.filter(q.expr,q.set)[0]:q.set[0]}if(i){q=m?{expr:D.pop(),set:C(m)}:k.find(D.pop(),D.length===1&&(D[0]==="~"||D[0]==="+")&&i.parentNode?i.parentNode:i,O);u=q.expr?k.filter(q.expr, +q.set):q.set;if(D.length>0)y=C(u);else N=false;for(;D.length;){q=M=D.pop();if(o.relative[M])q=D.pop();else M="";if(q==null)q=i;o.relative[M](y,q,O)}}else y=[]}y||(y=u);y||k.error(M||g);if(f.call(y)==="[object Array]")if(N)if(i&&i.nodeType===1)for(g=0;y[g]!=null;g++){if(y[g]&&(y[g]===true||y[g].nodeType===1&&k.contains(i,y[g])))n.push(u[g])}else for(g=0;y[g]!=null;g++)y[g]&&y[g].nodeType===1&&n.push(u[g]);else n.push.apply(n,y);else C(y,n);if(F){k(F,p,n,m);k.uniqueSort(n)}return n};k.uniqueSort=function(g){if(w){h= +l;g.sort(w);if(h)for(var i=1;i0};k.find=function(g,i,n){var m;if(!g)return[];for(var p=0,q=o.order.length;p":function(g,i){var n,m=typeof i==="string",p=0,q=g.length;if(m&&!/\W/.test(i))for(i=i.toLowerCase();p=0))n||m.push(u);else if(n)i[q]=false;return false},ID:function(g){return g[1].replace(/\\/g,"")},TAG:function(g){return g[1].toLowerCase()},CHILD:function(g){if(g[1]==="nth"){var i=/(-?)(\d*)n((?:\+|-)?\d*)/.exec(g[2]==="even"&&"2n"||g[2]==="odd"&&"2n+1"||!/\D/.test(g[2])&&"0n+"+g[2]||g[2]);g[2]=i[1]+(i[2]||1)-0;g[3]=i[3]-0}g[0]=e++;return g},ATTR:function(g,i,n, +m,p,q){i=g[1].replace(/\\/g,"");if(!q&&o.attrMap[i])g[1]=o.attrMap[i];if(g[2]==="~=")g[4]=" "+g[4]+" ";return g},PSEUDO:function(g,i,n,m,p){if(g[1]==="not")if((d.exec(g[3])||"").length>1||/^\w/.test(g[3]))g[3]=k(g[3],null,null,i);else{g=k.filter(g[3],i,n,true^p);n||m.push.apply(m,g);return false}else if(o.match.POS.test(g[0])||o.match.CHILD.test(g[0]))return true;return g},POS:function(g){g.unshift(true);return g}},filters:{enabled:function(g){return g.disabled===false&&g.type!=="hidden"},disabled:function(g){return g.disabled=== +true},checked:function(g){return g.checked===true},selected:function(g){return g.selected===true},parent:function(g){return!!g.firstChild},empty:function(g){return!g.firstChild},has:function(g,i,n){return!!k(n[3],g).length},header:function(g){return/h\d/i.test(g.nodeName)},text:function(g){return"text"===g.type},radio:function(g){return"radio"===g.type},checkbox:function(g){return"checkbox"===g.type},file:function(g){return"file"===g.type},password:function(g){return"password"===g.type},submit:function(g){return"submit"=== +g.type},image:function(g){return"image"===g.type},reset:function(g){return"reset"===g.type},button:function(g){return"button"===g.type||g.nodeName.toLowerCase()==="button"},input:function(g){return/input|select|textarea|button/i.test(g.nodeName)}},setFilters:{first:function(g,i){return i===0},last:function(g,i,n,m){return i===m.length-1},even:function(g,i){return i%2===0},odd:function(g,i){return i%2===1},lt:function(g,i,n){return in[3]-0},nth:function(g,i,n){return n[3]- +0===i},eq:function(g,i,n){return n[3]-0===i}},filter:{PSEUDO:function(g,i,n,m){var p=i[1],q=o.filters[p];if(q)return q(g,n,i,m);else if(p==="contains")return(g.textContent||g.innerText||k.getText([g])||"").indexOf(i[3])>=0;else if(p==="not"){i=i[3];n=0;for(m=i.length;n=0}},ID:function(g,i){return g.nodeType===1&&g.getAttribute("id")===i},TAG:function(g,i){return i==="*"&&g.nodeType===1||g.nodeName.toLowerCase()=== +i},CLASS:function(g,i){return(" "+(g.className||g.getAttribute("class"))+" ").indexOf(i)>-1},ATTR:function(g,i){var n=i[1];n=o.attrHandle[n]?o.attrHandle[n](g):g[n]!=null?g[n]:g.getAttribute(n);var m=n+"",p=i[2],q=i[4];return n==null?p==="!=":p==="="?m===q:p==="*="?m.indexOf(q)>=0:p==="~="?(" "+m+" ").indexOf(q)>=0:!q?m&&n!==false:p==="!="?m!==q:p==="^="?m.indexOf(q)===0:p==="$="?m.substr(m.length-q.length)===q:p==="|="?m===q||m.substr(0,q.length+1)===q+"-":false},POS:function(g,i,n,m){var p=o.setFilters[i[2]]; +if(p)return p(g,n,i,m)}}},x=o.match.POS,r=function(g,i){return"\\"+(i-0+1)},A;for(A in o.match){o.match[A]=RegExp(o.match[A].source+/(?![^\[]*\])(?![^\(]*\))/.source);o.leftMatch[A]=RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[A].source.replace(/\\(\d+)/g,r))}var C=function(g,i){g=Array.prototype.slice.call(g,0);if(i){i.push.apply(i,g);return i}return g};try{Array.prototype.slice.call(t.documentElement.childNodes,0)}catch(J){C=function(g,i){var n=0,m=i||[];if(f.call(g)==="[object Array]")Array.prototype.push.apply(m, +g);else if(typeof g.length==="number")for(var p=g.length;n";n.insertBefore(g,n.firstChild);if(t.getElementById(i)){o.find.ID=function(m,p,q){if(typeof p.getElementById!=="undefined"&&!q)return(p=p.getElementById(m[1]))?p.id===m[1]||typeof p.getAttributeNode!=="undefined"&&p.getAttributeNode("id").nodeValue===m[1]?[p]:B:[]};o.filter.ID=function(m,p){var q=typeof m.getAttributeNode!=="undefined"&&m.getAttributeNode("id");return m.nodeType===1&&q&&q.nodeValue===p}}n.removeChild(g); +n=g=null})();(function(){var g=t.createElement("div");g.appendChild(t.createComment(""));if(g.getElementsByTagName("*").length>0)o.find.TAG=function(i,n){var m=n.getElementsByTagName(i[1]);if(i[1]==="*"){for(var p=[],q=0;m[q];q++)m[q].nodeType===1&&p.push(m[q]);m=p}return m};g.innerHTML="";if(g.firstChild&&typeof g.firstChild.getAttribute!=="undefined"&&g.firstChild.getAttribute("href")!=="#")o.attrHandle.href=function(i){return i.getAttribute("href",2)};g=null})();t.querySelectorAll&& +function(){var g=k,i=t.createElement("div");i.innerHTML="

";if(!(i.querySelectorAll&&i.querySelectorAll(".TEST").length===0)){k=function(m,p,q,u){p=p||t;m=m.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!u&&!k.isXML(p))if(p.nodeType===9)try{return C(p.querySelectorAll(m),q)}catch(y){}else if(p.nodeType===1&&p.nodeName.toLowerCase()!=="object"){var F=p.getAttribute("id"),M=F||"__sizzle__";F||p.setAttribute("id",M);try{return C(p.querySelectorAll("#"+M+" "+m),q)}catch(N){}finally{F|| +p.removeAttribute("id")}}return g(m,p,q,u)};for(var n in g)k[n]=g[n];i=null}}();(function(){var g=t.documentElement,i=g.matchesSelector||g.mozMatchesSelector||g.webkitMatchesSelector||g.msMatchesSelector,n=false;try{i.call(t.documentElement,"[test!='']:sizzle")}catch(m){n=true}if(i)k.matchesSelector=function(p,q){q=q.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!k.isXML(p))try{if(n||!o.match.PSEUDO.test(q)&&!/!=/.test(q))return i.call(p,q)}catch(u){}return k(q,null,null,[p]).length>0}})();(function(){var g= +t.createElement("div");g.innerHTML="
";if(!(!g.getElementsByClassName||g.getElementsByClassName("e").length===0)){g.lastChild.className="e";if(g.getElementsByClassName("e").length!==1){o.order.splice(1,0,"CLASS");o.find.CLASS=function(i,n,m){if(typeof n.getElementsByClassName!=="undefined"&&!m)return n.getElementsByClassName(i[1])};g=null}}})();k.contains=t.documentElement.contains?function(g,i){return g!==i&&(g.contains?g.contains(i):true)}:t.documentElement.compareDocumentPosition? +function(g,i){return!!(g.compareDocumentPosition(i)&16)}:function(){return false};k.isXML=function(g){return(g=(g?g.ownerDocument||g:0).documentElement)?g.nodeName!=="HTML":false};var L=function(g,i){for(var n,m=[],p="",q=i.nodeType?[i]:i;n=o.match.PSEUDO.exec(g);){p+=n[0];g=g.replace(o.match.PSEUDO,"")}g=o.relative[g]?g+"*":g;n=0;for(var u=q.length;n0)for(var h=d;h0},closest:function(a,b){var d=[],e,f,h=this[0];if(c.isArray(a)){var l,k={},o=1;if(h&&a.length){e=0;for(f=a.length;e-1:c(h).is(e))d.push({selector:l,elem:h,level:o})}h= +h.parentNode;o++}}return d}l=cb.test(a)?c(a,b||this.context):null;e=0;for(f=this.length;e-1:c.find.matchesSelector(h,a)){d.push(h);break}else{h=h.parentNode;if(!h||!h.ownerDocument||h===b)break}d=d.length>1?c.unique(d):d;return this.pushStack(d,"closest",a)},index:function(a){if(!a||typeof a==="string")return c.inArray(this[0],a?c(a):this.parent().children());return c.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var d=typeof a==="string"?c(a,b||this.context): +c.makeArray(a),e=c.merge(this.get(),d);return this.pushStack(!d[0]||!d[0].parentNode||d[0].parentNode.nodeType===11||!e[0]||!e[0].parentNode||e[0].parentNode.nodeType===11?e:c.unique(e))},andSelf:function(){return this.add(this.prevObject)}});c.each({parent:function(a){return(a=a.parentNode)&&a.nodeType!==11?a:null},parents:function(a){return c.dir(a,"parentNode")},parentsUntil:function(a,b,d){return c.dir(a,"parentNode",d)},next:function(a){return c.nth(a,2,"nextSibling")},prev:function(a){return c.nth(a, +2,"previousSibling")},nextAll:function(a){return c.dir(a,"nextSibling")},prevAll:function(a){return c.dir(a,"previousSibling")},nextUntil:function(a,b,d){return c.dir(a,"nextSibling",d)},prevUntil:function(a,b,d){return c.dir(a,"previousSibling",d)},siblings:function(a){return c.sibling(a.parentNode.firstChild,a)},children:function(a){return c.sibling(a.firstChild)},contents:function(a){return c.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:c.makeArray(a.childNodes)}},function(a, +b){c.fn[a]=function(d,e){var f=c.map(this,b,d);Za.test(a)||(e=d);if(e&&typeof e==="string")f=c.filter(e,f);f=this.length>1?c.unique(f):f;if((this.length>1||ab.test(e))&&$a.test(a))f=f.reverse();return this.pushStack(f,a,bb.call(arguments).join(","))}});c.extend({filter:function(a,b,d){if(d)a=":not("+a+")";return b.length===1?c.find.matchesSelector(b[0],a)?[b[0]]:[]:c.find.matches(a,b)},dir:function(a,b,d){var e=[];for(a=a[b];a&&a.nodeType!==9&&(d===B||a.nodeType!==1||!c(a).is(d));){a.nodeType===1&& +e.push(a);a=a[b]}return e},nth:function(a,b,d){b=b||1;for(var e=0;a;a=a[d])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){for(var d=[];a;a=a.nextSibling)a.nodeType===1&&a!==b&&d.push(a);return d}});var za=/ jQuery\d+="(?:\d+|null)"/g,$=/^\s+/,Aa=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Ba=/<([\w:]+)/,db=/\s]+\/)>/g,P={option:[1, +""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]};P.optgroup=P.option;P.tbody=P.tfoot=P.colgroup=P.caption=P.thead;P.th=P.td;if(!c.support.htmlSerialize)P._default=[1,"div
","
"];c.fn.extend({text:function(a){if(c.isFunction(a))return this.each(function(b){var d= +c(this);d.text(a.call(this,b,d.text()))});if(typeof a!=="object"&&a!==B)return this.empty().append((this[0]&&this[0].ownerDocument||t).createTextNode(a));return c.text(this)},wrapAll:function(a){if(c.isFunction(a))return this.each(function(d){c(this).wrapAll(a.call(this,d))});if(this[0]){var b=c(a,this[0].ownerDocument).eq(0).clone(true);this[0].parentNode&&b.insertBefore(this[0]);b.map(function(){for(var d=this;d.firstChild&&d.firstChild.nodeType===1;)d=d.firstChild;return d}).append(this)}return this}, +wrapInner:function(a){if(c.isFunction(a))return this.each(function(b){c(this).wrapInner(a.call(this,b))});return this.each(function(){var b=c(this),d=b.contents();d.length?d.wrapAll(a):b.append(a)})},wrap:function(a){return this.each(function(){c(this).wrapAll(a)})},unwrap:function(){return this.parent().each(function(){c.nodeName(this,"body")||c(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.appendChild(a)})}, +prepend:function(){return this.domManip(arguments,true,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b,this)});else if(arguments.length){var a=c(arguments[0]);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,false,function(b){this.parentNode.insertBefore(b, +this.nextSibling)});else if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,c(arguments[0]).toArray());return a}},remove:function(a,b){for(var d=0,e;(e=this[d])!=null;d++)if(!a||c.filter(a,[e]).length){if(!b&&e.nodeType===1){c.cleanData(e.getElementsByTagName("*"));c.cleanData([e])}e.parentNode&&e.parentNode.removeChild(e)}return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++)for(b.nodeType===1&&c.cleanData(b.getElementsByTagName("*"));b.firstChild;)b.removeChild(b.firstChild); +return this},clone:function(a){var b=this.map(function(){if(!c.support.noCloneEvent&&!c.isXMLDoc(this)){var d=this.outerHTML,e=this.ownerDocument;if(!d){d=e.createElement("div");d.appendChild(this.cloneNode(true));d=d.innerHTML}return c.clean([d.replace(za,"").replace(fb,'="$1">').replace($,"")],e)[0]}else return this.cloneNode(true)});if(a===true){na(this,b);na(this.find("*"),b.find("*"))}return b},html:function(a){if(a===B)return this[0]&&this[0].nodeType===1?this[0].innerHTML.replace(za,""):null; +else if(typeof a==="string"&&!Ca.test(a)&&(c.support.leadingWhitespace||!$.test(a))&&!P[(Ba.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Aa,"<$1>");try{for(var b=0,d=this.length;b0||e.cacheable||this.length>1?h.cloneNode(true):h)}k.length&&c.each(k,Oa)}return this}});c.buildFragment=function(a,b,d){var e,f,h;b=b&&b[0]?b[0].ownerDocument||b[0]:t;if(a.length===1&&typeof a[0]==="string"&&a[0].length<512&&b===t&&!Ca.test(a[0])&&(c.support.checkClone||!Da.test(a[0]))){f=true;if(h=c.fragments[a[0]])if(h!==1)e=h}if(!e){e=b.createDocumentFragment();c.clean(a,b,e,d)}if(f)c.fragments[a[0]]=h?e:1;return{fragment:e,cacheable:f}};c.fragments={};c.each({appendTo:"append", +prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(a,b){c.fn[a]=function(d){var e=[];d=c(d);var f=this.length===1&&this[0].parentNode;if(f&&f.nodeType===11&&f.childNodes.length===1&&d.length===1){d[b](this[0]);return this}else{f=0;for(var h=d.length;f0?this.clone(true):this).get();c(d[f])[b](l);e=e.concat(l)}return this.pushStack(e,a,d.selector)}}});c.extend({clean:function(a,b,d,e){b=b||t;if(typeof b.createElement==="undefined")b=b.ownerDocument|| +b[0]&&b[0].ownerDocument||t;for(var f=[],h=0,l;(l=a[h])!=null;h++){if(typeof l==="number")l+="";if(l){if(typeof l==="string"&&!eb.test(l))l=b.createTextNode(l);else if(typeof l==="string"){l=l.replace(Aa,"<$1>");var k=(Ba.exec(l)||["",""])[1].toLowerCase(),o=P[k]||P._default,x=o[0],r=b.createElement("div");for(r.innerHTML=o[1]+l+o[2];x--;)r=r.lastChild;if(!c.support.tbody){x=db.test(l);k=k==="table"&&!x?r.firstChild&&r.firstChild.childNodes:o[1]===""&&!x?r.childNodes:[];for(o=k.length- +1;o>=0;--o)c.nodeName(k[o],"tbody")&&!k[o].childNodes.length&&k[o].parentNode.removeChild(k[o])}!c.support.leadingWhitespace&&$.test(l)&&r.insertBefore(b.createTextNode($.exec(l)[0]),r.firstChild);l=r.childNodes}if(l.nodeType)f.push(l);else f=c.merge(f,l)}}if(d)for(h=0;f[h];h++)if(e&&c.nodeName(f[h],"script")&&(!f[h].type||f[h].type.toLowerCase()==="text/javascript"))e.push(f[h].parentNode?f[h].parentNode.removeChild(f[h]):f[h]);else{f[h].nodeType===1&&f.splice.apply(f,[h+1,0].concat(c.makeArray(f[h].getElementsByTagName("script")))); +d.appendChild(f[h])}return f},cleanData:function(a){for(var b,d,e=c.cache,f=c.event.special,h=c.support.deleteExpando,l=0,k;(k=a[l])!=null;l++)if(!(k.nodeName&&c.noData[k.nodeName.toLowerCase()]))if(d=k[c.expando]){if((b=e[d])&&b.events)for(var o in b.events)f[o]?c.event.remove(k,o):c.removeEvent(k,o,b.handle);if(h)delete k[c.expando];else k.removeAttribute&&k.removeAttribute(c.expando);delete e[d]}}});var Ea=/alpha\([^)]*\)/i,gb=/opacity=([^)]*)/,hb=/-([a-z])/ig,ib=/([A-Z])/g,Fa=/^-?\d+(?:px)?$/i, +jb=/^-?\d/,kb={position:"absolute",visibility:"hidden",display:"block"},Pa=["Left","Right"],Qa=["Top","Bottom"],W,Ga,aa,lb=function(a,b){return b.toUpperCase()};c.fn.css=function(a,b){if(arguments.length===2&&b===B)return this;return c.access(this,a,b,true,function(d,e,f){return f!==B?c.style(d,e,f):c.css(d,e)})};c.extend({cssHooks:{opacity:{get:function(a,b){if(b){var d=W(a,"opacity","opacity");return d===""?"1":d}else return a.style.opacity}}},cssNumber:{zIndex:true,fontWeight:true,opacity:true, +zoom:true,lineHeight:true},cssProps:{"float":c.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,b,d,e){if(!(!a||a.nodeType===3||a.nodeType===8||!a.style)){var f,h=c.camelCase(b),l=a.style,k=c.cssHooks[h];b=c.cssProps[h]||h;if(d!==B){if(!(typeof d==="number"&&isNaN(d)||d==null)){if(typeof d==="number"&&!c.cssNumber[h])d+="px";if(!k||!("set"in k)||(d=k.set(a,d))!==B)try{l[b]=d}catch(o){}}}else{if(k&&"get"in k&&(f=k.get(a,false,e))!==B)return f;return l[b]}}},css:function(a,b,d){var e,f=c.camelCase(b), +h=c.cssHooks[f];b=c.cssProps[f]||f;if(h&&"get"in h&&(e=h.get(a,true,d))!==B)return e;else if(W)return W(a,b,f)},swap:function(a,b,d){var e={},f;for(f in b){e[f]=a.style[f];a.style[f]=b[f]}d.call(a);for(f in b)a.style[f]=e[f]},camelCase:function(a){return a.replace(hb,lb)}});c.curCSS=c.css;c.each(["height","width"],function(a,b){c.cssHooks[b]={get:function(d,e,f){var h;if(e){if(d.offsetWidth!==0)h=oa(d,b,f);else c.swap(d,kb,function(){h=oa(d,b,f)});if(h<=0){h=W(d,b,b);if(h==="0px"&&aa)h=aa(d,b,b); +if(h!=null)return h===""||h==="auto"?"0px":h}if(h<0||h==null){h=d.style[b];return h===""||h==="auto"?"0px":h}return typeof h==="string"?h:h+"px"}},set:function(d,e){if(Fa.test(e)){e=parseFloat(e);if(e>=0)return e+"px"}else return e}}});if(!c.support.opacity)c.cssHooks.opacity={get:function(a,b){return gb.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var d=a.style;d.zoom=1;var e=c.isNaN(b)?"":"alpha(opacity="+b*100+")",f= +d.filter||"";d.filter=Ea.test(f)?f.replace(Ea,e):d.filter+" "+e}};if(t.defaultView&&t.defaultView.getComputedStyle)Ga=function(a,b,d){var e;d=d.replace(ib,"-$1").toLowerCase();if(!(b=a.ownerDocument.defaultView))return B;if(b=b.getComputedStyle(a,null)){e=b.getPropertyValue(d);if(e===""&&!c.contains(a.ownerDocument.documentElement,a))e=c.style(a,d)}return e};if(t.documentElement.currentStyle)aa=function(a,b){var d,e,f=a.currentStyle&&a.currentStyle[b],h=a.style;if(!Fa.test(f)&&jb.test(f)){d=h.left; +e=a.runtimeStyle.left;a.runtimeStyle.left=a.currentStyle.left;h.left=b==="fontSize"?"1em":f||0;f=h.pixelLeft+"px";h.left=d;a.runtimeStyle.left=e}return f===""?"auto":f};W=Ga||aa;if(c.expr&&c.expr.filters){c.expr.filters.hidden=function(a){var b=a.offsetHeight;return a.offsetWidth===0&&b===0||!c.support.reliableHiddenOffsets&&(a.style.display||c.css(a,"display"))==="none"};c.expr.filters.visible=function(a){return!c.expr.filters.hidden(a)}}var mb=c.now(),nb=/)<[^<]*)*<\/script>/gi, +ob=/^(?:select|textarea)/i,pb=/^(?:color|date|datetime|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,qb=/^(?:GET|HEAD)$/,Ra=/\[\]$/,T=/\=\?(&|$)/,ja=/\?/,rb=/([?&])_=[^&]*/,sb=/^(\w+:)?\/\/([^\/?#]+)/,tb=/%20/g,ub=/#.*$/,Ha=c.fn.load;c.fn.extend({load:function(a,b,d){if(typeof a!=="string"&&Ha)return Ha.apply(this,arguments);else if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var f=a.slice(e,a.length);a=a.slice(0,e)}e="GET";if(b)if(c.isFunction(b)){d=b;b=null}else if(typeof b=== +"object"){b=c.param(b,c.ajaxSettings.traditional);e="POST"}var h=this;c.ajax({url:a,type:e,dataType:"html",data:b,complete:function(l,k){if(k==="success"||k==="notmodified")h.html(f?c("
").append(l.responseText.replace(nb,"")).find(f):l.responseText);d&&h.each(d,[l.responseText,k,l])}});return this},serialize:function(){return c.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?c.makeArray(this.elements):this}).filter(function(){return this.name&& +!this.disabled&&(this.checked||ob.test(this.nodeName)||pb.test(this.type))}).map(function(a,b){var d=c(this).val();return d==null?null:c.isArray(d)?c.map(d,function(e){return{name:b.name,value:e}}):{name:b.name,value:d}}).get()}});c.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){c.fn[b]=function(d){return this.bind(b,d)}});c.extend({get:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b=null}return c.ajax({type:"GET",url:a,data:b,success:d,dataType:e})}, +getScript:function(a,b){return c.get(a,null,b,"script")},getJSON:function(a,b,d){return c.get(a,b,d,"json")},post:function(a,b,d,e){if(c.isFunction(b)){e=e||d;d=b;b={}}return c.ajax({type:"POST",url:a,data:b,success:d,dataType:e})},ajaxSetup:function(a){c.extend(c.ajaxSettings,a)},ajaxSettings:{url:location.href,global:true,type:"GET",contentType:"application/x-www-form-urlencoded",processData:true,async:true,xhr:function(){return new E.XMLHttpRequest},accepts:{xml:"application/xml, text/xml",html:"text/html", +script:"text/javascript, application/javascript",json:"application/json, text/javascript",text:"text/plain",_default:"*/*"}},ajax:function(a){var b=c.extend(true,{},c.ajaxSettings,a),d,e,f,h=b.type.toUpperCase(),l=qb.test(h);b.url=b.url.replace(ub,"");b.context=a&&a.context!=null?a.context:b;if(b.data&&b.processData&&typeof b.data!=="string")b.data=c.param(b.data,b.traditional);if(b.dataType==="jsonp"){if(h==="GET")T.test(b.url)||(b.url+=(ja.test(b.url)?"&":"?")+(b.jsonp||"callback")+"=?");else if(!b.data|| +!T.test(b.data))b.data=(b.data?b.data+"&":"")+(b.jsonp||"callback")+"=?";b.dataType="json"}if(b.dataType==="json"&&(b.data&&T.test(b.data)||T.test(b.url))){d=b.jsonpCallback||"jsonp"+mb++;if(b.data)b.data=(b.data+"").replace(T,"="+d+"$1");b.url=b.url.replace(T,"="+d+"$1");b.dataType="script";var k=E[d];E[d]=function(m){if(c.isFunction(k))k(m);else{E[d]=B;try{delete E[d]}catch(p){}}f=m;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);r&&r.removeChild(A)}}if(b.dataType==="script"&&b.cache===null)b.cache= +false;if(b.cache===false&&l){var o=c.now(),x=b.url.replace(rb,"$1_="+o);b.url=x+(x===b.url?(ja.test(b.url)?"&":"?")+"_="+o:"")}if(b.data&&l)b.url+=(ja.test(b.url)?"&":"?")+b.data;b.global&&c.active++===0&&c.event.trigger("ajaxStart");o=(o=sb.exec(b.url))&&(o[1]&&o[1].toLowerCase()!==location.protocol||o[2].toLowerCase()!==location.host);if(b.dataType==="script"&&h==="GET"&&o){var r=t.getElementsByTagName("head")[0]||t.documentElement,A=t.createElement("script");if(b.scriptCharset)A.charset=b.scriptCharset; +A.src=b.url;if(!d){var C=false;A.onload=A.onreadystatechange=function(){if(!C&&(!this.readyState||this.readyState==="loaded"||this.readyState==="complete")){C=true;c.handleSuccess(b,w,e,f);c.handleComplete(b,w,e,f);A.onload=A.onreadystatechange=null;r&&A.parentNode&&r.removeChild(A)}}}r.insertBefore(A,r.firstChild);return B}var J=false,w=b.xhr();if(w){b.username?w.open(h,b.url,b.async,b.username,b.password):w.open(h,b.url,b.async);try{if(b.data!=null&&!l||a&&a.contentType)w.setRequestHeader("Content-Type", +b.contentType);if(b.ifModified){c.lastModified[b.url]&&w.setRequestHeader("If-Modified-Since",c.lastModified[b.url]);c.etag[b.url]&&w.setRequestHeader("If-None-Match",c.etag[b.url])}o||w.setRequestHeader("X-Requested-With","XMLHttpRequest");w.setRequestHeader("Accept",b.dataType&&b.accepts[b.dataType]?b.accepts[b.dataType]+", */*; q=0.01":b.accepts._default)}catch(I){}if(b.beforeSend&&b.beforeSend.call(b.context,w,b)===false){b.global&&c.active--===1&&c.event.trigger("ajaxStop");w.abort();return false}b.global&& +c.triggerGlobal(b,"ajaxSend",[w,b]);var L=w.onreadystatechange=function(m){if(!w||w.readyState===0||m==="abort"){J||c.handleComplete(b,w,e,f);J=true;if(w)w.onreadystatechange=c.noop}else if(!J&&w&&(w.readyState===4||m==="timeout")){J=true;w.onreadystatechange=c.noop;e=m==="timeout"?"timeout":!c.httpSuccess(w)?"error":b.ifModified&&c.httpNotModified(w,b.url)?"notmodified":"success";var p;if(e==="success")try{f=c.httpData(w,b.dataType,b)}catch(q){e="parsererror";p=q}if(e==="success"||e==="notmodified")d|| +c.handleSuccess(b,w,e,f);else c.handleError(b,w,e,p);d||c.handleComplete(b,w,e,f);m==="timeout"&&w.abort();if(b.async)w=null}};try{var g=w.abort;w.abort=function(){w&&Function.prototype.call.call(g,w);L("abort")}}catch(i){}b.async&&b.timeout>0&&setTimeout(function(){w&&!J&&L("timeout")},b.timeout);try{w.send(l||b.data==null?null:b.data)}catch(n){c.handleError(b,w,null,n);c.handleComplete(b,w,e,f)}b.async||L();return w}},param:function(a,b){var d=[],e=function(h,l){l=c.isFunction(l)?l():l;d[d.length]= +encodeURIComponent(h)+"="+encodeURIComponent(l)};if(b===B)b=c.ajaxSettings.traditional;if(c.isArray(a)||a.jquery)c.each(a,function(){e(this.name,this.value)});else for(var f in a)da(f,a[f],b,e);return d.join("&").replace(tb,"+")}});c.extend({active:0,lastModified:{},etag:{},handleError:function(a,b,d,e){a.error&&a.error.call(a.context,b,d,e);a.global&&c.triggerGlobal(a,"ajaxError",[b,a,e])},handleSuccess:function(a,b,d,e){a.success&&a.success.call(a.context,e,d,b);a.global&&c.triggerGlobal(a,"ajaxSuccess", +[b,a])},handleComplete:function(a,b,d){a.complete&&a.complete.call(a.context,b,d);a.global&&c.triggerGlobal(a,"ajaxComplete",[b,a]);a.global&&c.active--===1&&c.event.trigger("ajaxStop")},triggerGlobal:function(a,b,d){(a.context&&a.context.url==null?c(a.context):c.event).trigger(b,d)},httpSuccess:function(a){try{return!a.status&&location.protocol==="file:"||a.status>=200&&a.status<300||a.status===304||a.status===1223}catch(b){}return false},httpNotModified:function(a,b){var d=a.getResponseHeader("Last-Modified"), +e=a.getResponseHeader("Etag");if(d)c.lastModified[b]=d;if(e)c.etag[b]=e;return a.status===304},httpData:function(a,b,d){var e=a.getResponseHeader("content-type")||"",f=b==="xml"||!b&&e.indexOf("xml")>=0;a=f?a.responseXML:a.responseText;f&&a.documentElement.nodeName==="parsererror"&&c.error("parsererror");if(d&&d.dataFilter)a=d.dataFilter(a,b);if(typeof a==="string")if(b==="json"||!b&&e.indexOf("json")>=0)a=c.parseJSON(a);else if(b==="script"||!b&&e.indexOf("javascript")>=0)c.globalEval(a);return a}}); +if(E.ActiveXObject)c.ajaxSettings.xhr=function(){if(E.location.protocol!=="file:")try{return new E.XMLHttpRequest}catch(a){}try{return new E.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}};c.support.ajax=!!c.ajaxSettings.xhr();var ea={},vb=/^(?:toggle|show|hide)$/,wb=/^([+\-]=)?([\d+.\-]+)(.*)$/,ba,pa=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]];c.fn.extend({show:function(a,b,d){if(a||a===0)return this.animate(S("show", +3),a,b,d);else{d=0;for(var e=this.length;d=0;e--)if(d[e].elem===this){b&&d[e](true);d.splice(e,1)}});b||this.dequeue();return this}});c.each({slideDown:S("show",1),slideUp:S("hide",1),slideToggle:S("toggle",1),fadeIn:{opacity:"show"},fadeOut:{opacity:"hide"},fadeToggle:{opacity:"toggle"}},function(a,b){c.fn[a]=function(d,e,f){return this.animate(b, +d,e,f)}});c.extend({speed:function(a,b,d){var e=a&&typeof a==="object"?c.extend({},a):{complete:d||!d&&b||c.isFunction(a)&&a,duration:a,easing:d&&b||b&&!c.isFunction(b)&&b};e.duration=c.fx.off?0:typeof e.duration==="number"?e.duration:e.duration in c.fx.speeds?c.fx.speeds[e.duration]:c.fx.speeds._default;e.old=e.complete;e.complete=function(){e.queue!==false&&c(this).dequeue();c.isFunction(e.old)&&e.old.call(this)};return e},easing:{linear:function(a,b,d,e){return d+e*a},swing:function(a,b,d,e){return(-Math.cos(a* +Math.PI)/2+0.5)*e+d}},timers:[],fx:function(a,b,d){this.options=b;this.elem=a;this.prop=d;if(!b.orig)b.orig={}}});c.fx.prototype={update:function(){this.options.step&&this.options.step.call(this.elem,this.now,this);(c.fx.step[this.prop]||c.fx.step._default)(this)},cur:function(){if(this.elem[this.prop]!=null&&(!this.elem.style||this.elem.style[this.prop]==null))return this.elem[this.prop];var a=parseFloat(c.css(this.elem,this.prop));return a&&a>-1E4?a:0},custom:function(a,b,d){function e(l){return f.step(l)} +var f=this,h=c.fx;this.startTime=c.now();this.start=a;this.end=b;this.unit=d||this.unit||"px";this.now=this.start;this.pos=this.state=0;e.elem=this.elem;if(e()&&c.timers.push(e)&&!ba)ba=setInterval(h.tick,h.interval)},show:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.show=true;this.custom(this.prop==="width"||this.prop==="height"?1:0,this.cur());c(this.elem).show()},hide:function(){this.options.orig[this.prop]=c.style(this.elem,this.prop);this.options.hide=true; +this.custom(this.cur(),0)},step:function(a){var b=c.now(),d=true;if(a||b>=this.options.duration+this.startTime){this.now=this.end;this.pos=this.state=1;this.update();this.options.curAnim[this.prop]=true;for(var e in this.options.curAnim)if(this.options.curAnim[e]!==true)d=false;if(d){if(this.options.overflow!=null&&!c.support.shrinkWrapBlocks){var f=this.elem,h=this.options;c.each(["","X","Y"],function(k,o){f.style["overflow"+o]=h.overflow[k]})}this.options.hide&&c(this.elem).hide();if(this.options.hide|| +this.options.show)for(var l in this.options.curAnim)c.style(this.elem,l,this.options.orig[l]);this.options.complete.call(this.elem)}return false}else{a=b-this.startTime;this.state=a/this.options.duration;b=this.options.easing||(c.easing.swing?"swing":"linear");this.pos=c.easing[this.options.specialEasing&&this.options.specialEasing[this.prop]||b](this.state,a,0,1,this.options.duration);this.now=this.start+(this.end-this.start)*this.pos;this.update()}return true}};c.extend(c.fx,{tick:function(){for(var a= +c.timers,b=0;b-1;e={};var x={};if(o)x=f.position();l=o?x.top:parseInt(l,10)||0;k=o?x.left:parseInt(k,10)||0;if(c.isFunction(b))b=b.call(a,d,h);if(b.top!=null)e.top=b.top-h.top+l;if(b.left!=null)e.left=b.left-h.left+k;"using"in b?b.using.call(a, +e):f.css(e)}};c.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),d=this.offset(),e=Ia.test(b[0].nodeName)?{top:0,left:0}:b.offset();d.top-=parseFloat(c.css(a,"marginTop"))||0;d.left-=parseFloat(c.css(a,"marginLeft"))||0;e.top+=parseFloat(c.css(b[0],"borderTopWidth"))||0;e.left+=parseFloat(c.css(b[0],"borderLeftWidth"))||0;return{top:d.top-e.top,left:d.left-e.left}},offsetParent:function(){return this.map(function(){for(var a=this.offsetParent||t.body;a&&!Ia.test(a.nodeName)&& +c.css(a,"position")==="static";)a=a.offsetParent;return a})}});c.each(["Left","Top"],function(a,b){var d="scroll"+b;c.fn[d]=function(e){var f=this[0],h;if(!f)return null;if(e!==B)return this.each(function(){if(h=fa(this))h.scrollTo(!a?e:c(h).scrollLeft(),a?e:c(h).scrollTop());else this[d]=e});else return(h=fa(f))?"pageXOffset"in h?h[a?"pageYOffset":"pageXOffset"]:c.support.boxModel&&h.document.documentElement[d]||h.document.body[d]:f[d]}});c.each(["Height","Width"],function(a,b){var d=b.toLowerCase(); +c.fn["inner"+b]=function(){return this[0]?parseFloat(c.css(this[0],d,"padding")):null};c.fn["outer"+b]=function(e){return this[0]?parseFloat(c.css(this[0],d,e?"margin":"border")):null};c.fn[d]=function(e){var f=this[0];if(!f)return e==null?null:this;if(c.isFunction(e))return this.each(function(l){var k=c(this);k[d](e.call(this,l,k[d]()))});if(c.isWindow(f))return f.document.compatMode==="CSS1Compat"&&f.document.documentElement["client"+b]||f.document.body["client"+b];else if(f.nodeType===9)return Math.max(f.documentElement["client"+ +b],f.body["scroll"+b],f.documentElement["scroll"+b],f.body["offset"+b],f.documentElement["offset"+b]);else if(e===B){f=c.css(f,d);var h=parseFloat(f);return c.isNaN(h)?f:h}else return this.css(d,typeof e==="string"?e:e+"px")}})})(window); diff --git a/News/includes/lib/plugins.js b/News/includes/lib/plugins.js new file mode 100644 index 0000000..9716e4a --- /dev/null +++ b/News/includes/lib/plugins.js @@ -0,0 +1,526 @@ +/*! + * jQuery UI 1.8.7 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI + */ +(function(c,j){function k(a){return!c(a).parents().andSelf().filter(function(){return c.curCSS(this,"visibility")==="hidden"||c.expr.filters.hidden(this)}).length}c.ui=c.ui||{};if(!c.ui.version){c.extend(c.ui,{version:"1.8.7",keyCode:{ALT:18,BACKSPACE:8,CAPS_LOCK:20,COMMA:188,COMMAND:91,COMMAND_LEFT:91,COMMAND_RIGHT:93,CONTROL:17,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,INSERT:45,LEFT:37,MENU:93,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106, +NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SHIFT:16,SPACE:32,TAB:9,UP:38,WINDOWS:91}});c.fn.extend({_focus:c.fn.focus,focus:function(a,b){return typeof a==="number"?this.each(function(){var d=this;setTimeout(function(){c(d).focus();b&&b.call(d)},a)}):this._focus.apply(this,arguments)},scrollParent:function(){var a;a=c.browser.msie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(c.curCSS(this, +"position",1))&&/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(c.curCSS(this,"overflow",1)+c.curCSS(this,"overflow-y",1)+c.curCSS(this,"overflow-x",1))}).eq(0);return/fixed/.test(this.css("position"))||!a.length?c(document):a},zIndex:function(a){if(a!==j)return this.css("zIndex",a);if(this.length){a=c(this[0]);for(var b;a.length&&a[0]!==document;){b=a.css("position"); +if(b==="absolute"||b==="relative"||b==="fixed"){b=parseInt(a.css("zIndex"),10);if(!isNaN(b)&&b!==0)return b}a=a.parent()}}return 0},disableSelection:function(){return this.bind((c.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(a){a.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}});c.each(["Width","Height"],function(a,b){function d(f,g,l,m){c.each(e,function(){g-=parseFloat(c.curCSS(f,"padding"+this,true))||0;if(l)g-=parseFloat(c.curCSS(f, +"border"+this+"Width",true))||0;if(m)g-=parseFloat(c.curCSS(f,"margin"+this,true))||0});return g}var e=b==="Width"?["Left","Right"]:["Top","Bottom"],h=b.toLowerCase(),i={innerWidth:c.fn.innerWidth,innerHeight:c.fn.innerHeight,outerWidth:c.fn.outerWidth,outerHeight:c.fn.outerHeight};c.fn["inner"+b]=function(f){if(f===j)return i["inner"+b].call(this);return this.each(function(){c(this).css(h,d(this,f)+"px")})};c.fn["outer"+b]=function(f,g){if(typeof f!=="number")return i["outer"+b].call(this,f);return this.each(function(){c(this).css(h, +d(this,f,true,g)+"px")})}});c.extend(c.expr[":"],{data:function(a,b,d){return!!c.data(a,d[3])},focusable:function(a){var b=a.nodeName.toLowerCase(),d=c.attr(a,"tabindex");if("area"===b){b=a.parentNode;d=b.name;if(!a.href||!d||b.nodeName.toLowerCase()!=="map")return false;a=c("img[usemap=#"+d+"]")[0];return!!a&&k(a)}return(/input|select|textarea|button|object/.test(b)?!a.disabled:"a"==b?a.href||!isNaN(d):!isNaN(d))&&k(a)},tabbable:function(a){var b=c.attr(a,"tabindex");return(isNaN(b)||b>=0)&&c(a).is(":focusable")}}); +c(function(){var a=document.body,b=a.appendChild(b=document.createElement("div"));c.extend(b.style,{minHeight:"100px",height:"auto",padding:0,borderWidth:0});c.support.minHeight=b.offsetHeight===100;c.support.selectstart="onselectstart"in b;a.removeChild(b).style.display="none"});c.extend(c.ui,{plugin:{add:function(a,b,d){a=c.ui[a].prototype;for(var e in d){a.plugins[e]=a.plugins[e]||[];a.plugins[e].push([b,d[e]])}},call:function(a,b,d){if((b=a.plugins[b])&&a.element[0].parentNode)for(var e=0;e0)return true;a[b]=1;d=a[b]>0;a[b]=0;return d},isOverAxis:function(a,b,d){return a>b&&a=9)&&!a.button)return this._mouseUp(a);if(this._mouseStarted){this._mouseDrag(a); +return a.preventDefault()}if(this._mouseDistanceMet(a)&&this._mouseDelayMet(a))(this._mouseStarted=this._mouseStart(this._mouseDownEvent,a)!==false)?this._mouseDrag(a):this._mouseUp(a);return!this._mouseStarted},_mouseUp:function(a){c(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate);if(this._mouseStarted){this._mouseStarted=false;a.target==this._mouseDownEvent.target&&c.data(a.target,this.widgetName+".preventClickEvent", +true);this._mouseStop(a)}return false},_mouseDistanceMet:function(a){return Math.max(Math.abs(this._mouseDownEvent.pageX-a.pageX),Math.abs(this._mouseDownEvent.pageY-a.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return true}})})(jQuery); +;/* + * jQuery UI Draggable 1.8.7 + * + * Copyright 2010, AUTHORS.txt (http://jqueryui.com/about) + * Dual licensed under the MIT or GPL Version 2 licenses. + * http://jquery.org/license + * + * http://docs.jquery.com/UI/Draggables + * + * Depends: + * jquery.ui.core.js + * jquery.ui.mouse.js + * jquery.ui.widget.js + */ +(function(d){d.widget("ui.draggable",d.ui.mouse,{widgetEventPrefix:"drag",options:{addClasses:true,appendTo:"parent",axis:false,connectToSortable:false,containment:false,cursor:"auto",cursorAt:false,grid:false,handle:false,helper:"original",iframeFix:false,opacity:false,refreshPositions:false,revert:false,revertDuration:500,scope:"default",scroll:true,scrollSensitivity:20,scrollSpeed:20,snap:false,snapMode:"both",snapTolerance:20,stack:false,zIndex:false},_create:function(){if(this.options.helper== +"original"&&!/^(?:r|a|f)/.test(this.element.css("position")))this.element[0].style.position="relative";this.options.addClasses&&this.element.addClass("ui-draggable");this.options.disabled&&this.element.addClass("ui-draggable-disabled");this._mouseInit()},destroy:function(){if(this.element.data("draggable")){this.element.removeData("draggable").unbind(".draggable").removeClass("ui-draggable ui-draggable-dragging ui-draggable-disabled");this._mouseDestroy();return this}},_mouseCapture:function(a){var b= +this.options;if(this.helper||b.disabled||d(a.target).is(".ui-resizable-handle"))return false;this.handle=this._getHandle(a);if(!this.handle)return false;return true},_mouseStart:function(a){var b=this.options;this.helper=this._createHelper(a);this._cacheHelperProportions();if(d.ui.ddmanager)d.ui.ddmanager.current=this;this._cacheMargins();this.cssPosition=this.helper.css("position");this.scrollParent=this.helper.scrollParent();this.offset=this.positionAbs=this.element.offset();this.offset={top:this.offset.top- +this.margins.top,left:this.offset.left-this.margins.left};d.extend(this.offset,{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this.position=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);b.containment&&this._setContainment();if(this._trigger("start",a)===false){this._clear();return false}this._cacheHelperProportions(); +d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.helper.addClass("ui-draggable-dragging");this._mouseDrag(a,true);return true},_mouseDrag:function(a,b){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute");if(!b){b=this._uiHash();if(this._trigger("drag",a,b)===false){this._mouseUp({});return false}this.position=b.position}if(!this.options.axis||this.options.axis!="y")this.helper[0].style.left=this.position.left+"px";if(!this.options.axis|| +this.options.axis!="x")this.helper[0].style.top=this.position.top+"px";d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);return false},_mouseStop:function(a){var b=false;if(d.ui.ddmanager&&!this.options.dropBehaviour)b=d.ui.ddmanager.drop(this,a);if(this.dropped){b=this.dropped;this.dropped=false}if(!this.element[0]||!this.element[0].parentNode)return false;if(this.options.revert=="invalid"&&!b||this.options.revert=="valid"&&b||this.options.revert===true||d.isFunction(this.options.revert)&&this.options.revert.call(this.element, +b)){var c=this;d(this.helper).animate(this.originalPosition,parseInt(this.options.revertDuration,10),function(){c._trigger("stop",a)!==false&&c._clear()})}else this._trigger("stop",a)!==false&&this._clear();return false},cancel:function(){this.helper.is(".ui-draggable-dragging")?this._mouseUp({}):this._clear();return this},_getHandle:function(a){var b=!this.options.handle||!d(this.options.handle,this.element).length?true:false;d(this.options.handle,this.element).find("*").andSelf().each(function(){if(this== +a.target)b=true});return b},_createHelper:function(a){var b=this.options;a=d.isFunction(b.helper)?d(b.helper.apply(this.element[0],[a])):b.helper=="clone"?this.element.clone():this.element;a.parents("body").length||a.appendTo(b.appendTo=="parent"?this.element[0].parentNode:b.appendTo);a[0]!=this.element[0]&&!/(fixed|absolute)/.test(a.css("position"))&&a.css("position","absolute");return a},_adjustOffsetFromHelper:function(a){if(typeof a=="string")a=a.split(" ");if(d.isArray(a))a={left:+a[0],top:+a[1]|| +0};if("left"in a)this.offset.click.left=a.left+this.margins.left;if("right"in a)this.offset.click.left=this.helperProportions.width-a.right+this.margins.left;if("top"in a)this.offset.click.top=a.top+this.margins.top;if("bottom"in a)this.offset.click.top=this.helperProportions.height-a.bottom+this.margins.top},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var a=this.offsetParent.offset();if(this.cssPosition=="absolute"&&this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0], +this.offsetParent[0])){a.left+=this.scrollParent.scrollLeft();a.top+=this.scrollParent.scrollTop()}if(this.offsetParent[0]==document.body||this.offsetParent[0].tagName&&this.offsetParent[0].tagName.toLowerCase()=="html"&&d.browser.msie)a={top:0,left:0};return{top:a.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:a.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if(this.cssPosition=="relative"){var a=this.element.position();return{top:a.top- +(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:a.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}else return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.element.css("marginLeft"),10)||0,top:parseInt(this.element.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var a=this.options;if(a.containment== +"parent")a.containment=this.helper[0].parentNode;if(a.containment=="document"||a.containment=="window")this.containment=[(a.containment=="document"?0:d(window).scrollLeft())-this.offset.relative.left-this.offset.parent.left,(a.containment=="document"?0:d(window).scrollTop())-this.offset.relative.top-this.offset.parent.top,(a.containment=="document"?0:d(window).scrollLeft())+d(a.containment=="document"?document:window).width()-this.helperProportions.width-this.margins.left,(a.containment=="document"? +0:d(window).scrollTop())+(d(a.containment=="document"?document:window).height()||document.body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top];if(!/^(document|window|parent)$/.test(a.containment)&&a.containment.constructor!=Array){var b=d(a.containment)[0];if(b){a=d(a.containment).offset();var c=d(b).css("overflow")!="hidden";this.containment=[a.left+(parseInt(d(b).css("borderLeftWidth"),10)||0)+(parseInt(d(b).css("paddingLeft"),10)||0)-this.margins.left,a.top+(parseInt(d(b).css("borderTopWidth"), +10)||0)+(parseInt(d(b).css("paddingTop"),10)||0)-this.margins.top,a.left+(c?Math.max(b.scrollWidth,b.offsetWidth):b.offsetWidth)-(parseInt(d(b).css("borderLeftWidth"),10)||0)-(parseInt(d(b).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,a.top+(c?Math.max(b.scrollHeight,b.offsetHeight):b.offsetHeight)-(parseInt(d(b).css("borderTopWidth"),10)||0)-(parseInt(d(b).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top]}}else if(a.containment.constructor== +Array)this.containment=a.containment},_convertPositionTo:function(a,b){if(!b)b=this.position;a=a=="absolute"?1:-1;var c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName);return{top:b.top+this.offset.relative.top*a+this.offset.parent.top*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollTop(): +f?0:c.scrollTop())*a),left:b.left+this.offset.relative.left*a+this.offset.parent.left*a-(d.browser.safari&&d.browser.version<526&&this.cssPosition=="fixed"?0:(this.cssPosition=="fixed"?-this.scrollParent.scrollLeft():f?0:c.scrollLeft())*a)}},_generatePosition:function(a){var b=this.options,c=this.cssPosition=="absolute"&&!(this.scrollParent[0]!=document&&d.ui.contains(this.scrollParent[0],this.offsetParent[0]))?this.offsetParent:this.scrollParent,f=/(html|body)/i.test(c[0].tagName),e=a.pageX,g=a.pageY; +if(this.originalPosition){if(this.containment){if(a.pageX-this.offset.click.leftthis.containment[2])e=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g-this.originalPageY)/ +b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.topthis.containment[3])?g:!(g-this.offset.click.topthis.containment[2])?e:!(e-this.offset.click.left
').css({width:this.offsetWidth+"px",height:this.offsetHeight+"px",position:"absolute",opacity:"0.001",zIndex:1E3}).css(d(this).offset()).appendTo("body")})}, +stop:function(){d("div.ui-draggable-iframeFix").each(function(){this.parentNode.removeChild(this)})}});d.ui.plugin.add("draggable","opacity",{start:function(a,b){a=d(b.helper);b=d(this).data("draggable").options;if(a.css("opacity"))b._opacity=a.css("opacity");a.css("opacity",b.opacity)},stop:function(a,b){a=d(this).data("draggable").options;a._opacity&&d(b.helper).css("opacity",a._opacity)}});d.ui.plugin.add("draggable","scroll",{start:function(){var a=d(this).data("draggable");if(a.scrollParent[0]!= +document&&a.scrollParent[0].tagName!="HTML")a.overflowOffset=a.scrollParent.offset()},drag:function(a){var b=d(this).data("draggable"),c=b.options,f=false;if(b.scrollParent[0]!=document&&b.scrollParent[0].tagName!="HTML"){if(!c.axis||c.axis!="x")if(b.overflowOffset.top+b.scrollParent[0].offsetHeight-a.pageY=0;h--){var i=c.snapElements[h].left,k=i+c.snapElements[h].width,j=c.snapElements[h].top,l=j+c.snapElements[h].height;if(i-e=j&&f<=l||h>=j&&h<=l||fl)&&(e>= +i&&e<=k||g>=i&&g<=k||ek);default:return false}};d.ui.ddmanager={current:null,droppables:{"default":[]},prepareOffsets:function(a,b){var c=d.ui.ddmanager.droppables[a.options.scope]||[],e=b?b.type:null,g=(a.currentItem||a.element).find(":data(droppable)").andSelf(),f=0;a:for(;f *",opacity:false,placeholder:false,revert:false,scroll:true,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1E3},_create:function(){this.containerCache={};this.element.addClass("ui-sortable"); +this.refresh();this.floating=this.items.length?/left|right/.test(this.items[0].item.css("float")):false;this.offset=this.element.offset();this._mouseInit()},destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").removeData("sortable").unbind(".sortable");this._mouseDestroy();for(var a=this.items.length-1;a>=0;a--)this.items[a].item.removeData("sortable-item");return this},_setOption:function(a,b){if(a==="disabled"){this.options[a]=b;this.widget()[b?"addClass":"removeClass"]("ui-sortable-disabled")}else d.Widget.prototype._setOption.apply(this, +arguments)},_mouseCapture:function(a,b){if(this.reverting)return false;if(this.options.disabled||this.options.type=="static")return false;this._refreshItems(a);var c=null,e=this;d(a.target).parents().each(function(){if(d.data(this,"sortable-item")==e){c=d(this);return false}});if(d.data(a.target,"sortable-item")==e)c=d(a.target);if(!c)return false;if(this.options.handle&&!b){var f=false;d(this.options.handle,c).find("*").andSelf().each(function(){if(this==a.target)f=true});if(!f)return false}this.currentItem= +c;this._removeCurrentsFromItems();return true},_mouseStart:function(a,b,c){b=this.options;var e=this;this.currentContainer=this;this.refreshPositions();this.helper=this._createHelper(a);this._cacheHelperProportions();this._cacheMargins();this.scrollParent=this.helper.scrollParent();this.offset=this.currentItem.offset();this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left};this.helper.css("position","absolute");this.cssPosition=this.helper.css("position");d.extend(this.offset, +{click:{left:a.pageX-this.offset.left,top:a.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()});this.originalPosition=this._generatePosition(a);this.originalPageX=a.pageX;this.originalPageY=a.pageY;b.cursorAt&&this._adjustOffsetFromHelper(b.cursorAt);this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]};this.helper[0]!=this.currentItem[0]&&this.currentItem.hide();this._createPlaceholder();b.containment&&this._setContainment(); +if(b.cursor){if(d("body").css("cursor"))this._storedCursor=d("body").css("cursor");d("body").css("cursor",b.cursor)}if(b.opacity){if(this.helper.css("opacity"))this._storedOpacity=this.helper.css("opacity");this.helper.css("opacity",b.opacity)}if(b.zIndex){if(this.helper.css("zIndex"))this._storedZIndex=this.helper.css("zIndex");this.helper.css("zIndex",b.zIndex)}if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML")this.overflowOffset=this.scrollParent.offset();this._trigger("start", +a,this._uiHash());this._preserveHelperProportions||this._cacheHelperProportions();if(!c)for(c=this.containers.length-1;c>=0;c--)this.containers[c]._trigger("activate",a,e._uiHash(this));if(d.ui.ddmanager)d.ui.ddmanager.current=this;d.ui.ddmanager&&!b.dropBehaviour&&d.ui.ddmanager.prepareOffsets(this,a);this.dragging=true;this.helper.addClass("ui-sortable-helper");this._mouseDrag(a);return true},_mouseDrag:function(a){this.position=this._generatePosition(a);this.positionAbs=this._convertPositionTo("absolute"); +if(!this.lastPositionAbs)this.lastPositionAbs=this.positionAbs;if(this.options.scroll){var b=this.options,c=false;if(this.scrollParent[0]!=document&&this.scrollParent[0].tagName!="HTML"){if(this.overflowOffset.top+this.scrollParent[0].offsetHeight-a.pageY=0;b--){c=this.items[b];var e=c.item[0],f=this._intersectsWithPointer(c);if(f)if(e!=this.currentItem[0]&&this.placeholder[f==1?"next":"prev"]()[0]!=e&&!d.ui.contains(this.placeholder[0],e)&&(this.options.type=="semi-dynamic"?!d.ui.contains(this.element[0],e):true)){this.direction=f==1?"down":"up";if(this.options.tolerance=="pointer"||this._intersectsWithSides(c))this._rearrange(a, +c);else break;this._trigger("change",a,this._uiHash());break}}this._contactContainers(a);d.ui.ddmanager&&d.ui.ddmanager.drag(this,a);this._trigger("sort",a,this._uiHash());this.lastPositionAbs=this.positionAbs;return false},_mouseStop:function(a,b){if(a){d.ui.ddmanager&&!this.options.dropBehaviour&&d.ui.ddmanager.drop(this,a);if(this.options.revert){var c=this;b=c.placeholder.offset();c.reverting=true;d(this.helper).animate({left:b.left-this.offset.parent.left-c.margins.left+(this.offsetParent[0]== +document.body?0:this.offsetParent[0].scrollLeft),top:b.top-this.offset.parent.top-c.margins.top+(this.offsetParent[0]==document.body?0:this.offsetParent[0].scrollTop)},parseInt(this.options.revert,10)||500,function(){c._clear(a)})}else this._clear(a,b);return false}},cancel:function(){var a=this;if(this.dragging){this._mouseUp();this.options.helper=="original"?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var b=this.containers.length-1;b>=0;b--){this.containers[b]._trigger("deactivate", +null,a._uiHash(this));if(this.containers[b].containerCache.over){this.containers[b]._trigger("out",null,a._uiHash(this));this.containers[b].containerCache.over=0}}}this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]);this.options.helper!="original"&&this.helper&&this.helper[0].parentNode&&this.helper.remove();d.extend(this,{helper:null,dragging:false,reverting:false,_noFinalSort:null});this.domPosition.prev?d(this.domPosition.prev).after(this.currentItem): +d(this.domPosition.parent).prepend(this.currentItem);return this},serialize:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};d(b).each(function(){var e=(d(a.item||this).attr(a.attribute||"id")||"").match(a.expression||/(.+)[-=_](.+)/);if(e)c.push((a.key||e[1]+"[]")+"="+(a.key&&a.expression?e[1]:e[2]))});!c.length&&a.key&&c.push(a.key+"=");return c.join("&")},toArray:function(a){var b=this._getItemsAsjQuery(a&&a.connected),c=[];a=a||{};b.each(function(){c.push(d(a.item||this).attr(a.attribute|| +"id")||"")});return c},_intersectsWith:function(a){var b=this.positionAbs.left,c=b+this.helperProportions.width,e=this.positionAbs.top,f=e+this.helperProportions.height,g=a.left,h=g+a.width,i=a.top,k=i+a.height,j=this.offset.click.top,l=this.offset.click.left;j=e+j>i&&e+jg&&b+la[this.floating?"width":"height"]?j:g0?"down":"up")}, +_getDragHorizontalDirection:function(){var a=this.positionAbs.left-this.lastPositionAbs.left;return a!=0&&(a>0?"right":"left")},refresh:function(a){this._refreshItems(a);this.refreshPositions();return this},_connectWith:function(){var a=this.options;return a.connectWith.constructor==String?[a.connectWith]:a.connectWith},_getItemsAsjQuery:function(a){var b=[],c=[],e=this._connectWith();if(e&&a)for(a=e.length-1;a>=0;a--)for(var f=d(e[a]),g=f.length-1;g>=0;g--){var h=d.data(f[g],"sortable");if(h&&h!= +this&&!h.options.disabled)c.push([d.isFunction(h.options.items)?h.options.items.call(h.element):d(h.options.items,h.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),h])}c.push([d.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):d(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]);for(a=c.length-1;a>=0;a--)c[a][0].each(function(){b.push(this)});return d(b)},_removeCurrentsFromItems:function(){for(var a= +this.currentItem.find(":data(sortable-item)"),b=0;b=0;f--)for(var g=d(e[f]),h=g.length-1;h>=0;h--){var i=d.data(g[h],"sortable"); +if(i&&i!=this&&!i.options.disabled){c.push([d.isFunction(i.options.items)?i.options.items.call(i.element[0],a,{item:this.currentItem}):d(i.options.items,i.element),i]);this.containers.push(i)}}for(f=c.length-1;f>=0;f--){a=c[f][1];e=c[f][0];h=0;for(g=e.length;h= +0;b--){var c=this.items[b],e=this.options.toleranceElement?d(this.options.toleranceElement,c.item):c.item;if(!a){c.width=e.outerWidth();c.height=e.outerHeight()}e=e.offset();c.left=e.left;c.top=e.top}if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(b=this.containers.length-1;b>=0;b--){e=this.containers[b].element.offset();this.containers[b].containerCache.left=e.left;this.containers[b].containerCache.top=e.top;this.containers[b].containerCache.width= +this.containers[b].element.outerWidth();this.containers[b].containerCache.height=this.containers[b].element.outerHeight()}return this},_createPlaceholder:function(a){var b=a||this,c=b.options;if(!c.placeholder||c.placeholder.constructor==String){var e=c.placeholder;c.placeholder={element:function(){var f=d(document.createElement(b.currentItem[0].nodeName)).addClass(e||b.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper")[0];if(!e)f.style.visibility="hidden";return f}, +update:function(f,g){if(!(e&&!c.forcePlaceholderSize)){g.height()||g.height(b.currentItem.innerHeight()-parseInt(b.currentItem.css("paddingTop")||0,10)-parseInt(b.currentItem.css("paddingBottom")||0,10));g.width()||g.width(b.currentItem.innerWidth()-parseInt(b.currentItem.css("paddingLeft")||0,10)-parseInt(b.currentItem.css("paddingRight")||0,10))}}}}b.placeholder=d(c.placeholder.element.call(b.element,b.currentItem));b.currentItem.after(b.placeholder);c.placeholder.update(b,b.placeholder)},_contactContainers:function(a){for(var b= +null,c=null,e=this.containers.length-1;e>=0;e--)if(!d.ui.contains(this.currentItem[0],this.containers[e].element[0]))if(this._intersectsWith(this.containers[e].containerCache)){if(!(b&&d.ui.contains(this.containers[e].element[0],b.element[0]))){b=this.containers[e];c=e}}else if(this.containers[e].containerCache.over){this.containers[e]._trigger("out",a,this._uiHash(this));this.containers[e].containerCache.over=0}if(b)if(this.containers.length===1){this.containers[c]._trigger("over",a,this._uiHash(this)); +this.containers[c].containerCache.over=1}else if(this.currentContainer!=this.containers[c]){b=1E4;e=null;for(var f=this.positionAbs[this.containers[c].floating?"left":"top"],g=this.items.length-1;g>=0;g--)if(d.ui.contains(this.containers[c].element[0],this.items[g].item[0])){var h=this.items[g][this.containers[c].floating?"left":"top"];if(Math.abs(h-f)this.containment[2])f=this.containment[2]+this.offset.click.left;if(a.pageY-this.offset.click.top>this.containment[3])g=this.containment[3]+this.offset.click.top}if(b.grid){g=this.originalPageY+Math.round((g-this.originalPageY)/b.grid[1])*b.grid[1];g=this.containment?!(g-this.offset.click.topthis.containment[3])? +g:!(g-this.offset.click.topthis.containment[2])?f:!(f-this.offset.click.left=0;e--)if(d.ui.contains(this.containers[e].element[0],this.currentItem[0])&&!b){c.push(function(f){return function(g){f._trigger("receive", +g,this._uiHash(this))}}.call(this,this.containers[e]));c.push(function(f){return function(g){f._trigger("update",g,this._uiHash(this))}}.call(this,this.containers[e]))}}for(e=this.containers.length-1;e>=0;e--){b||c.push(function(f){return function(g){f._trigger("deactivate",g,this._uiHash(this))}}.call(this,this.containers[e]));if(this.containers[e].containerCache.over){c.push(function(f){return function(g){f._trigger("out",g,this._uiHash(this))}}.call(this,this.containers[e]));this.containers[e].containerCache.over= +0}}this._storedCursor&&d("body").css("cursor",this._storedCursor);this._storedOpacity&&this.helper.css("opacity",this._storedOpacity);if(this._storedZIndex)this.helper.css("zIndex",this._storedZIndex=="auto"?"":this._storedZIndex);this.dragging=false;if(this.cancelHelperRemoval){if(!b){this._trigger("beforeStop",a,this._uiHash());for(e=0;e=0?'&':'?')+q;options.data=null;}else +options.data=q;var $form=this,callbacks=[];if(options.resetForm)callbacks.push(function(){$form.resetForm();});if(options.clearForm)callbacks.push(function(){$form.clearForm();});if(!options.dataType&&options.target){var oldSuccess=options.success||function(){};callbacks.push(function(data){$(options.target).html(data).each(oldSuccess,arguments);});}else if(options.success)callbacks.push(options.success);options.success=function(data,status){for(var i=0,max=callbacks.length;i');var io=$io[0];if($.browser.msie||$.browser.opera)io.src='javascript:false;document.write("");';$io.css({position:'absolute',top:'-1000px',left:'-1000px'});var xhr={responseText:null,responseXML:null,status:0,statusText:'n/a',getAllResponseHeaders:function(){},getResponseHeader:function(){},setRequestHeader:function(){}};var g=opts.global;if(g&&!$.active++)$.event.trigger("ajaxStart");if(g)$.event.trigger("ajaxSend",[xhr,opts]);var cbInvoked=0;var timedOut=0;setTimeout(function(){var t=$form.attr('target'),a=$form.attr('action');$form.attr({target:id,encoding:'multipart/form-data',enctype:'multipart/form-data',method:'POST',action:opts.url});if(opts.timeout)setTimeout(function(){timedOut=true;cb();},opts.timeout);var extraInputs=[];try{if(options.extraData)for(var n in options.extraData)extraInputs.push($('').appendTo(form)[0]);$io.appendTo('body');io.attachEvent?io.attachEvent('onload',cb):io.addEventListener('load',cb,false);form.submit();}finally{$form.attr('action',a);t?$form.attr('target',t):$form.removeAttr('target');$(extraInputs).remove();}},10);function cb(){if(cbInvoked++)return;io.detachEvent?io.detachEvent('onload',cb):io.removeEventListener('load',cb,false);var operaHack=0;var ok=true;try{if(timedOut)throw'timeout';var data,doc;doc=io.contentWindow?io.contentWindow.document:io.contentDocument?io.contentDocument:io.document;if(doc.body==null&&!operaHack&&$.browser.opera){operaHack=1;cbInvoked--;setTimeout(cb,100);return;}xhr.responseText=doc.body?doc.body.innerHTML:null;xhr.responseXML=doc.XMLDocument?doc.XMLDocument:doc;xhr.getResponseHeader=function(header){var headers={'content-type':opts.dataType};return headers[header];};if(opts.dataType=='json'||opts.dataType=='script'){var ta=doc.getElementsByTagName('textarea')[0];xhr.responseText=ta?ta.value:xhr.responseText;}else if(opts.dataType=='xml'&&!xhr.responseXML&&xhr.responseText!=null){xhr.responseXML=toXml(xhr.responseText);}data=$.httpData(xhr,opts.dataType);}catch(e){ok=false;$.handleError(opts,xhr,'error',e);}if(ok){opts.success(data,'success');if(g)$.event.trigger("ajaxSuccess",[xhr,opts]);}if(g)$.event.trigger("ajaxComplete",[xhr,opts]);if(g&&!--$.active)$.event.trigger("ajaxStop");if(opts.complete)opts.complete(xhr,ok?'success':'error');setTimeout(function(){$io.remove();xhr.responseXML=null;},100);};function toXml(s,doc){if(window.ActiveXObject){doc=new ActiveXObject('Microsoft.XMLDOM');doc.async='false';doc.loadXML(s);}else +doc=(new DOMParser()).parseFromString(s,'text/xml');return(doc&&doc.documentElement&&doc.documentElement.tagName!='parsererror')?doc:null;};};};$.fn.ajaxForm=function(options){return this.ajaxFormUnbind().bind('submit.form-plugin',function(){$(this).ajaxSubmit(options);return false;}).each(function(){$(":submit,input:image",this).bind('click.form-plugin',function(e){var $form=this.form;$form.clk=this;if(this.type=='image'){if(e.offsetX!=undefined){$form.clk_x=e.offsetX;$form.clk_y=e.offsetY;}else if(typeof $.fn.offset=='function'){var offset=$(this).offset();$form.clk_x=e.pageX-offset.left;$form.clk_y=e.pageY-offset.top;}else{$form.clk_x=e.pageX-this.offsetLeft;$form.clk_y=e.pageY-this.offsetTop;}}setTimeout(function(){$form.clk=$form.clk_x=$form.clk_y=null;},10);});});};$.fn.ajaxFormUnbind=function(){this.unbind('submit.form-plugin');return this.each(function(){$(":submit,input:image",this).unbind('click.form-plugin');});};$.fn.formToArray=function(semantic){var a=[];if(this.length==0)return a;var form=this[0];var els=semantic?form.getElementsByTagName('*'):form.elements;if(!els)return a;for(var i=0,max=els.length;i=0?"&":"?")+q;options.data=null}else{options.data=q}var $form=this,callbacks=[];if(options.resetForm){callbacks.push(function(){$form.resetForm()})}if(options.clearForm){callbacks.push(function(){$form.clearForm()})}if(!options.dataType&&options.target){var oldSuccess=options.success||function(){};callbacks.push(function(data){var fn=options.replaceTarget?"replaceWith":"html";$(options.target)[fn](data).each(oldSuccess,arguments)})}else{if(options.success){callbacks.push(options.success)}}options.success=function(data,status,xhr){var context=options.context||options;for(var i=0,max=callbacks.length;i0;var mp="multipart/form-data";var multipart=($form.attr("enctype")==mp||$form.attr("encoding")==mp);if(options.iframe!==false&&(fileInputs||options.iframe||multipart)){if(options.closeKeepAlive){$.get(options.closeKeepAlive,fileUpload)}else{fileUpload()}}else{$.ajax(options)}this.trigger("form-submit-notify",[this,options]);return this;function fileUpload(){var form=$form[0];if($(":input[name=submit],:input[id=submit]",form).length){alert('Error: Form elements must not have name or id of "submit".');return}var s=$.extend(true,{},$.ajaxSettings,options);s.context=s.context||s;var id="jqFormIO"+(new Date().getTime()),fn="_"+id;window[fn]=function(){var f=$io.data("form-plugin-onload");if(f){f();window[fn]=undefined;try{delete window[fn]}catch(e){}}};var $io=$('