summaryrefslogtreecommitdiff
path: root/modules/gallery/helpers
diff options
context:
space:
mode:
authorTristan Zur <tzur@webserver.ccwn.org>2015-06-10 20:55:53 +0200
committerTristan Zur <tzur@webserver.ccwn.org>2015-06-10 20:55:53 +0200
commit406abd7c4df1ace2cd3e4e17159e8941a2e8c0c4 (patch)
treea324be16021f44f2fd6d55e609f47024e945b1db /modules/gallery/helpers
Initial import
Diffstat (limited to 'modules/gallery/helpers')
-rw-r--r--modules/gallery/helpers/MY_html.php91
-rw-r--r--modules/gallery/helpers/MY_num.php54
-rw-r--r--modules/gallery/helpers/MY_remote.php167
-rw-r--r--modules/gallery/helpers/MY_url.php92
-rw-r--r--modules/gallery/helpers/MY_valid.php26
-rw-r--r--modules/gallery/helpers/access.php758
-rw-r--r--modules/gallery/helpers/ajax.php31
-rw-r--r--modules/gallery/helpers/album.php126
-rw-r--r--modules/gallery/helpers/auth.php134
-rw-r--r--modules/gallery/helpers/batch.php40
-rw-r--r--modules/gallery/helpers/block_manager.php115
-rw-r--r--modules/gallery/helpers/data_rest.php115
-rw-r--r--modules/gallery/helpers/dir.php40
-rw-r--r--modules/gallery/helpers/encoding.php35
-rw-r--r--modules/gallery/helpers/gallery.php233
-rw-r--r--modules/gallery/helpers/gallery_block.php145
-rw-r--r--modules/gallery/helpers/gallery_error.php30
-rw-r--r--modules/gallery/helpers/gallery_event.php621
-rw-r--r--modules/gallery/helpers/gallery_graphics.php183
-rw-r--r--modules/gallery/helpers/gallery_installer.php844
-rw-r--r--modules/gallery/helpers/gallery_rss.php76
-rw-r--r--modules/gallery/helpers/gallery_task.php826
-rw-r--r--modules/gallery/helpers/gallery_theme.php151
-rw-r--r--modules/gallery/helpers/graphics.php546
-rw-r--r--modules/gallery/helpers/identity.php247
-rw-r--r--modules/gallery/helpers/item.php433
-rw-r--r--modules/gallery/helpers/item_rest.php210
-rw-r--r--modules/gallery/helpers/items_rest.php96
-rw-r--r--modules/gallery/helpers/json.php31
-rw-r--r--modules/gallery/helpers/l10n_client.php323
-rw-r--r--modules/gallery/helpers/l10n_scanner.php178
-rw-r--r--modules/gallery/helpers/legal_file.php310
-rw-r--r--modules/gallery/helpers/locales.php264
-rw-r--r--modules/gallery/helpers/log.php108
-rw-r--r--modules/gallery/helpers/message.php109
-rw-r--r--modules/gallery/helpers/model_cache.php42
-rw-r--r--modules/gallery/helpers/module.php594
-rw-r--r--modules/gallery/helpers/movie.php282
-rw-r--r--modules/gallery/helpers/photo.php145
-rw-r--r--modules/gallery/helpers/random.php48
-rw-r--r--modules/gallery/helpers/site_status.php132
-rw-r--r--modules/gallery/helpers/system.php113
-rw-r--r--modules/gallery/helpers/task.php113
-rw-r--r--modules/gallery/helpers/theme.php113
-rw-r--r--modules/gallery/helpers/tree_rest.php92
-rw-r--r--modules/gallery/helpers/upgrade_checker.php105
-rw-r--r--modules/gallery/helpers/user_profile.php55
-rw-r--r--modules/gallery/helpers/xml.php35
48 files changed, 9657 insertions, 0 deletions
diff --git a/modules/gallery/helpers/MY_html.php b/modules/gallery/helpers/MY_html.php
new file mode 100644
index 0000000..767fe3f
--- /dev/null
+++ b/modules/gallery/helpers/MY_html.php
@@ -0,0 +1,91 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class html extends html_Core {
+ /**
+ * Returns a string that is safe to be used in HTML (XSS protection).
+ *
+ * If $html is a string, the returned string will be HTML escaped.
+ * If $html is a SafeString instance, the returned string may contain
+ * unescaped HTML which is assumed to be safe.
+ *
+ * Example:<pre>
+ * <div><?= html::clean($php_var) ?>
+ * </pre>
+ */
+ static function clean($html) {
+ return new SafeString($html);
+ }
+
+ /**
+ * Returns a string that is safe to be used in HTML (XSS protection),
+ * purifying (filtering) the given HTML to ensure that the result contains
+ * only non-malicious HTML.
+ *
+ * Example:<pre>
+ * <div><?= html::purify($item->title) ?>
+ * </pre>
+ */
+ static function purify($html) {
+ return SafeString::purify($html);
+ }
+
+ /**
+ * Flags the given string as safe to be used in HTML (free of malicious HTML/JS).
+ *
+ * Example:<pre>
+ * // Parameters to t() are automatically escaped by default.
+ * // If the parameter is marked as clean, it won't get escaped.
+ * t('Go <a href="%url">there</a>',
+ * array("url" => html::mark_clean(url::current())))
+ * </pre>
+ */
+ static function mark_clean($html) {
+ return SafeString::of_safe_html($html);
+ }
+
+ /**
+ * Escapes the given string for use in JavaScript.
+ *
+ * Example:<pre>
+ * <script type="text/javascript>"
+ * var some_js_string = <?= html::js_string($php_string) ?>;
+ * </script>
+ * </pre>
+ */
+ static function js_string($string) {
+ return SafeString::of($string)->for_js();
+ }
+
+ /**
+ * Returns a string safe for use in HTML element attributes.
+ *
+ * Assumes that the HTML element attribute is already
+ * delimited by single or double quotes
+ *
+ * Example:<pre>
+ * <a title="<?= html::clean_for_attribute($php_var) ?>">;
+ * </script>
+ * </pre>
+ * @return the string escaped for use in HTML attributes.
+ */
+ static function clean_attribute($string) {
+ return html::clean($string)->for_html_attr();
+ }
+}
diff --git a/modules/gallery/helpers/MY_num.php b/modules/gallery/helpers/MY_num.php
new file mode 100644
index 0000000..a550a1a
--- /dev/null
+++ b/modules/gallery/helpers/MY_num.php
@@ -0,0 +1,54 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class num extends num_Core {
+ /**
+ * Convert a size value as accepted by PHP's shorthand to bytes.
+ * ref: http://us2.php.net/manual/en/function.ini-get.php
+ * ref: http://us2.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
+ */
+ static function convert_to_bytes($val) {
+ $val = trim($val);
+ $last = strtolower($val[strlen($val)-1]);
+ switch($last) {
+ case 'g':
+ $val *= 1024;
+ case 'm':
+ $val *= 1024;
+ case 'k':
+ $val *= 1024;
+ }
+
+ return $val;
+ }
+
+ /**
+ * Convert a size value as accepted by PHP's shorthand to bytes.
+ * ref: http://us2.php.net/manual/en/function.ini-get.php
+ * ref: http://us2.php.net/manual/en/faq.using.php#faq.using.shorthandbytes
+ */
+ static function convert_to_human_readable($num) {
+ foreach (array("G" => 1e9, "M" => 1e6, "K" => 1e3) as $k => $v) {
+ if ($num > $v) {
+ $num = round($num / $v) . $k;
+ }
+ }
+ return $num;
+ }
+}
diff --git a/modules/gallery/helpers/MY_remote.php b/modules/gallery/helpers/MY_remote.php
new file mode 100644
index 0000000..59804b9
--- /dev/null
+++ b/modules/gallery/helpers/MY_remote.php
@@ -0,0 +1,167 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class remote extends remote_Core {
+
+ static function post($url, $post_data_array, $extra_headers=array()) {
+ $post_data_raw = self::_encode_post_data($post_data_array, $extra_headers);
+
+ /* Read the web page into a buffer */
+ list ($response_status, $response_headers, $response_body) =
+ remote::do_request($url, 'POST', $extra_headers, $post_data_raw);
+
+ return array($response_body, $response_status, $response_headers);
+ }
+
+ static function success($response_status) {
+ return preg_match("/^HTTP\/\d+\.\d+\s2\d{2}(\s|$)/", trim($response_status));
+ }
+
+ /**
+ * Encode the post data. For each key/value pair, urlencode both the key and the value and then
+ * concatenate together. As per the specification, each key/value pair is separated with an
+ * ampersand (&)
+ * @param array $post_data_array the key/value post data
+ * @param array $extra_headers extra headers to pass to the server
+ * @return string the encoded post data
+ */
+ private static function _encode_post_data($post_data_array, &$extra_headers) {
+ $post_data_raw = '';
+ foreach ($post_data_array as $key => $value) {
+ if (!empty($post_data_raw)) {
+ $post_data_raw .= '&';
+ }
+ $post_data_raw .= urlencode($key) . '=' . urlencode($value);
+ }
+
+ $extra_headers['Content-Type'] = 'application/x-www-form-urlencoded';
+ $extra_headers['Content-Length'] = strlen($post_data_raw);
+
+ return $post_data_raw;
+ }
+
+ /**
+ * A single request, without following redirects
+ *
+ * @todo: Handle redirects? If so, only for GET (i.e. not for POST), and use G2's
+ * WebHelper_simple::_parseLocation logic.
+ */
+ static function do_request($url, $method='GET', $headers=array(), $body='') {
+ if (!array_key_exists("User-Agent", $headers)) {
+ $headers["User-Agent"] = "Gallery3";
+ }
+ /* Convert illegal characters */
+ $url = str_replace(' ', '%20', $url);
+
+ $url_components = self::_parse_url_for_fsockopen($url);
+ $handle = fsockopen(
+ $url_components['fsockhost'], $url_components['port'], $errno, $errstr, 5);
+ if (empty($handle)) {
+ // log "Error $errno: '$errstr' requesting $url";
+ return array(null, null, null);
+ }
+
+ $header_lines = array('Host: ' . $url_components['host']);
+ foreach ($headers as $key => $value) {
+ $header_lines[] = $key . ': ' . $value;
+ }
+
+ $success = fwrite($handle, sprintf("%s %s HTTP/1.0\r\n%s\r\n\r\n%s",
+ $method,
+ $url_components['uri'],
+ implode("\r\n", $header_lines),
+ $body));
+ if (!$success) {
+ // Zero bytes written or false was returned
+ // log "fwrite failed in requestWebPage($url)" . ($success === false ? ' - false' : ''
+ return array(null, null, null);
+ }
+ fflush($handle);
+
+ /*
+ * Read the status line. fgets stops after newlines. The first line is the protocol
+ * version followed by a numeric status code and its associated textual phrase.
+ */
+ $response_status = trim(fgets($handle, 4096));
+ if (empty($response_status)) {
+ // 'Empty http response code, maybe timeout'
+ return array(null, null, null);
+ }
+
+ /* Read the headers */
+ $response_headers = array();
+ while (!feof($handle)) {
+ $line = trim(fgets($handle, 4096));
+ if (empty($line)) {
+ break;
+ }
+
+ /* Normalize the line endings */
+ $line = str_replace("\r", '', $line);
+
+ list ($key, $value) = explode(':', $line, 2);
+ if (isset($response_headers[$key])) {
+ if (!is_array($response_headers[$key])) {
+ $response_headers[$key] = array($response_headers[$key]);
+ }
+ $response_headers[$key][] = trim($value);
+ } else {
+ $response_headers[$key] = trim($value);
+ }
+ }
+
+ /* Read the body */
+ $response_body = '';
+ while (!feof($handle)) {
+ $response_body .= fread($handle, 4096);
+ }
+ fclose($handle);
+
+ return array($response_status, $response_headers, $response_body);
+ }
+
+ /**
+ * Prepare for fsockopen call.
+ * @param string $url
+ * @return array url components
+ * @access private
+ */
+ private static function _parse_url_for_fsockopen($url) {
+ $url_components = parse_url($url);
+ if (strtolower($url_components['scheme']) == 'https') {
+ $url_components['fsockhost'] = 'ssl://' . $url_components['host'];
+ $default_port = 443;
+ } else {
+ $url_components['fsockhost'] = $url_components['host'];
+ $default_port = 80;
+ }
+ if (empty($url_components['port'])) {
+ $url_components['port'] = $default_port;
+ }
+ if (empty($url_components['path'])) {
+ $url_components['path'] = '/';
+ }
+ $uri = $url_components['path']
+ . (empty($url_components['query']) ? '' : '?' . $url_components['query']);
+ /* Unescape ampersands, since if the url comes from form input it will be escaped */
+ $url_components['uri'] = str_replace('&amp;', '&', $uri);
+ return $url_components;
+ }
+}
+
diff --git a/modules/gallery/helpers/MY_url.php b/modules/gallery/helpers/MY_url.php
new file mode 100644
index 0000000..eba08b2
--- /dev/null
+++ b/modules/gallery/helpers/MY_url.php
@@ -0,0 +1,92 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class url extends url_Core {
+ static function parse_url() {
+ if (Router::$controller) {
+ return;
+ }
+
+ // Work around problems with the CGI sapi by enforcing our default path
+ if ($_SERVER["SCRIPT_NAME"] && "/" . Router::$current_uri == $_SERVER["SCRIPT_NAME"]) {
+ Router::$controller_path = MODPATH . "gallery/controllers/albums.php";
+ Router::$controller = "albums";
+ Router::$method = 1;
+ return;
+ }
+
+ $item = item::find_by_relative_url(html_entity_decode(Router::$current_uri, ENT_QUOTES));
+ if ($item && $item->loaded()) {
+ Router::$controller = "{$item->type}s";
+ Router::$controller_path = MODPATH . "gallery/controllers/{$item->type}s.php";
+ Router::$method = "show";
+ Router::$arguments = array($item);
+ }
+ }
+
+ /**
+ * Just like url::file() except that it returns an absolute URI
+ */
+ static function abs_file($path) {
+ return url::base(false, request::protocol()) . $path;
+ }
+
+ /**
+ * Just like url::site() except that it returns an absolute URI and
+ * doesn't take a protocol parameter.
+ */
+ static function abs_site($path) {
+ return url::site($path, request::protocol());
+ }
+
+ /**
+ * Just like url::current except that it returns an absolute URI
+ */
+ static function abs_current($qs=false) {
+ return self::abs_site(url::current($qs));
+ }
+
+ /**
+ * Just like url::merge except that it escapes any XSS in the path.
+ */
+ static function merge(array $arguments) {
+ return htmlspecialchars(parent::merge($arguments));
+ }
+
+ /**
+ * Just like url::current except that it escapes any XSS in the path.
+ */
+ static function current($qs=false, $suffix=false) {
+ return htmlspecialchars(parent::current($qs, $suffix));
+ }
+
+ /**
+ * Merge extra an query string onto a given url safely.
+ * @param string the original url
+ * @param array the query string data in key=value form
+ */
+ static function merge_querystring($url, $query_params) {
+ $qs = implode("&", $query_params);
+ if (strpos($url, "?") === false) {
+ return $url . "?$qs";
+ } else {
+ return $url . "&$qs";
+ }
+ }
+}
diff --git a/modules/gallery/helpers/MY_valid.php b/modules/gallery/helpers/MY_valid.php
new file mode 100644
index 0000000..f1dd9c3
--- /dev/null
+++ b/modules/gallery/helpers/MY_valid.php
@@ -0,0 +1,26 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class valid extends valid_Core {
+ static function url($url) {
+ return valid_Core::url($url) &&
+ (!strncasecmp($url, "http://", strlen("http://")) ||
+ !strncasecmp($url, "https://", strlen("https://")));
+ }
+}
diff --git a/modules/gallery/helpers/access.php b/modules/gallery/helpers/access.php
new file mode 100644
index 0000000..a7dca57
--- /dev/null
+++ b/modules/gallery/helpers/access.php
@@ -0,0 +1,758 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+/**
+ * API for Gallery Access control.
+ *
+ * Permissions are hierarchical, and apply only to groups and albums. They cascade down from the
+ * top of the Gallery to the bottom, so if you set a permission in the root album, that permission
+ * applies for any sub-album unless the sub-album overrides it. Likewise, any permission applied
+ * to an album applies to any photos inside the album. Overrides can be applied at any level of
+ * the hierarchy for any permission other than View permissions.
+ *
+ * View permissions are an exceptional case. In the case of viewability, we want to ensure that
+ * if an album's parent is inaccessible, then this album must be inaccessible also. So while view
+ * permissions cascade downwards and you're free to set the ALLOW permission on any album, that
+ * ALLOW permission will be ignored unless all that album's parents are marked ALLOW also.
+ *
+ * Implementatation Notes:
+ *
+ * Notes refer to this example album hierarchy:
+ * A1
+ * / \
+ * A2 A3
+ * / \
+ * A4 A5
+ *
+ * o We have the concept of "intents". A user can specify that he intends for A3 to be
+ * inaccessible (ie: a DENY on the "view" permission to the EVERYBODY group). Once A3 is
+ * inaccessible, A5 can never be displayed to that group. If A1 is made inaccessible, then the
+ * entire tree is hidden. If subsequently A1 is made accessible, then the whole tree is
+ * available again *except* A3 and below since the user's "intent" for A3 is maintained.
+ *
+ * o Intents are specified as <group_id, perm, item_id> tuples. It would be inefficient to check
+ * these tuples every time we want to do a lookup, so we use these intents to create an entire
+ * table of permissions for easy lookup in the Access_Cache_Model. There's a 1:1 mapping
+ * between Item_Model and Access_Cache_Model entries.
+ *
+ * o For efficiency, we create columns in Access_Intent_Model and Access_Cache_Model for each of
+ * the possible Group_Model and Permission_Model combinations. This may lead to performance
+ * issues for very large Gallery installs, but for small to medium sized ones (5-10 groups, 5-10
+ * permissions) it's especially efficient because there's a single field value for each
+ * group/permission/item combination.
+ *
+ * o For efficiency, we store the cache columns for view permissions directly in the Item_Model.
+ * This means that we can filter items by group/permission combination without doing any table
+ * joins making for an especially efficient permission check at the expense of having to
+ * maintain extra columns for each item.
+ *
+ * o If at any time the Access_Cache_Model becomes invalid, we can rebuild the entire table from
+ * the Access_Intent_Model
+ */
+class access_Core {
+ const DENY = "0";
+ const ALLOW = "1";
+ const INHERIT = null; // access_intent
+ const UNKNOWN = null; // cache (access_cache, items)
+
+ /**
+ * Does the active user have this permission on this item?
+ *
+ * @param string $perm_name
+ * @param Item_Model $item
+ * @return boolean
+ */
+ static function can($perm_name, $item) {
+ return access::user_can(identity::active_user(), $perm_name, $item);
+ }
+
+ /**
+ * Does the user have this permission on this item?
+ *
+ * @param User_Model $user
+ * @param string $perm_name
+ * @param Item_Model $item
+ * @return boolean
+ */
+ static function user_can($user, $perm_name, $item) {
+ if (!$item->loaded()) {
+ return false;
+ }
+
+ if ($user->admin) {
+ return true;
+ }
+
+ // Use the nearest parent album (including the current item) so that we take advantage
+ // of the cache when checking many items in a single album.
+ $id = ($item->type == "album") ? $item->id : $item->parent_id;
+ $resource = $perm_name == "view" ?
+ $item : model_cache::get("access_cache", $id, "item_id");
+
+ foreach ($user->groups() as $group) {
+ if ($resource->__get("{$perm_name}_{$group->id}") === access::ALLOW) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * If the active user does not have this permission, failed with an access::forbidden().
+ *
+ * @param string $perm_name
+ * @param Item_Model $item
+ * @return boolean
+ */
+ static function required($perm_name, $item) {
+ if (!access::can($perm_name, $item)) {
+ if ($perm_name == "view") {
+ // Treat as if the item didn't exist, don't leak any information.
+ throw new Kohana_404_Exception();
+ } else {
+ access::forbidden();
+ }
+ }
+ }
+
+ /**
+ * Does this group have this permission on this item?
+ *
+ * @param Group_Model $group
+ * @param string $perm_name
+ * @param Item_Model $item
+ * @return boolean
+ */
+ static function group_can($group, $perm_name, $item) {
+ // Use the nearest parent album (including the current item) so that we take advantage
+ // of the cache when checking many items in a single album.
+ $id = ($item->type == "album") ? $item->id : $item->parent_id;
+ $resource = $perm_name == "view" ?
+ $item : model_cache::get("access_cache", $id, "item_id");
+
+ return $resource->__get("{$perm_name}_{$group->id}") === access::ALLOW;
+ }
+
+ /**
+ * Return this group's intent for this permission on this item.
+ *
+ * @param Group_Model $group
+ * @param string $perm_name
+ * @param Item_Model $item
+ * @return boolean access::ALLOW, access::DENY or access::INHERIT (null) for no intent
+ */
+ static function group_intent($group, $perm_name, $item) {
+ $intent = model_cache::get("access_intent", $item->id, "item_id");
+ return $intent->__get("{$perm_name}_{$group->id}");
+ }
+
+ /**
+ * Is the permission on this item locked by a parent? If so return the nearest parent that
+ * locks it.
+ *
+ * @param Group_Model $group
+ * @param string $perm_name
+ * @param Item_Model $item
+ * @return ORM_Model item that locks this one
+ */
+ static function locked_by($group, $perm_name, $item) {
+ if ($perm_name != "view") {
+ return null;
+ }
+
+ // For view permissions, if any parent is access::DENY, then those parents lock this one.
+ // Return
+ $lock = ORM::factory("item")
+ ->where("left_ptr", "<=", $item->left_ptr)
+ ->where("right_ptr", ">=", $item->right_ptr)
+ ->where("items.id", "<>", $item->id)
+ ->join("access_intents", "items.id", "access_intents.item_id")
+ ->where("access_intents.view_$group->id", "=", access::DENY)
+ ->order_by("level", "DESC")
+ ->limit(1)
+ ->find();
+
+ if ($lock->loaded()) {
+ return $lock;
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Terminate immediately with an HTTP 403 Forbidden response.
+ */
+ static function forbidden() {
+ throw new Kohana_Exception("@todo FORBIDDEN", null, 403);
+ }
+
+ /**
+ * Internal method to set a permission
+ *
+ * @param Group_Model $group
+ * @param string $perm_name
+ * @param Item_Model $item
+ * @param boolean $value
+ */
+ private static function _set(Group_Definition $group, $perm_name, $album, $value) {
+ if (!($group instanceof Group_Definition)) {
+ throw new Exception("@todo PERMISSIONS_ONLY_WORK_ON_GROUPS");
+ }
+ if (!$album->loaded()) {
+ throw new Exception("@todo INVALID_ALBUM $album->id");
+ }
+ if (!$album->is_album()) {
+ throw new Exception("@todo INVALID_ALBUM_TYPE not an album");
+ }
+ $access = model_cache::get("access_intent", $album->id, "item_id");
+ $access->__set("{$perm_name}_{$group->id}", $value);
+ $access->save();
+
+ if ($perm_name == "view") {
+ self::_update_access_view_cache($group, $album);
+ } else {
+ self::_update_access_non_view_cache($group, $perm_name, $album);
+ }
+
+ access::update_htaccess_files($album, $group, $perm_name, $value);
+ model_cache::clear();
+ }
+
+ /**
+ * Allow a group to have a permission on an item.
+ *
+ * @param Group_Model $group
+ * @param string $perm_name
+ * @param Item_Model $item
+ */
+ static function allow($group, $perm_name, $item) {
+ self::_set($group, $perm_name, $item, self::ALLOW);
+ }
+
+ /**
+ * Deny a group the given permission on an item.
+ *
+ * @param Group_Model $group
+ * @param string $perm_name
+ * @param Item_Model $item
+ */
+ static function deny($group, $perm_name, $item) {
+ self::_set($group, $perm_name, $item, self::DENY);
+ }
+
+ /**
+ * Unset the given permission for this item and use inherited values
+ *
+ * @param Group_Model $group
+ * @param string $perm_name
+ * @param Item_Model $item
+ */
+ static function reset($group, $perm_name, $item) {
+ if ($item->id == 1) {
+ throw new Exception("@todo CANT_RESET_ROOT_PERMISSION");
+ }
+ self::_set($group, $perm_name, $item, self::INHERIT);
+ }
+
+ /**
+ * Recalculate the permissions for an album's hierarchy.
+ */
+ static function recalculate_album_permissions($album) {
+ foreach (self::_get_all_groups() as $group) {
+ foreach (ORM::factory("permission")->find_all() as $perm) {
+ if ($perm->name == "view") {
+ self::_update_access_view_cache($group, $album);
+ } else {
+ self::_update_access_non_view_cache($group, $perm->name, $album);
+ }
+ }
+ }
+ model_cache::clear();
+ }
+
+ /**
+ * Recalculate the permissions for a single photo.
+ */
+ static function recalculate_photo_permissions($photo) {
+ $parent = $photo->parent();
+ $parent_access_cache = ORM::factory("access_cache")->where("item_id", "=", $parent->id)->find();
+ $photo_access_cache = ORM::factory("access_cache")->where("item_id", "=", $photo->id)->find();
+ foreach (self::_get_all_groups() as $group) {
+ foreach (ORM::factory("permission")->find_all() as $perm) {
+ $field = "{$perm->name}_{$group->id}";
+ if ($perm->name == "view") {
+ $photo->$field = $parent->$field;
+ } else {
+ $photo_access_cache->$field = $parent_access_cache->$field;
+ }
+ }
+ }
+ $photo_access_cache->save();
+ $photo->save();
+ model_cache::clear();
+ }
+
+ /**
+ * Register a permission so that modules can use it.
+ *
+ * @param string $name The internal name for for this permission
+ * @param string $display_name The internationalized version of the displayable name
+ * @return void
+ */
+ static function register_permission($name, $display_name) {
+ $permission = ORM::factory("permission", $name);
+ if ($permission->loaded()) {
+ throw new Exception("@todo PERMISSION_ALREADY_EXISTS $name");
+ }
+ $permission->name = $name;
+ $permission->display_name = $display_name;
+ $permission->save();
+
+ foreach (self::_get_all_groups() as $group) {
+ self::_add_columns($name, $group);
+ }
+ }
+
+ /**
+ * Delete a permission.
+ *
+ * @param string $perm_name
+ * @return void
+ */
+ static function delete_permission($name) {
+ foreach (self::_get_all_groups() as $group) {
+ self::_drop_columns($name, $group);
+ }
+ $permission = ORM::factory("permission")->where("name", "=", $name)->find();
+ if ($permission->loaded()) {
+ $permission->delete();
+ }
+ }
+
+ /**
+ * Add the appropriate columns for a new group
+ *
+ * @param Group_Model $group
+ * @return void
+ */
+ static function add_group($group) {
+ foreach (ORM::factory("permission")->find_all() as $perm) {
+ self::_add_columns($perm->name, $group);
+ }
+ }
+
+ /**
+ * Remove a group's permission columns (usually when it's deleted)
+ *
+ * @param Group_Model $group
+ * @return void
+ */
+ static function delete_group($group) {
+ foreach (ORM::factory("permission")->find_all() as $perm) {
+ self::_drop_columns($perm->name, $group);
+ }
+ }
+
+ /**
+ * Add new access rows when a new item is added.
+ *
+ * @param Item_Model $item
+ * @return void
+ */
+ static function add_item($item) {
+ $access_intent = ORM::factory("access_intent", $item->id);
+ if ($access_intent->loaded()) {
+ throw new Exception("@todo ITEM_ALREADY_ADDED $item->id");
+ }
+ $access_intent = ORM::factory("access_intent");
+ $access_intent->item_id = $item->id;
+ $access_intent->save();
+
+ // Create a new access cache entry and copy the parents values.
+ $access_cache = ORM::factory("access_cache");
+ $access_cache->item_id = $item->id;
+ if ($item->id != 1) {
+ $parent_access_cache =
+ ORM::factory("access_cache")->where("item_id", "=", $item->parent()->id)->find();
+ foreach (self::_get_all_groups() as $group) {
+ foreach (ORM::factory("permission")->find_all() as $perm) {
+ $field = "{$perm->name}_{$group->id}";
+ if ($perm->name == "view") {
+ $item->$field = $item->parent()->$field;
+ } else {
+ $access_cache->$field = $parent_access_cache->$field;
+ }
+ }
+ }
+ }
+ $item->save();
+ $access_cache->save();
+ }
+
+ /**
+ * Delete appropriate access rows when an item is deleted.
+ *
+ * @param Item_Model $item
+ * @return void
+ */
+ static function delete_item($item) {
+ ORM::factory("access_intent")->where("item_id", "=", $item->id)->find()->delete();
+ ORM::factory("access_cache")->where("item_id", "=", $item->id)->find()->delete();
+ }
+
+ /**
+ * Verify our Cross Site Request Forgery token is valid, else throw an exception.
+ */
+ static function verify_csrf() {
+ $input = Input::instance();
+ if ($input->post("csrf", $input->get("csrf", null)) !== Session::instance()->get("csrf")) {
+ access::forbidden();
+ }
+ }
+
+ /**
+ * Get the Cross Site Request Forgery token for this session.
+ * @return string
+ */
+ static function csrf_token() {
+ $session = Session::instance();
+ $csrf = $session->get("csrf");
+ if (empty($csrf)) {
+ $csrf = random::hash();
+ $session->set("csrf", $csrf);
+ }
+ return $csrf;
+ }
+
+ /**
+ * Generate an <input> element containing the Cross Site Request Forgery token for this session.
+ * @return string
+ */
+ static function csrf_form_field() {
+ return "<input type=\"hidden\" name=\"csrf\" value=\"" . access::csrf_token() . "\"/>";
+ }
+
+ /**
+ * Internal method to get all available groups.
+ *
+ * @return ORM_Iterator
+ */
+ private static function _get_all_groups() {
+ // When we build the gallery package, it's possible that there is no identity provider
+ // installed yet. This is ok at packaging time, so work around it.
+ if (module::is_active(module::get_var("gallery", "identity_provider", "user"))) {
+ return identity::groups();
+ } else {
+ return array();
+ }
+ }
+
+ /**
+ * Internal method to remove Permission/Group columns
+ *
+ * @param Group_Model $group
+ * @param string $perm_name
+ * @return void
+ */
+ private static function _drop_columns($perm_name, $group) {
+ $field = "{$perm_name}_{$group->id}";
+ $cache_table = $perm_name == "view" ? "items" : "access_caches";
+ Database::instance()->query("ALTER TABLE {{$cache_table}} DROP `$field`");
+ Database::instance()->query("ALTER TABLE {access_intents} DROP `$field`");
+ model_cache::clear();
+ ORM::factory("access_intent")->clear_cache();
+ }
+
+ /**
+ * Internal method to add Permission/Group columns
+ *
+ * @param Group_Model $group
+ * @param string $perm_name
+ * @return void
+ */
+ private static function _add_columns($perm_name, $group) {
+ $field = "{$perm_name}_{$group->id}";
+ $cache_table = $perm_name == "view" ? "items" : "access_caches";
+ $not_null = $cache_table == "items" ? "" : "NOT NULL";
+ Database::instance()->query(
+ "ALTER TABLE {{$cache_table}} ADD `$field` BINARY $not_null DEFAULT FALSE");
+ Database::instance()->query(
+ "ALTER TABLE {access_intents} ADD `$field` BINARY DEFAULT NULL");
+ db::build()
+ ->update("access_intents")
+ ->set($field, access::DENY)
+ ->where("item_id", "=", 1)
+ ->execute();
+ model_cache::clear();
+ ORM::factory("access_intent")->clear_cache();
+ }
+
+ /**
+ * Update the Access_Cache model based on information from the Access_Intent model for view
+ * permissions only.
+ *
+ * @todo: use database locking
+ *
+ * @param Group_Model $group
+ * @param Item_Model $item
+ * @return void
+ */
+ private static function _update_access_view_cache($group, $item) {
+ $access = ORM::factory("access_intent")->where("item_id", "=", $item->id)->find();
+ $field = "view_{$group->id}";
+
+ // With view permissions, deny values in the parent can override allow values in the child,
+ // so start from the bottom of the tree and work upwards overlaying negative on top of
+ // positive.
+ //
+ // If the item's intent is ALLOW or DEFAULT, it's possible that some ancestor has specified
+ // DENY and this ALLOW cannot be obeyed. So in that case, back up the tree and find any
+ // non-DEFAULT and non-ALLOW parent and propagate from there. If we can't find a matching
+ // item, then its safe to propagate from here.
+ if ($access->$field !== access::DENY) {
+ $tmp_item = ORM::factory("item")
+ ->where("left_ptr", "<", $item->left_ptr)
+ ->where("right_ptr", ">", $item->right_ptr)
+ ->join("access_intents", "access_intents.item_id", "items.id")
+ ->where("access_intents.$field", "=", access::DENY)
+ ->order_by("left_ptr", "DESC")
+ ->limit(1)
+ ->find();
+ if ($tmp_item->loaded()) {
+ $item = $tmp_item;
+ }
+ }
+
+ // We will have a problem if we're trying to change a DENY to an ALLOW because the
+ // access_caches table will already contain DENY values and we won't be able to overwrite
+ // them according the rule above. So mark every permission below this level as UNKNOWN so
+ // that we can tell which permissions have been changed, and which ones need to be updated.
+ db::build()
+ ->update("items")
+ ->set($field, access::UNKNOWN)
+ ->where("left_ptr", ">=", $item->left_ptr)
+ ->where("right_ptr", "<=", $item->right_ptr)
+ ->execute();
+
+ $query = ORM::factory("access_intent")
+ ->select(array("access_intents.$field", "items.left_ptr", "items.right_ptr", "items.id"))
+ ->join("items", "items.id", "access_intents.item_id")
+ ->where("left_ptr", ">=", $item->left_ptr)
+ ->where("right_ptr", "<=", $item->right_ptr)
+ ->where("type", "=", "album")
+ ->where("access_intents.$field", "IS NOT", access::INHERIT)
+ ->order_by("level", "DESC")
+ ->find_all();
+ foreach ($query as $row) {
+ if ($row->$field == access::ALLOW) {
+ // Propagate ALLOW for any row that is still UNKNOWN.
+ db::build()
+ ->update("items")
+ ->set($field, $row->$field)
+ ->where($field, "IS", access::UNKNOWN) // UNKNOWN is NULL so we have to use IS
+ ->where("left_ptr", ">=", $row->left_ptr)
+ ->where("right_ptr", "<=", $row->right_ptr)
+ ->execute();
+ } else if ($row->$field == access::DENY) {
+ // DENY overwrites everything below it
+ db::build()
+ ->update("items")
+ ->set($field, $row->$field)
+ ->where("left_ptr", ">=", $row->left_ptr)
+ ->where("right_ptr", "<=", $row->right_ptr)
+ ->execute();
+ }
+ }
+
+ // Finally, if our intent is DEFAULT at this point it means that we were unable to find a
+ // DENY parent in the hierarchy to propagate from. So we'll still have a UNKNOWN values in
+ // the hierarchy, and all of those are safe to change to ALLOW.
+ db::build()
+ ->update("items")
+ ->set($field, access::ALLOW)
+ ->where($field, "IS", access::UNKNOWN) // UNKNOWN is NULL so we have to use IS
+ ->where("left_ptr", ">=", $item->left_ptr)
+ ->where("right_ptr", "<=", $item->right_ptr)
+ ->execute();
+ }
+
+ /**
+ * Update the Access_Cache model based on information from the Access_Intent model for non-view
+ * permissions.
+ *
+ * @todo: use database locking
+ *
+ * @param Group_Model $group
+ * @param string $perm_name
+ * @param Item_Model $item
+ * @return void
+ */
+ private static function _update_access_non_view_cache($group, $perm_name, $item) {
+ $access = ORM::factory("access_intent")->where("item_id", "=", $item->id)->find();
+
+ $field = "{$perm_name}_{$group->id}";
+
+ // If the item's intent is DEFAULT, then we need to back up the chain to find the nearest
+ // parent with an intent and propagate from there.
+ //
+ // @todo To optimize this, we wouldn't need to propagate from the parent, we could just
+ // propagate from here with the parent's intent.
+ if ($access->$field === access::INHERIT) {
+ $tmp_item = ORM::factory("item")
+ ->join("access_intents", "items.id", "access_intents.item_id")
+ ->where("left_ptr", "<", $item->left_ptr)
+ ->where("right_ptr", ">", $item->right_ptr)
+ ->where($field, "IS NOT", access::UNKNOWN) // UNKNOWN is NULL so we have to use IS NOT
+ ->order_by("left_ptr", "DESC")
+ ->limit(1)
+ ->find();
+ if ($tmp_item->loaded()) {
+ $item = $tmp_item;
+ }
+ }
+
+ // With non-view permissions, each level can override any permissions that came above it
+ // so start at the top and work downwards, overlaying permissions as we go.
+ $query = ORM::factory("access_intent")
+ ->select(array("access_intents.$field", "items.left_ptr", "items.right_ptr"))
+ ->join("items", "items.id", "access_intents.item_id")
+ ->where("left_ptr", ">=", $item->left_ptr)
+ ->where("right_ptr", "<=", $item->right_ptr)
+ ->where($field, "IS NOT", access::INHERIT)
+ ->order_by("level", "ASC")
+ ->find_all();
+ foreach ($query as $row) {
+ $value = ($row->$field === access::ALLOW) ? true : false;
+ db::build()
+ ->update("access_caches")
+ ->set($field, $value)
+ ->where("item_id", "IN",
+ db::build()
+ ->select("id")
+ ->from("items")
+ ->where("left_ptr", ">=", $row->left_ptr)
+ ->where("right_ptr", "<=", $row->right_ptr))
+ ->execute();
+ }
+ }
+
+ /**
+ * Rebuild the .htaccess files that prevent direct access to albums, resizes and thumbnails. We
+ * call this internally any time we change the view or view_full permissions for guest users.
+ * This function is only public because we use it in maintenance tasks.
+ *
+ * @param Item_Model the album
+ * @param Group_Model the group whose permission is changing
+ * @param string the permission name
+ * @param string the new permission value (eg access::DENY)
+ */
+ static function update_htaccess_files($album, $group, $perm_name, $value) {
+ if ($group->id != identity::everybody()->id ||
+ !($perm_name == "view" || $perm_name == "view_full")) {
+ return;
+ }
+
+ $dirs = array($album->file_path());
+ if ($perm_name == "view") {
+ $dirs[] = dirname($album->resize_path());
+ $dirs[] = dirname($album->thumb_path());
+ }
+
+ $base_url = url::base(true);
+ $sep = "?";
+ if (strpos($base_url, "?") !== false) {
+ $sep = "&";
+ }
+ $base_url .= $sep . "kohana_uri=/file_proxy";
+ // Replace "/index.php/?kohana..." with "/index.php?koahan..."
+ // Doesn't apply to "/?kohana..." or "/foo/?kohana..."
+ // Can't check for "index.php" since the file might be renamed, and
+ // there might be more Apache aliases / rewrites at work.
+ $url_path = parse_url($base_url, PHP_URL_PATH);
+ // Does the URL path have a file component?
+ if (preg_match("#[^/]+\.php#i", $url_path)) {
+ $base_url = str_replace("/?", "?", $base_url);
+ }
+
+ foreach ($dirs as $dir) {
+ if ($value === access::DENY) {
+ $fp = fopen("$dir/.htaccess", "w+");
+ fwrite($fp, "<IfModule mod_rewrite.c>\n");
+ fwrite($fp, " RewriteEngine On\n");
+ fwrite($fp, " RewriteRule (.*) $base_url/\$1 [L]\n");
+ fwrite($fp, "</IfModule>\n");
+ fwrite($fp, "<IfModule !mod_rewrite.c>\n");
+ fwrite($fp, " Order Deny,Allow\n");
+ fwrite($fp, " Deny from All\n");
+ fwrite($fp, "</IfModule>\n");
+ fclose($fp);
+ } else {
+ @unlink($dir . "/.htaccess");
+ }
+ }
+ }
+
+ static function private_key() {
+ return module::get_var("gallery", "private_key");
+ }
+
+ /**
+ * Verify that our htaccess based permission system actually works. Create a temporary
+ * directory containing an .htaccess file that uses mod_rewrite to redirect /verify to
+ * /success. Then request that url. If we retrieve it successfully, then our redirects are
+ * working and our permission system works.
+ */
+ static function htaccess_works() {
+ $success_url = url::file("var/security_test/success");
+
+ @mkdir(VARPATH . "security_test");
+ try {
+ if ($fp = @fopen(VARPATH . "security_test/.htaccess", "w+")) {
+ fwrite($fp, "Options +FollowSymLinks\n");
+ fwrite($fp, "RewriteEngine On\n");
+ fwrite($fp, "RewriteRule verify $success_url [L]\n");
+ fclose($fp);
+ }
+
+ if ($fp = @fopen(VARPATH . "security_test/success", "w+")) {
+ fwrite($fp, "success");
+ fclose($fp);
+ }
+
+ // Proxy our authorization headers so that if the entire Gallery is covered by Basic Auth
+ // this callback will still work.
+ $headers = array();
+ if (function_exists("apache_request_headers")) {
+ $arh = apache_request_headers();
+ if (!empty($arh["Authorization"])) {
+ $headers["Authorization"] = $arh["Authorization"];
+ }
+ }
+ list ($status, $headers, $body) =
+ remote::do_request(url::abs_file("var/security_test/verify"), "GET", $headers);
+ $works = ($status == "HTTP/1.1 200 OK") && ($body == "success");
+ } catch (Exception $e) {
+ @dir::unlink(VARPATH . "security_test");
+ throw $e;
+ }
+ @dir::unlink(VARPATH . "security_test");
+
+ return $works;
+ }
+}
diff --git a/modules/gallery/helpers/ajax.php b/modules/gallery/helpers/ajax.php
new file mode 100644
index 0000000..0c69fe7
--- /dev/null
+++ b/modules/gallery/helpers/ajax.php
@@ -0,0 +1,31 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class ajax_Core {
+ /**
+ * Encode an Ajax response so that it's UTF-7 safe.
+ *
+ * @param string $message string to print
+ */
+ static function response($content) {
+ header("Content-Type: text/plain; charset=" . Kohana::CHARSET);
+ print "<meta http-equiv=\"content-type\" content=\"text/html; charset=utf-8\">\n";
+ print $content;
+ }
+}
diff --git a/modules/gallery/helpers/album.php b/modules/gallery/helpers/album.php
new file mode 100644
index 0000000..23aed8a
--- /dev/null
+++ b/modules/gallery/helpers/album.php
@@ -0,0 +1,126 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * This is the API for handling albums.
+ *
+ * Note: by design, this class does not do any permission checking.
+ */
+class album_Core {
+
+ static function get_add_form($parent) {
+ $form = new Forge("albums/create/{$parent->id}", "", "post", array("id" => "g-add-album-form"));
+ $group = $form->group("add_album")
+ ->label(t("Add an album to %album_title", array("album_title" => $parent->title)));
+ $group->input("title")->label(t("Title"))
+ ->error_messages("required", t("You must provide a title"))
+ ->error_messages("length", t("Your title is too long"));
+ $group->textarea("description")->label(t("Description"));
+ $group->input("name")->label(t("Directory name"))
+ ->error_messages("no_slashes", t("The directory name can't contain the \"/\" character"))
+ ->error_messages("required", t("You must provide a directory name"))
+ ->error_messages("length", t("Your directory name is too long"))
+ ->error_messages("conflict", t("There is already a movie, photo or album with this name"));
+ $group->input("slug")->label(t("Internet Address"))
+ ->error_messages(
+ "reserved", t("This address is reserved and can't be used."))
+ ->error_messages(
+ "not_url_safe",
+ t("The internet address should contain only letters, numbers, hyphens and underscores"))
+ ->error_messages("required", t("You must provide an internet address"))
+ ->error_messages("length", t("Your internet address is too long"));
+ $group->hidden("type")->value("album");
+
+ module::event("album_add_form", $parent, $form);
+
+ $group->submit("")->value(t("Create"));
+ $form->script("")
+ ->url(url::abs_file("modules/gallery/js/albums_form_add.js"));
+
+ return $form;
+ }
+
+ static function get_edit_form($parent) {
+ $form = new Forge(
+ "albums/update/{$parent->id}", "", "post", array("id" => "g-edit-album-form"));
+ $form->hidden("from_id")->value($parent->id);
+ $group = $form->group("edit_item")->label(t("Edit Album"));
+
+ $group->input("title")->label(t("Title"))->value($parent->title)
+ ->error_messages("required", t("You must provide a title"))
+ ->error_messages("length", t("Your title is too long"));
+ $group->textarea("description")->label(t("Description"))->value($parent->description);
+ if ($parent->id != 1) {
+ $group->input("name")->label(t("Directory Name"))->value($parent->name)
+ ->error_messages("conflict", t("There is already a movie, photo or album with this name"))
+ ->error_messages("no_slashes", t("The directory name can't contain a \"/\""))
+ ->error_messages("no_trailing_period", t("The directory name can't end in \".\""))
+ ->error_messages("required", t("You must provide a directory name"))
+ ->error_messages("length", t("Your directory name is too long"));
+ $group->input("slug")->label(t("Internet Address"))->value($parent->slug)
+ ->error_messages(
+ "conflict", t("There is already a movie, photo or album with this internet address"))
+ ->error_messages(
+ "reserved", t("This address is reserved and can't be used."))
+ ->error_messages(
+ "not_url_safe",
+ t("The internet address should contain only letters, numbers, hyphens and underscores"))
+ ->error_messages("required", t("You must provide an internet address"))
+ ->error_messages("length", t("Your internet address is too long"));
+ } else {
+ $group->hidden("name")->value($parent->name);
+ $group->hidden("slug")->value($parent->slug);
+ }
+
+ $sort_order = $group->group("sort_order", array("id" => "g-album-sort-order"))
+ ->label(t("Sort Order"));
+
+ $sort_order->dropdown("column", array("id" => "g-album-sort-column"))
+ ->label(t("Sort by"))
+ ->options(album::get_sort_order_options())
+ ->selected($parent->sort_column);
+ $sort_order->dropdown("direction", array("id" => "g-album-sort-direction"))
+ ->label(t("Order"))
+ ->options(array("ASC" => t("Ascending"),
+ "DESC" => t("Descending")))
+ ->selected($parent->sort_order);
+
+ module::event("item_edit_form", $parent, $form);
+
+ $group = $form->group("buttons")->label("");
+ $group->hidden("type")->value("album");
+ $group->submit("")->value(t("Modify"));
+ return $form;
+ }
+
+ /**
+ * Return a structured set of all the possible sort orders.
+ */
+ static function get_sort_order_options() {
+ return array("weight" => t("Manual"),
+ "captured" => t("Date captured"),
+ "created" => t("Date uploaded"),
+ "title" => t("Title"),
+ "name" => t("File name"),
+ "updated" => t("Date modified"),
+ "view_count" => t("Number of views"),
+ "rand_key" => t("Random"));
+ }
+}
diff --git a/modules/gallery/helpers/auth.php b/modules/gallery/helpers/auth.php
new file mode 100644
index 0000000..2eb3c25
--- /dev/null
+++ b/modules/gallery/helpers/auth.php
@@ -0,0 +1,134 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class auth_Core {
+ static function get_login_form($url) {
+ $form = new Forge($url, "", "post", array("id" => "g-login-form"));
+ $form->set_attr("class", "g-narrow");
+ $form->hidden("continue_url")->value(Session::instance()->get("continue_url"));
+ $group = $form->group("login")->label(t("Login"));
+ $group->input("name")->label(t("Username"))->id("g-username")->class(null)
+ ->callback("auth::validate_too_many_failed_logins")
+ ->error_messages(
+ "too_many_failed_logins", t("Too many failed login attempts. Try again later"));
+ $group->password("password")->label(t("Password"))->id("g-password")->class(null);
+ $group->inputs["name"]->error_messages("invalid_login", t("Invalid name or password"));
+ $group->submit("")->value(t("Login"));
+ return $form;
+ }
+
+ static function login($user) {
+ identity::set_active_user($user);
+ if (identity::is_writable()) {
+ $user->login_count += 1;
+ $user->last_login = time();
+ $user->save();
+ }
+ log::info("user", t("User %name logged in", array("name" => $user->name)));
+ module::event("user_login", $user);
+ }
+
+ static function logout() {
+ $user = identity::active_user();
+ if (!$user->guest) {
+ try {
+ Session::instance()->destroy();
+ } catch (Exception $e) {
+ Kohana_Log::add("error", $e);
+ }
+ module::event("user_logout", $user);
+ }
+ log::info("user", t("User %name logged out", array("name" => $user->name)),
+ t('<a href="%url">%user_name</a>',
+ array("url" => user_profile::url($user->id),
+ "user_name" => html::clean($user->name))));
+ }
+
+ /**
+ * After there have been 5 failed auth attempts, any failure leads to getting locked out for a
+ * minute.
+ */
+ static function too_many_failures($name) {
+ $failed = ORM::factory("failed_auth")
+ ->where("name", "=", $name)
+ ->find();
+ return ($failed->loaded() &&
+ $failed->count > 5 &&
+ (time() - $failed->time < 60));
+ }
+
+ static function validate_too_many_failed_logins($name_input) {
+ if (auth::too_many_failures($name_input->value)) {
+ $name_input->add_error("too_many_failed_logins", 1);
+ }
+ }
+
+ static function validate_too_many_failed_auth_attempts($form_input) {
+ if (auth::too_many_failures(identity::active_user()->name)) {
+ $form_input->add_error("too_many_failed_auth_attempts", 1);
+ }
+ }
+
+ /**
+ * Record a failed authentication for this user
+ */
+ static function record_failed_attempt($name) {
+ $failed = ORM::factory("failed_auth")
+ ->where("name", "=", $name)
+ ->find();
+ if (!$failed->loaded()) {
+ $failed->name = $name;
+ }
+ $failed->time = time();
+ $failed->count++;
+ $failed->save();
+ }
+
+ /**
+ * Clear any failed logins for this user
+ */
+ static function clear_failed_attempts($user) {
+ ORM::factory("failed_auth")
+ ->where("name", "=", $user->name)
+ ->delete_all();
+ }
+
+ /**
+ * Checks whether the current user (= admin) must
+ * actively re-authenticate before access is given
+ * to the admin area.
+ */
+ static function must_reauth_for_admin_area() {
+ if (!identity::active_user()->admin) {
+ access::forbidden();
+ }
+
+ $session = Session::instance();
+ $last_active_auth = $session->get("active_auth_timestamp", 0);
+ $last_admin_area_activity = $session->get("admin_area_activity_timestamp", 0);
+ $admin_area_timeout = module::get_var("gallery", "admin_area_timeout");
+
+ if (max($last_active_auth, $last_admin_area_activity) + $admin_area_timeout < time()) {
+ return true;
+ }
+
+ $session->set("admin_area_activity_timestamp", time());
+ return false;
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/batch.php b/modules/gallery/helpers/batch.php
new file mode 100644
index 0000000..bf2425e
--- /dev/null
+++ b/modules/gallery/helpers/batch.php
@@ -0,0 +1,40 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class batch_Core {
+ static function start() {
+ $session = Session::instance();
+ $session->set("batch_level", $session->get("batch_level", 0) + 1);
+ }
+
+ static function stop() {
+ $session = Session::instance();
+ $batch_level = $session->get("batch_level", 0) - 1;
+ if ($batch_level > 0) {
+ $session->set("batch_level", $batch_level);
+ } else {
+ $session->delete("batch_level");
+ module::event("batch_complete");
+ }
+ }
+
+ static function in_progress() {
+ return Session::instance()->get("batch_level", 0) > 0;
+ }
+}
diff --git a/modules/gallery/helpers/block_manager.php b/modules/gallery/helpers/block_manager.php
new file mode 100644
index 0000000..a227946
--- /dev/null
+++ b/modules/gallery/helpers/block_manager.php
@@ -0,0 +1,115 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class block_manager_Core {
+ static function get_active($location) {
+ return unserialize(module::get_var("gallery", "blocks_$location", "a:0:{}"));
+ }
+
+ static function set_active($location, $blocks) {
+ module::set_var("gallery", "blocks_$location", serialize($blocks));
+ }
+
+ static function add($location, $module_name, $block_id) {
+ $blocks = block_manager::get_active($location);
+ $blocks[random::int()] = array($module_name, $block_id);
+
+ block_manager::set_active($location, $blocks);
+ }
+
+ static function activate_blocks($module_name) {
+ $block_class = "{$module_name}_block";
+ if (class_exists($block_class) && method_exists($block_class, "get_site_list")) {
+ $blocks = call_user_func(array($block_class, "get_site_list"));
+ foreach (array_keys($blocks) as $block_id) {
+ block_manager::add("site_sidebar", $module_name, $block_id);
+ }
+ }
+ }
+
+ static function remove($location, $block_id) {
+ $blocks = block_manager::get_active($location);
+ unset($blocks[$block_id]);
+ block_manager::set_active($location, $blocks);
+ }
+
+ static function remove_blocks_for_module($location, $module_name) {
+ $blocks = block_manager::get_active($location);
+ foreach ($blocks as $key => $block) {
+ if ($block[0] == $module_name) {
+ unset($blocks[$key]);
+ }
+ }
+ block_manager::set_active($location, $blocks);
+ }
+
+ static function deactivate_blocks($module_name) {
+ $block_class = "{$module_name}_block";
+ if (class_exists($block_class) && method_exists($block_class, "get_site_list")) {
+ $blocks = call_user_func(array($block_class, "get_site_list"));
+ foreach (array_keys($blocks) as $block_id) {
+ block_manager::remove_blocks_for_module("site_sidebar", $module_name);
+ }
+ }
+
+ if (class_exists($block_class) && method_exists($block_class, "get_admin_list")) {
+ $blocks = call_user_func(array($block_class, "get_admin_list"));
+ foreach (array("dashboard_sidebar", "dashboard_center") as $location) {
+ block_manager::remove_blocks_for_module($location, $module_name);
+ }
+ }
+ }
+
+ static function get_available_admin_blocks() {
+ return self::_get_blocks("get_admin_list");
+ }
+
+ static function get_available_site_blocks() {
+ return self::_get_blocks("get_site_list");
+ }
+
+ private static function _get_blocks($function) {
+ $blocks = array();
+
+ foreach (module::active() as $module) {
+ $class_name = "{$module->name}_block";
+ if (class_exists($class_name) && method_exists($class_name, $function)) {
+ foreach (call_user_func(array($class_name, $function)) as $id => $title) {
+ $blocks["{$module->name}:$id"] = $title;
+ }
+ }
+ }
+ return $blocks;
+ }
+
+ static function get_html($location, $theme=null) {
+ $active = block_manager::get_active($location);
+ $result = "";
+ foreach ($active as $id => $desc) {
+ if (class_exists("$desc[0]_block") && method_exists("$desc[0]_block", "get")) {
+ $block = call_user_func(array("$desc[0]_block", "get"), $desc[1], $theme);
+ if (!empty($block)) {
+ $block->id = $id;
+ $result .= $block;
+ }
+ }
+ }
+ return $result;
+ }
+}
diff --git a/modules/gallery/helpers/data_rest.php b/modules/gallery/helpers/data_rest.php
new file mode 100644
index 0000000..a0a225f
--- /dev/null
+++ b/modules/gallery/helpers/data_rest.php
@@ -0,0 +1,115 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * This resource returns the raw contents of Item_Model data files. It's analogous to the
+ * file_proxy controller, but it uses the REST authentication model.
+ */
+class data_rest_Core {
+ static function get($request) {
+ $item = rest::resolve($request->url);
+
+ $p = $request->params;
+ if (!isset($p->size) || !in_array($p->size, array("thumb", "resize", "full"))) {
+ throw new Rest_Exception("Bad Request", 400, array("errors" => array("size" => "invalid")));
+ }
+
+ // Note: this code is roughly duplicated in file_proxy, so if you modify this, please look to
+ // see if you should make the same change there as well.
+
+ if ($p->size == "full") {
+ if ($item->is_album()) {
+ throw new Kohana_404_Exception();
+ }
+ access::required("view_full", $item);
+ $file = $item->file_path();
+ } else if ($p->size == "resize") {
+ access::required("view", $item);
+ $file = $item->resize_path();
+ } else {
+ access::required("view", $item);
+ $file = $item->thumb_path();
+ }
+
+ if (!file_exists($file)) {
+ throw new Kohana_404_Exception();
+ }
+
+ header("Content-Length: " . filesize($file));
+
+ if (isset($p->m)) {
+ header("Pragma:");
+ // Check that the content hasn't expired or it wasn't changed since cached
+ expires::check(2592000, $item->updated);
+
+ expires::set(2592000, $item->updated); // 30 days
+ }
+
+ // We don't need to save the session for this request
+ Session::instance()->abort_save();
+
+ // Dump out the image. If the item is a movie or album, then its thumbnail will be a JPG.
+ if (($item->is_movie() || $item->is_album()) && $p->size == "thumb") {
+ header("Content-Type: image/jpeg");
+ } else {
+ header("Content-Type: $item->mime_type");
+ }
+
+ if (TEST_MODE) {
+ return $file;
+ } else {
+ Kohana::close_buffers(false);
+
+ if (isset($p->encoding) && $p->encoding == "base64") {
+ print base64_encode(file_get_contents($file));
+ } else {
+ readfile($file);
+ }
+ }
+
+ // We must exit here to keep the regular REST framework reply code from adding more bytes on
+ // at the end or tinkering with headers.
+ exit;
+ }
+
+ static function resolve($id) {
+ $item = ORM::factory("item", $id);
+ if (!access::can("view", $item)) {
+ throw new Kohana_404_Exception();
+ }
+ return $item;
+ }
+
+ static function url($item, $size) {
+ if ($size == "full") {
+ $file = $item->file_path();
+ } else if ($size == "resize") {
+ $file = $item->resize_path();
+ } else {
+ $file = $item->thumb_path();
+ }
+ if (!file_exists($file)) {
+ throw new Kohana_404_Exception();
+ }
+
+ return url::abs_site("rest/data/{$item->id}?size=$size&m=" . filemtime($file));
+ }
+}
+
diff --git a/modules/gallery/helpers/dir.php b/modules/gallery/helpers/dir.php
new file mode 100644
index 0000000..807f7bd
--- /dev/null
+++ b/modules/gallery/helpers/dir.php
@@ -0,0 +1,40 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class dir_Core {
+ static function unlink($path) {
+ if (is_dir($path) && is_writable($path)) {
+ foreach (new DirectoryIterator($path) as $resource) {
+ if ($resource->isDot()) {
+ unset($resource);
+ continue;
+ } else if ($resource->isFile()) {
+ unlink($resource->getPathName());
+ } else if ($resource->isDir()) {
+ dir::unlink($resource->getRealPath());
+ }
+ unset($resource);
+ }
+ return @rmdir($path);
+ }
+ return false;
+ }
+
+
+}
diff --git a/modules/gallery/helpers/encoding.php b/modules/gallery/helpers/encoding.php
new file mode 100644
index 0000000..073aef9
--- /dev/null
+++ b/modules/gallery/helpers/encoding.php
@@ -0,0 +1,35 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class encoding_Core {
+ static function convert_to_utf8($value) {
+ if (function_exists("mb_detect_encoding")) {
+ // Rely on mb_detect_encoding()'s strict mode
+ $src_encoding = mb_detect_encoding($value, mb_detect_order(), true);
+ if ($src_encoding != "UTF-8") {
+ if (function_exists("mb_convert_encoding") && $src_encoding) {
+ $value = mb_convert_encoding($value, "UTF-8", $src_encoding);
+ } else {
+ $value = utf8_encode($value);
+ }
+ }
+ }
+ return $value;
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/gallery.php b/modules/gallery/helpers/gallery.php
new file mode 100644
index 0000000..0adabe4
--- /dev/null
+++ b/modules/gallery/helpers/gallery.php
@@ -0,0 +1,233 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class gallery_Core {
+ const VERSION = "3.0.9";
+ const CODE_NAME = "Chartres";
+ const RELEASE_CHANNEL = "release";
+ const RELEASE_BRANCH = "3.0.x";
+
+ /**
+ * If Gallery is in maintenance mode, then force all non-admins to get routed to a "This site is
+ * down for maintenance" page.
+ */
+ static function maintenance_mode() {
+ if (module::get_var("gallery", "maintenance_mode", 0) &&
+ !identity::active_user()->admin) {
+ try {
+ $class = new ReflectionClass(ucfirst(Router::$controller).'_Controller');
+ $allowed = $class->getConstant("ALLOW_MAINTENANCE_MODE") === true;
+ } catch (ReflectionClass $e) {
+ $allowed = false;
+ }
+ if (!$allowed) {
+ if (Router::$controller == "admin") {
+ // At this point we're in the admin theme and it doesn't have a themed login page, so
+ // we can't just swap in the login controller and have it work. So redirect back to the
+ // root item where we'll run this code again with the site theme.
+ url::redirect(item::root()->abs_url());
+ } else {
+ Session::instance()->set("continue_url", url::abs_site("admin/maintenance"));
+ Router::$controller = "login";
+ Router::$controller_path = MODPATH . "gallery/controllers/login.php";
+ Router::$method = "html";
+ }
+ }
+ }
+ }
+
+ /**
+ * If the gallery is only available to registered users and the user is not logged in, present
+ * the login page.
+ */
+ static function private_gallery() {
+ if (identity::active_user()->guest &&
+ !access::user_can(identity::guest(), "view", item::root()) &&
+ php_sapi_name() != "cli") {
+ try {
+ $class = new ReflectionClass(ucfirst(Router::$controller).'_Controller');
+ $allowed = $class->getConstant("ALLOW_PRIVATE_GALLERY") === true;
+ } catch (ReflectionClass $e) {
+ $allowed = false;
+ }
+ if (!$allowed) {
+ if (Router::$controller == "admin") {
+ // At this point we're in the admin theme and it doesn't have a themed login page, so
+ // we can't just swap in the login controller and have it work. So redirect back to the
+ // root item where we'll run this code again with the site theme.
+ url::redirect(item::root()->abs_url());
+ } else {
+ Session::instance()->set("continue_url", url::abs_current());
+ Router::$controller = "login";
+ Router::$controller_path = MODPATH . "gallery/controllers/login.php";
+ Router::$method = "html";
+ }
+ }
+ }
+ }
+
+ /**
+ * This function is called when the Gallery is fully initialized. We relay it to modules as the
+ * "gallery_ready" event. Any module that wants to perform an action at the start of every
+ * request should implement the <module>_event::gallery_ready() handler.
+ */
+ static function ready() {
+ // Don't keep a session for robots; it's a waste of database space.
+ if (request::user_agent("robot")) {
+ Session::instance()->abort_save();
+ }
+
+ module::event("gallery_ready");
+ }
+
+ /**
+ * This function is called right before the Kohana framework shuts down. We relay it to modules
+ * as the "gallery_shutdown" event. Any module that wants to perform an action at the start of
+ * every request should implement the <module>_event::gallery_shutdown() handler.
+ */
+ static function shutdown() {
+ module::event("gallery_shutdown");
+ }
+
+ /**
+ * Return a unix timestamp in a user specified format including date and time.
+ * @param $timestamp unix timestamp
+ * @return string
+ */
+ static function date_time($timestamp) {
+ return date(module::get_var("gallery", "date_time_format"), $timestamp);
+ }
+
+ /**
+ * Return a unix timestamp in a user specified format that's just the date.
+ * @param $timestamp unix timestamp
+ * @return string
+ */
+ static function date($timestamp) {
+ return date(module::get_var("gallery", "date_format"), $timestamp);
+ }
+
+ /**
+ * Return a unix timestamp in a user specified format that's just the time.
+ * @param $timestamp unix timestamp
+ * @return string
+ */
+ static function time($timestamp) {
+ return date(module::get_var("gallery", "time_format", "H:i:s"), $timestamp);
+ }
+
+ /**
+ * Provide a wrapper function for Kohana::find_file that first strips the extension and
+ * then calls the Kohana::find_file and supplies the extension as the type.
+ * @param string directory to search in
+ * @param string filename to look for
+ * @param boolean file required (optional: default false)
+ * @return array if the extension is config, i18n or l10n
+ * @return string if the file is found (relative to the DOCROOT)
+ * @return false if the file is not found
+ */
+ static function find_file($directory, $file, $required=false) {
+ $file_name = substr($file, 0, -strlen($ext = strrchr($file, '.')));
+ $file_name = Kohana::find_file($directory, $file_name, $required, substr($ext, 1));
+ if (!$file_name) {
+ if (file_exists(DOCROOT . "lib/$directory/$file")) {
+ return "lib/$directory/$file";
+ } else if (file_exists(DOCROOT . "lib/$file")) {
+ return "lib/$file";
+ }
+ }
+
+ if (is_string($file_name)) {
+ // make relative to DOCROOT
+ $parts = explode("/", $file_name);
+ $count = count($parts);
+ foreach ($parts as $idx => $part) {
+ // If this part is "modules" or "themes" make sure that the part 2 after this
+ // is the target directory, and if it is then we're done. This check makes
+ // sure that if Gallery is installed in a directory called "modules" or "themes"
+ // We don't parse the directory structure incorrectly.
+ if (in_array($part, array("modules", "themes")) &&
+ $idx + 2 < $count &&
+ $parts[$idx + 2] == $directory) {
+ break;
+ }
+ unset($parts[$idx]);
+ }
+ $file_name = implode("/", $parts);
+ }
+ return $file_name;
+ }
+
+ /**
+ * Set the PATH environment variable to the paths specified.
+ * @param array Array of paths. Each array entry can contain a colon separated list of paths.
+ */
+ static function set_path_env($paths) {
+ $path_env = array();
+ foreach ($paths as $path) {
+ if ($path) {
+ array_push($path_env, $path);
+ }
+ }
+ putenv("PATH=" . implode(":", $path_env));
+ }
+
+ /**
+ * Return a string describing this version of Gallery and the type of release.
+ */
+ static function version_string() {
+ if (gallery::RELEASE_CHANNEL == "git") {
+ $build_number = gallery::build_number();
+ return sprintf(
+ "%s (branch %s, %s)", gallery::VERSION, gallery::RELEASE_BRANCH,
+ $build_number ? " build $build_number" : "unknown build number");
+ } else {
+ return sprintf("%s (%s)", gallery::VERSION, gallery::CODE_NAME);
+ }
+ }
+
+ /**
+ * Return the contents of the .build_number file, which should be a single integer
+ * or return null if the .build_number file is missing.
+ */
+ static function build_number() {
+ $build_file = DOCROOT . ".build_number";
+ if (file_exists($build_file)) {
+ $result = parse_ini_file(DOCROOT . ".build_number");
+ return $result["build_number"];
+ }
+ return null;
+ }
+
+ /**
+ * Return true if we should show the profiler at the bottom of the page. Note that this
+ * function is called at database setup time so it cannot rely on the database.
+ */
+ static function show_profiler() {
+ return file_exists(VARPATH . "PROFILE");
+ }
+
+ /**
+ * Return true if we should allow Javascript and CSS combining for performance reasons.
+ * Typically we want this, but it's convenient for developers to be able to disable it.
+ */
+ static function allow_css_and_js_combining() {
+ return !file_exists(VARPATH . "DONT_COMBINE");
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/gallery_block.php b/modules/gallery/helpers/gallery_block.php
new file mode 100644
index 0000000..5ac4d74
--- /dev/null
+++ b/modules/gallery/helpers/gallery_block.php
@@ -0,0 +1,145 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class gallery_block_Core {
+ static function get_admin_list() {
+ return array(
+ "welcome" => t("Welcome to Gallery 3!"),
+ "photo_stream" => t("Photo stream"),
+ "log_entries" => t("Log entries"),
+ "stats" => t("Gallery stats"),
+ "platform_info" => t("Platform information"),
+ "project_news" => t("Gallery project news"),
+ "upgrade_checker" => t("Check for Gallery upgrades")
+ );
+ }
+
+ static function get_site_list() {
+ return array("language" => t("Language preference"));
+ }
+
+ static function get($block_id) {
+ $block = new Block();
+ switch ($block_id) {
+ case "welcome":
+ $block->css_id = "g-welcome";
+ $block->title = t("Welcome to Gallery 3");
+ $block->content = new View("admin_block_welcome.html");
+ break;
+
+ case "photo_stream":
+ $block->css_id = "g-photo-stream";
+ $block->title = t("Photo stream");
+ $block->content = new View("admin_block_photo_stream.html");
+ $block->content->photos = ORM::factory("item")
+ ->where("type", "=", "photo")->order_by("created", "DESC")->find_all(10);
+ break;
+
+ case "log_entries":
+ $block->css_id = "g-log-entries";
+ $block->title = t("Log entries");
+ $block->content = new View("admin_block_log_entries.html");
+ $block->content->entries = ORM::factory("log")
+ ->order_by(array("timestamp" => "DESC", "id" => "DESC"))->find_all(5);
+ break;
+
+ case "stats":
+ $block->css_id = "g-stats";
+ $block->title = t("Gallery stats");
+ $block->content = new View("admin_block_stats.html");
+ $block->content->album_count =
+ ORM::factory("item")->where("type", "=", "album")->where("id", "<>", 1)->count_all();
+ $block->content->photo_count = ORM::factory("item")->where("type", "=", "photo")->count_all();
+ break;
+
+ case "platform_info":
+ $block->css_id = "g-platform";
+ $block->title = t("Platform information");
+ $block->content = new View("admin_block_platform.html");
+ break;
+
+ case "project_news":
+ $block->css_id = "g-project-news";
+ $block->title = t("Gallery project news");
+ $block->content = new View("admin_block_news.html");
+ $block->content->feed = feed::parse("http://galleryproject.org/node/feed", 3);
+ break;
+
+ case "block_adder":
+ if ($form = gallery_block::get_add_block_form()) {
+ $block->css_id = "g-block-adder";
+ $block->title = t("Dashboard content");
+ $block->content = $form;
+ } else {
+ $block = "";
+ }
+ break;
+
+ case "language":
+ $locales = locales::installed();
+ if (count($locales) > 1) {
+ foreach ($locales as $locale => $display_name) {
+ $locales[$locale] = SafeString::of_safe_html($display_name);
+ }
+ $block = new Block();
+ $block->css_id = "g-user-language-block";
+ $block->title = t("Language preference");
+ $block->content = new View("user_languages_block.html");
+ $block->content->installed_locales = array_merge(array("" => t("« none »")), $locales);
+ $block->content->selected = (string) locales::cookie_locale();
+ } else {
+ $block = "";
+ }
+ break;
+
+ case "upgrade_checker":
+ $block = new Block();
+ $block->css_id = "g-upgrade-available-block";
+ $block->title = t("Check for Gallery upgrades");
+ $block->content = new View("upgrade_checker_block.html");
+ $block->content->version_info = upgrade_checker::version_info();
+ $block->content->auto_check_enabled = upgrade_checker::auto_check_enabled();
+ $block->content->new_version = upgrade_checker::get_upgrade_message();
+ $block->content->build_number = gallery::build_number();
+ }
+ return $block;
+ }
+
+ static function get_add_block_form() {
+ $available_blocks = block_manager::get_available_admin_blocks();
+
+ $active = array();
+ foreach (array_merge(block_manager::get_active("dashboard_sidebar"),
+ block_manager::get_active("dashboard_center")) as $b) {
+ unset($available_blocks[implode(":", $b)]);
+ }
+
+ if (!$available_blocks) {
+ return;
+ }
+
+ $form = new Forge("admin/dashboard/add_block", "", "post",
+ array("id" => "g-add-dashboard-block-form"));
+ $group = $form->group("add_block")->label(t("Add Block"));
+ $group->dropdown("id")->label(t("Available blocks"))->options($available_blocks);
+ $group->submit("center")->value(t("Add to center"));
+ $group->submit("sidebar")->value(t("Add to sidebar"));
+ return $form;
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/gallery_error.php b/modules/gallery/helpers/gallery_error.php
new file mode 100644
index 0000000..76c8ca9
--- /dev/null
+++ b/modules/gallery/helpers/gallery_error.php
@@ -0,0 +1,30 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class gallery_error_Core {
+ static function error_handler($severity, $message, $filename, $lineno) {
+ if (error_reporting() == 0) {
+ return;
+ }
+
+ if (error_reporting() & $severity) {
+ throw new ErrorException($message, 0, $severity, $filename, $lineno);
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/gallery_event.php b/modules/gallery/helpers/gallery_event.php
new file mode 100644
index 0000000..a319b9c
--- /dev/null
+++ b/modules/gallery/helpers/gallery_event.php
@@ -0,0 +1,621 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+class gallery_event_Core {
+ /**
+ * Initialization.
+ */
+ static function gallery_ready() {
+ if (!get_cfg_var("date.timezone")) {
+ if (!(rand() % 4)) {
+ Kohana_Log::add("error", "date.timezone setting not detected in " .
+ get_cfg_var("cfg_file_path") . " falling back to UTC. " .
+ "Consult http://php.net/manual/function.get-cfg-var.php for help.");
+ }
+ }
+
+ identity::load_user();
+ theme::load_themes();
+ locales::set_request_locale();
+ }
+
+ static function gallery_shutdown() {
+ // Every 500th request, do a pass over var/logs and var/tmp and delete old files.
+ // Limit ourselves to deleting a single file so that we don't spend too much CPU
+ // time on it. As long as servers call this at least twice a day they'll eventually
+ // wind up with a clean var/logs directory because we only create 1 file a day there.
+ // var/tmp might be stickier because theoretically we could wind up spamming that
+ // dir with a lot of files. But let's start with this and refine as we go.
+ if (!(rand() % 500)) {
+ // Note that this code is roughly duplicated in gallery_task::file_cleanup
+ $threshold = time() - 1209600; // older than 2 weeks
+ foreach(array("logs", "tmp") as $dir) {
+ $dir = VARPATH . $dir;
+ if ($dh = opendir($dir)) {
+ while (($file = readdir($dh)) !== false) {
+ if ($file[0] == ".") {
+ continue;
+ }
+
+ // Ignore directories for now, but we should really address them in the long term.
+ if (is_dir("$dir/$file")) {
+ continue;
+ }
+
+ if (filemtime("$dir/$file") <= $threshold) {
+ unlink("$dir/$file");
+ break;
+ }
+ }
+ }
+ }
+ }
+ // Delete all files marked using system::delete_later.
+ system::delete_marked_files();
+ }
+
+ static function user_deleted($user) {
+ $admin = identity::admin_user();
+ if (!empty($admin)) { // could be empty if there is not identity provider
+ db::build()
+ ->update("tasks")
+ ->set("owner_id", $admin->id)
+ ->where("owner_id", "=", $user->id)
+ ->execute();
+ db::build()
+ ->update("items")
+ ->set("owner_id", $admin->id)
+ ->where("owner_id", "=", $user->id)
+ ->execute();
+ db::build()
+ ->update("logs")
+ ->set("user_id", $admin->id)
+ ->where("user_id", "=", $user->id)
+ ->execute();
+ }
+ }
+
+ static function identity_provider_changed($old_provider, $new_provider) {
+ $admin = identity::admin_user();
+ db::build()
+ ->update("tasks")
+ ->set("owner_id", $admin->id)
+ ->execute();
+ db::build()
+ ->update("items")
+ ->set("owner_id", $admin->id)
+ ->execute();
+ db::build()
+ ->update("logs")
+ ->set("user_id", $admin->id)
+ ->execute();
+ module::set_var("gallery", "email_from", $admin->email);
+ module::set_var("gallery", "email_reply_to", $admin->email);
+ }
+
+ static function group_created($group) {
+ access::add_group($group);
+ }
+
+ static function group_deleted($group) {
+ access::delete_group($group);
+ }
+
+ static function item_created($item) {
+ access::add_item($item);
+
+ // Build our thumbnail/resizes.
+ try {
+ graphics::generate($item);
+ } catch (Exception $e) {
+ log::error("graphics", t("Couldn't create a thumbnail or resize for %item_title",
+ array("item_title" => $item->title)),
+ html::anchor($item->abs_url(), t("details")));
+ Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString());
+ }
+
+ if ($item->is_photo() || $item->is_movie()) {
+ // If the parent has no cover item, make this it.
+ $parent = $item->parent();
+ if (access::can("edit", $parent) && $parent->album_cover_item_id == null) {
+ item::make_album_cover($item);
+ }
+ }
+ }
+
+ static function item_deleted($item) {
+ access::delete_item($item);
+
+ // Find any other albums that had the deleted item as the album cover and null it out.
+ // In some cases this may leave us with a missing album cover up in this item's parent
+ // hierarchy, but in most cases it'll work out fine.
+ foreach (ORM::factory("item")
+ ->where("album_cover_item_id", "=", $item->id)
+ ->find_all() as $parent) {
+ item::remove_album_cover($parent);
+ }
+
+ $parent = $item->parent();
+ if (!$parent->album_cover_item_id) {
+ // Assume that we deleted the album cover
+ if (batch::in_progress()) {
+ // Remember that this parent is missing an album cover, for later.
+ $batch_missing_album_cover = Session::instance()->get("batch_missing_album_cover", array());
+ $batch_missing_album_cover[$parent->id] = 1;
+ Session::instance()->set("batch_missing_album_cover", $batch_missing_album_cover);
+ } else {
+ // Choose the first viewable child as the new cover.
+ if ($child = $parent->viewable()->children(1)->current()) {
+ item::make_album_cover($child);
+ }
+ }
+ }
+ }
+
+ static function item_updated_data_file($item) {
+ graphics::generate($item);
+
+ // Update any places where this is the album cover
+ foreach (ORM::factory("item")
+ ->where("album_cover_item_id", "=", $item->id)
+ ->find_all() as $target) {
+ $target->thumb_dirty = 1;
+ $target->save();
+ graphics::generate($target);
+ }
+ }
+
+ static function batch_complete() {
+ // Set the album covers for any items that where we probably deleted the album cover during
+ // this batch. The item may have been deleted, so don't count on it being around. Choose the
+ // first child as the new album cover.
+ // NOTE: if the first child doesn't have an album cover, then this won't work.
+ foreach (array_keys(Session::instance()->get("batch_missing_album_cover", array())) as $id) {
+ $item = ORM::factory("item", $id);
+ if ($item->loaded() && !$item->album_cover_item_id) {
+ if ($child = $item->children(1)->current()) {
+ item::make_album_cover($child);
+ }
+ }
+ }
+ Session::instance()->delete("batch_missing_album_cover");
+ }
+
+ static function item_moved($item, $old_parent) {
+ if ($item->is_album()) {
+ access::recalculate_album_permissions($item->parent());
+ } else {
+ access::recalculate_photo_permissions($item);
+ }
+
+ // If the new parent doesn't have an album cover, make this it.
+ if (!$item->parent()->album_cover_item_id) {
+ item::make_album_cover($item);
+ }
+ }
+
+ static function user_login($user) {
+ // If this user is an admin, check to see if there are any post-install tasks that we need
+ // to run and take care of those now.
+ if ($user->admin && module::get_var("gallery", "choose_default_tookit", null)) {
+ graphics::choose_default_toolkit();
+ module::clear_var("gallery", "choose_default_tookit");
+ }
+ Session::instance()->set("active_auth_timestamp", time());
+ auth::clear_failed_attempts($user);
+ }
+
+ static function user_auth_failed($name) {
+ auth::record_failed_attempt($name);
+ }
+
+ static function user_auth($user) {
+ auth::clear_failed_attempts($user);
+ Session::instance()->set("active_auth_timestamp", time());
+ }
+
+ static function item_index_data($item, $data) {
+ $data[] = $item->description;
+ $data[] = $item->name;
+ $data[] = $item->title;
+ }
+
+ static function user_menu($menu, $theme) {
+ if ($theme->page_subtype != "login") {
+ $user = identity::active_user();
+ if ($user->guest) {
+ $menu->append(Menu::factory("dialog")
+ ->id("user_menu_login")
+ ->css_id("g-login-link")
+ ->url(url::site("login/ajax"))
+ ->label(t("Login")));
+ } else {
+ $csrf = access::csrf_token();
+ $menu->append(Menu::factory("link")
+ ->id("user_menu_edit_profile")
+ ->css_id("g-user-profile-link")
+ ->view("login_current_user.html")
+ ->url(user_profile::url($user->id))
+ ->label($user->display_name()));
+
+ if (Router::$controller == "admin") {
+ $continue_url = url::abs_site("");
+ } else if ($item = $theme->item()) {
+ if (access::user_can(identity::guest(), "view", $theme->item)) {
+ $continue_url = $item->abs_url();
+ } else {
+ $continue_url = item::root()->abs_url();
+ }
+ } else {
+ $continue_url = url::abs_current();
+ }
+
+ $menu->append(Menu::factory("link")
+ ->id("user_menu_logout")
+ ->css_id("g-logout-link")
+ ->url(url::site("logout?csrf=$csrf&amp;continue_url=" . urlencode($continue_url)))
+ ->label(t("Logout")));
+ }
+ }
+ }
+
+ static function site_menu($menu, $theme, $item_css_selector) {
+ if ($theme->page_subtype != "login") {
+ $menu->append(Menu::factory("link")
+ ->id("home")
+ ->label(t("Home"))
+ ->url(item::root()->url()));
+
+
+ $item = $theme->item();
+
+ if (!empty($item)) {
+ $can_edit = $item && access::can("edit", $item);
+ $can_add = $item && access::can("add", $item);
+
+ if ($can_add) {
+ $menu->append($add_menu = Menu::factory("submenu")
+ ->id("add_menu")
+ ->label(t("Add")));
+ $is_album_writable =
+ is_writable($item->is_album() ? $item->file_path() : $item->parent()->file_path());
+ if ($is_album_writable) {
+ $add_menu->append(Menu::factory("dialog")
+ ->id("add_photos_item")
+ ->label(t("Add photos"))
+ ->url(url::site("uploader/index/$item->id")));
+ if ($item->is_album()) {
+ $add_menu->append(Menu::factory("dialog")
+ ->id("add_album_item")
+ ->label(t("Add an album"))
+ ->url(url::site("form/add/albums/$item->id?type=album")));
+ }
+ } else {
+ message::warning(t("The album '%album_name' is not writable.",
+ array("album_name" => $item->title)));
+ }
+ }
+
+ switch ($item->type) {
+ case "album":
+ $option_text = t("Album options");
+ $edit_text = t("Edit album");
+ $delete_text = t("Delete album");
+ break;
+ case "movie":
+ $option_text = t("Movie options");
+ $edit_text = t("Edit movie");
+ $delete_text = t("Delete movie");
+ break;
+ default:
+ $option_text = t("Photo options");
+ $edit_text = t("Edit photo");
+ $delete_text = t("Delete photo");
+ }
+
+ $menu->append($options_menu = Menu::factory("submenu")
+ ->id("options_menu")
+ ->label($option_text));
+ if ($item && ($can_edit || $can_add)) {
+ if ($can_edit) {
+ $options_menu->append(Menu::factory("dialog")
+ ->id("edit_item")
+ ->label($edit_text)
+ ->url(url::site("form/edit/{$item->type}s/$item->id?from_id={$item->id}")));
+ }
+
+ if ($item->is_album()) {
+ if ($can_edit) {
+ $options_menu->append(Menu::factory("dialog")
+ ->id("edit_permissions")
+ ->label(t("Edit permissions"))
+ ->url(url::site("permissions/browse/$item->id")));
+ }
+ }
+ }
+
+ $csrf = access::csrf_token();
+ $page_type = $theme->page_type();
+ if ($can_edit && $item->is_photo() && graphics::can("rotate")) {
+ $options_menu
+ ->append(
+ Menu::factory("ajax_link")
+ ->id("rotate_ccw")
+ ->label(t("Rotate 90° counter clockwise"))
+ ->css_class("ui-icon-rotate-ccw")
+ ->ajax_handler("function(data) { " .
+ "\$.gallery_replace_image(data, \$('$item_css_selector')) }")
+ ->url(url::site("quick/rotate/$item->id/ccw?csrf=$csrf&amp;from_id={$item->id}&amp;page_type=$page_type")))
+ ->append(
+ Menu::factory("ajax_link")
+ ->id("rotate_cw")
+ ->label(t("Rotate 90° clockwise"))
+ ->css_class("ui-icon-rotate-cw")
+ ->ajax_handler("function(data) { " .
+ "\$.gallery_replace_image(data, \$('$item_css_selector')) }")
+ ->url(url::site("quick/rotate/$item->id/cw?csrf=$csrf&amp;from_id={$item->id}&amp;page_type=$page_type")));
+ }
+
+ if ($item->id != item::root()->id) {
+ $parent = $item->parent();
+ if (access::can("edit", $parent)) {
+ // We can't make this item the highlight if it's an album with no album cover, or if it's
+ // already the album cover.
+ if (($item->type == "album" && empty($item->album_cover_item_id)) ||
+ ($item->type == "album" && $parent->album_cover_item_id == $item->album_cover_item_id) ||
+ $parent->album_cover_item_id == $item->id) {
+ $disabledState = "ui-state-disabled";
+ } else {
+ $disabledState = "";
+ }
+
+ if ($item->parent()->id != 1) {
+ $options_menu
+ ->append(
+ Menu::factory("ajax_link")
+ ->id("make_album_cover")
+ ->label(t("Choose as the album cover"))
+ ->css_class("ui-icon-star $disabledState")
+ ->ajax_handler("function(data) { window.location.reload() }")
+ ->url(url::site("quick/make_album_cover/$item->id?csrf=$csrf")));
+ }
+ $options_menu
+ ->append(
+ Menu::factory("dialog")
+ ->id("delete")
+ ->label($delete_text)
+ ->css_class("ui-icon-trash")
+ ->css_class("g-quick-delete")
+ ->url(url::site("quick/form_delete/$item->id?csrf=$csrf&amp;from_id={$item->id}&amp;page_type=$page_type")));
+ }
+ }
+ }
+
+ if (identity::active_user()->admin) {
+ $menu->append($admin_menu = Menu::factory("submenu")
+ ->id("admin_menu")
+ ->label(t("Admin")));
+ module::event("admin_menu", $admin_menu, $theme);
+
+ $settings_menu = $admin_menu->get("settings_menu");
+ uasort($settings_menu->elements, array("Menu", "title_comparator"));
+ }
+ }
+ }
+
+ static function admin_menu($menu, $theme) {
+ $menu
+ ->append(Menu::factory("link")
+ ->id("dashboard")
+ ->label(t("Dashboard"))
+ ->url(url::site("admin")))
+ ->append(Menu::factory("submenu")
+ ->id("settings_menu")
+ ->label(t("Settings"))
+ ->append(Menu::factory("link")
+ ->id("graphics_toolkits")
+ ->label(t("Graphics"))
+ ->url(url::site("admin/graphics")))
+ ->append(Menu::factory("link")
+ ->id("movies_settings")
+ ->label(t("Movies"))
+ ->url(url::site("admin/movies")))
+ ->append(Menu::factory("link")
+ ->id("languages")
+ ->label(t("Languages"))
+ ->url(url::site("admin/languages")))
+ ->append(Menu::factory("link")
+ ->id("advanced")
+ ->label(t("Advanced"))
+ ->url(url::site("admin/advanced_settings"))))
+ ->append(Menu::factory("link")
+ ->id("modules")
+ ->label(t("Modules"))
+ ->url(url::site("admin/modules")))
+ ->append(Menu::factory("submenu")
+ ->id("content_menu")
+ ->label(t("Content")))
+ ->append(Menu::factory("submenu")
+ ->id("appearance_menu")
+ ->label(t("Appearance"))
+ ->append(Menu::factory("link")
+ ->id("themes")
+ ->label(t("Theme choice"))
+ ->url(url::site("admin/themes")))
+ ->append(Menu::factory("link")
+ ->id("theme_options")
+ ->label(t("Theme options"))
+ ->url(url::site("admin/theme_options")))
+ ->append(Menu::factory("link")
+ ->id("sidebar")
+ ->label(t("Manage sidebar"))
+ ->url(url::site("admin/sidebar"))))
+ ->append(Menu::factory("submenu")
+ ->id("statistics_menu")
+ ->label(t("Statistics")))
+ ->append(Menu::factory("link")
+ ->id("maintenance")
+ ->label(t("Maintenance"))
+ ->url(url::site("admin/maintenance")));
+ return $menu;
+ }
+
+ static function context_menu($menu, $theme, $item, $thumb_css_selector) {
+ $menu->append($options_menu = Menu::factory("submenu")
+ ->id("options_menu")
+ ->label(t("Options"))
+ ->css_class("ui-icon-carat-1-n"));
+
+ $page_type = $theme->page_type();
+ if (access::can("edit", $item)) {
+ switch ($item->type) {
+ case "movie":
+ $edit_title = t("Edit this movie");
+ $delete_title = t("Delete this movie");
+ break;
+
+ case "album":
+ $edit_title = t("Edit this album");
+ $delete_title = t("Delete this album");
+ break;
+
+ default:
+ $edit_title = t("Edit this photo");
+ $delete_title = t("Delete this photo");
+ break;
+ }
+ $cover_title = t("Choose as the album cover");
+
+ $csrf = access::csrf_token();
+
+ $theme_item = $theme->item();
+ $options_menu->append(Menu::factory("dialog")
+ ->id("edit")
+ ->label($edit_title)
+ ->css_class("ui-icon-pencil")
+ ->url(url::site("quick/form_edit/$item->id?from_id={$theme_item->id}")));
+
+ if ($item->is_photo() && graphics::can("rotate")) {
+ $options_menu
+ ->append(
+ Menu::factory("ajax_link")
+ ->id("rotate_ccw")
+ ->label(t("Rotate 90° counter clockwise"))
+ ->css_class("ui-icon-rotate-ccw")
+ ->ajax_handler("function(data) { " .
+ "\$.gallery_replace_image(data, \$('$thumb_css_selector')) }")
+ ->url(url::site("quick/rotate/$item->id/ccw?csrf=$csrf&amp;from_id={$theme_item->id}&amp;page_type=$page_type")))
+ ->append(
+ Menu::factory("ajax_link")
+ ->id("rotate_cw")
+ ->label(t("Rotate 90° clockwise"))
+ ->css_class("ui-icon-rotate-cw")
+ ->ajax_handler("function(data) { " .
+ "\$.gallery_replace_image(data, \$('$thumb_css_selector')) }")
+ ->url(url::site("quick/rotate/$item->id/cw?csrf=$csrf&amp;from_id={$theme_item->id}&amp;page_type=$page_type")));
+ }
+
+ $parent = $item->parent();
+ if (access::can("edit", $parent)) {
+ // We can't make this item the highlight if it's an album with no album cover, or if it's
+ // already the album cover.
+ if (($item->type == "album" && empty($item->album_cover_item_id)) ||
+ ($item->type == "album" && $parent->album_cover_item_id == $item->album_cover_item_id) ||
+ $parent->album_cover_item_id == $item->id) {
+ $disabledState = "ui-state-disabled";
+ } else {
+ $disabledState = "";
+ }
+ if ($item->parent()->id != 1) {
+ $options_menu
+ ->append(Menu::factory("ajax_link")
+ ->id("make_album_cover")
+ ->label($cover_title)
+ ->css_class("ui-icon-star $disabledState")
+ ->ajax_handler("function(data) { window.location.reload() }")
+ ->url(url::site("quick/make_album_cover/$item->id?csrf=$csrf")));
+ }
+ $options_menu
+ ->append(Menu::factory("dialog")
+ ->id("delete")
+ ->label($delete_title)
+ ->css_class("ui-icon-trash")
+ ->url(url::site("quick/form_delete/$item->id?csrf=$csrf&amp;" .
+ "from_id={$theme_item->id}&amp;page_type=$page_type")));
+ }
+
+ if ($item->is_album()) {
+ $options_menu
+ ->append(Menu::factory("dialog")
+ ->id("add_item")
+ ->label(t("Add a photo"))
+ ->css_class("ui-icon-plus")
+ ->url(url::site("uploader/index/$item->id")))
+ ->append(Menu::factory("dialog")
+ ->id("add_album")
+ ->label(t("Add an album"))
+ ->css_class("ui-icon-note")
+ ->url(url::site("form/add/albums/$item->id?type=album")))
+ ->append(Menu::factory("dialog")
+ ->id("edit_permissions")
+ ->label(t("Edit permissions"))
+ ->css_class("ui-icon-key")
+ ->url(url::site("permissions/browse/$item->id")));
+ }
+ }
+ }
+
+ static function show_user_profile($data) {
+ $v = new View("user_profile_info.html");
+
+ $fields = array("name" => t("Name"), "locale" => t("Language Preference"),
+ "email" => t("Email"), "full_name" => t("Full name"), "url" => t("Web site"));
+ if (!$data->user->guest) {
+ $fields = array("name" => t("Name"), "full_name" => t("Full name"), "url" => t("Web site"));
+ }
+ $v->user_profile_data = array();
+ foreach ($fields as $field => $label) {
+ if (!empty($data->user->$field)) {
+ $value = $data->user->$field;
+ if ($field == "locale") {
+ $value = locales::display_name($value);
+ } else if ($field == "url") {
+ $value = html::mark_clean(html::anchor(html::clean($data->user->$field)));
+ }
+ $v->user_profile_data[(string) $label] = $value;
+ }
+ }
+ $data->content[] = (object) array("title" => t("User information"), "view" => $v);
+
+ }
+
+ static function user_updated($original_user, $updated_user) {
+ // If the default from/reply-to email address is set to the install time placeholder value
+ // of unknown@unknown.com then adopt the value from the first admin to set their own email
+ // address so that we at least have a valid address for the Gallery.
+ if ($updated_user->admin) {
+ $email = module::get_var("gallery", "email_from", "");
+ if ($email == "unknown@unknown.com") {
+ module::set_var("gallery", "email_from", $updated_user->email);
+ module::set_var("gallery", "email_reply_to", $updated_user->email);
+ }
+ }
+ }
+}
diff --git a/modules/gallery/helpers/gallery_graphics.php b/modules/gallery/helpers/gallery_graphics.php
new file mode 100644
index 0000000..eb76353
--- /dev/null
+++ b/modules/gallery/helpers/gallery_graphics.php
@@ -0,0 +1,183 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class gallery_graphics_Core {
+ /**
+ * Rotate an image. Valid options are degrees
+ *
+ * @param string $input_file
+ * @param string $output_file
+ * @param array $options
+ * @param Item_Model $item (optional)
+ */
+ static function rotate($input_file, $output_file, $options, $item=null) {
+ graphics::init_toolkit();
+
+ $temp_file = system::temp_filename("rotate_", pathinfo($output_file, PATHINFO_EXTENSION));
+ module::event("graphics_rotate", $input_file, $temp_file, $options, $item);
+
+ if (@filesize($temp_file) > 0) {
+ // A graphics_rotate event made an image - move it to output_file and use it.
+ @rename($temp_file, $output_file);
+ } else {
+ // No events made an image - proceed with standard process.
+ if (@filesize($input_file) == 0) {
+ throw new Exception("@todo EMPTY_INPUT_FILE");
+ }
+
+ if (!isset($options["degrees"])) {
+ $options["degrees"] = 0;
+ }
+
+ // Rotate the image. This also implicitly converts its format if needed.
+ Image::factory($input_file)
+ ->quality(module::get_var("gallery", "image_quality"))
+ ->rotate($options["degrees"])
+ ->save($output_file);
+ }
+
+ module::event("graphics_rotate_completed", $input_file, $output_file, $options, $item);
+ }
+
+ /**
+ * Resize an image. Valid options are width, height and master. Master is one of the Image
+ * master dimension constants.
+ *
+ * @param string $input_file
+ * @param string $output_file
+ * @param array $options
+ * @param Item_Model $item (optional)
+ */
+ static function resize($input_file, $output_file, $options, $item=null) {
+ graphics::init_toolkit();
+
+ $temp_file = system::temp_filename("resize_", pathinfo($output_file, PATHINFO_EXTENSION));
+ module::event("graphics_resize", $input_file, $temp_file, $options, $item);
+
+ if (@filesize($temp_file) > 0) {
+ // A graphics_resize event made an image - move it to output_file and use it.
+ @rename($temp_file, $output_file);
+ } else {
+ // No events made an image - proceed with standard process.
+ if (@filesize($input_file) == 0) {
+ throw new Exception("@todo EMPTY_INPUT_FILE");
+ }
+
+ list ($input_width, $input_height, $input_mime, $input_extension) =
+ photo::get_file_metadata($input_file);
+ if ($input_width && $input_height &&
+ (empty($options["width"]) || empty($options["height"]) || empty($options["master"]) ||
+ (max($input_width, $input_height) <= min($options["width"], $options["height"])))) {
+ // Photo dimensions well-defined, but options not well-defined or would upscale the image.
+ // Do not resize. Check mimes to see if we can copy the file or if we need to convert it.
+ // (checking mimes avoids needlessly converting jpg to jpeg, etc.)
+ $output_mime = legal_file::get_photo_types_by_extension(pathinfo($output_file, PATHINFO_EXTENSION));
+ if ($input_mime && $output_mime && ($input_mime == $output_mime)) {
+ // Mimes well-defined and identical - copy input to output
+ copy($input_file, $output_file);
+ } else {
+ // Mimes not well-defined or not the same - convert input to output
+ $image = Image::factory($input_file)
+ ->quality(module::get_var("gallery", "image_quality"))
+ ->save($output_file);
+ }
+ } else {
+ // Resize the image. This also implicitly converts its format if needed.
+ $image = Image::factory($input_file)
+ ->resize($options["width"], $options["height"], $options["master"])
+ ->quality(module::get_var("gallery", "image_quality"));
+ if (graphics::can("sharpen")) {
+ $image->sharpen(module::get_var("gallery", "image_sharpen"));
+ }
+ $image->save($output_file);
+ }
+ }
+
+ module::event("graphics_resize_completed", $input_file, $output_file, $options, $item);
+ }
+
+ /**
+ * Overlay an image on top of the input file.
+ *
+ * Valid options are: file, position, transparency, padding
+ *
+ * Valid positions: northwest, north, northeast,
+ * west, center, east,
+ * southwest, south, southeast
+ *
+ * padding is in pixels
+ *
+ * @param string $input_file
+ * @param string $output_file
+ * @param array $options
+ * @param Item_Model $item (optional)
+ */
+ static function composite($input_file, $output_file, $options, $item=null) {
+ try {
+ graphics::init_toolkit();
+
+ $temp_file = system::temp_filename("composite_", pathinfo($output_file, PATHINFO_EXTENSION));
+ module::event("graphics_composite", $input_file, $temp_file, $options, $item);
+
+ if (@filesize($temp_file) > 0) {
+ // A graphics_composite event made an image - move it to output_file and use it.
+ @rename($temp_file, $output_file);
+ } else {
+ // No events made an image - proceed with standard process.
+
+ list ($width, $height) = photo::get_file_metadata($input_file);
+ list ($w_width, $w_height) = photo::get_file_metadata($options["file"]);
+
+ $pad = isset($options["padding"]) ? $options["padding"] : 10;
+ $top = $pad;
+ $left = $pad;
+ $y_center = max($height / 2 - $w_height / 2, $pad);
+ $x_center = max($width / 2 - $w_width / 2, $pad);
+ $bottom = max($height - $w_height - $pad, $pad);
+ $right = max($width - $w_width - $pad, $pad);
+
+ switch ($options["position"]) {
+ case "northwest": $x = $left; $y = $top; break;
+ case "north": $x = $x_center; $y = $top; break;
+ case "northeast": $x = $right; $y = $top; break;
+ case "west": $x = $left; $y = $y_center; break;
+ case "center": $x = $x_center; $y = $y_center; break;
+ case "east": $x = $right; $y = $y_center; break;
+ case "southwest": $x = $left; $y = $bottom; break;
+ case "south": $x = $x_center; $y = $bottom; break;
+ case "southeast": $x = $right; $y = $bottom; break;
+ }
+
+ Image::factory($input_file)
+ ->composite($options["file"], $x, $y, $options["transparency"])
+ ->quality(module::get_var("gallery", "image_quality"))
+ ->save($output_file);
+ }
+
+ module::event("graphics_composite_completed", $input_file, $output_file, $options, $item);
+ } catch (ErrorException $e) {
+ // Unlike rotate and resize, composite catches its exceptions here. This is because
+ // composite is typically called for watermarks. If during thumb/resize generation
+ // the watermark fails, we'd still like the image resized, just without its watermark.
+ // If the exception isn't caught here, graphics::generate will replace it with a
+ // placeholder.
+ Kohana_Log::add("error", $e->getMessage());
+ }
+ }
+}
diff --git a/modules/gallery/helpers/gallery_installer.php b/modules/gallery/helpers/gallery_installer.php
new file mode 100644
index 0000000..d49be83
--- /dev/null
+++ b/modules/gallery/helpers/gallery_installer.php
@@ -0,0 +1,844 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class gallery_installer {
+ static function install() {
+ $db = Database::instance();
+ $db->query("CREATE TABLE {access_caches} (
+ `id` int(9) NOT NULL auto_increment,
+ `item_id` int(9),
+ PRIMARY KEY (`id`),
+ KEY (`item_id`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {access_intents} (
+ `id` int(9) NOT NULL auto_increment,
+ `item_id` int(9),
+ PRIMARY KEY (`id`))
+ DEFAULT CHARSET=utf8;");
+
+ // Using a simple index instead of a unique key for the
+ // key column to avoid handling of concurrency issues
+ // on insert. Thus allowing concurrent inserts on the
+ // same cache key, as does Memcache / xcache.
+ $db->query("CREATE TABLE {caches} (
+ `id` int(9) NOT NULL auto_increment,
+ `key` varchar(255) NOT NULL,
+ `tags` varchar(255),
+ `expiration` int(9) NOT NULL,
+ `cache` longblob,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY (`key`),
+ KEY (`tags`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {failed_auths} (
+ `id` int(9) NOT NULL auto_increment,
+ `count` int(9) NOT NULL,
+ `name` varchar(255) NOT NULL,
+ `time` int(9) NOT NULL,
+ PRIMARY KEY (`id`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {graphics_rules} (
+ `id` int(9) NOT NULL auto_increment,
+ `active` BOOLEAN default 0,
+ `args` varchar(255) default NULL,
+ `module_name` varchar(64) NOT NULL,
+ `operation` varchar(64) NOT NULL,
+ `priority` int(9) NOT NULL,
+ `target` varchar(32) NOT NULL,
+ PRIMARY KEY (`id`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {incoming_translations} (
+ `id` int(9) NOT NULL auto_increment,
+ `key` char(32) NOT NULL,
+ `locale` char(10) NOT NULL,
+ `message` text NOT NULL,
+ `revision` int(9) DEFAULT NULL,
+ `translation` text,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY(`key`, `locale`),
+ KEY `locale_key` (`locale`, `key`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {items} (
+ `id` int(9) NOT NULL auto_increment,
+ `album_cover_item_id` int(9) default NULL,
+ `captured` int(9) default NULL,
+ `created` int(9) default NULL,
+ `description` text default NULL,
+ `height` int(9) default NULL,
+ `left_ptr` int(9) NOT NULL,
+ `level` int(9) NOT NULL,
+ `mime_type` varchar(64) default NULL,
+ `name` varchar(255) default NULL,
+ `owner_id` int(9) default NULL,
+ `parent_id` int(9) NOT NULL,
+ `rand_key` decimal(11,10) default NULL,
+ `relative_path_cache` varchar(255) default NULL,
+ `relative_url_cache` varchar(255) default NULL,
+ `resize_dirty` boolean default 1,
+ `resize_height` int(9) default NULL,
+ `resize_width` int(9) default NULL,
+ `right_ptr` int(9) NOT NULL,
+ `slug` varchar(255) default NULL,
+ `sort_column` varchar(64) default NULL,
+ `sort_order` char(4) default 'ASC',
+ `thumb_dirty` boolean default 1,
+ `thumb_height` int(9) default NULL,
+ `thumb_width` int(9) default NULL,
+ `title` varchar(255) default NULL,
+ `type` varchar(32) NOT NULL,
+ `updated` int(9) default NULL,
+ `view_count` int(9) default 0,
+ `weight` int(9) NOT NULL default 0,
+ `width` int(9) default NULL,
+ PRIMARY KEY (`id`),
+ KEY `parent_id` (`parent_id`),
+ KEY `type` (`type`),
+ KEY `random` (`rand_key`),
+ KEY `weight` (`weight` DESC),
+ KEY `left_ptr` (`left_ptr`),
+ KEY `relative_path_cache` (`relative_path_cache`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {logs} (
+ `id` int(9) NOT NULL auto_increment,
+ `category` varchar(64) default NULL,
+ `html` varchar(255) default NULL,
+ `message` text default NULL,
+ `referer` varchar(255) default NULL,
+ `severity` int(9) default 0,
+ `timestamp` int(9) default 0,
+ `url` varchar(255) default NULL,
+ `user_id` int(9) default 0,
+ PRIMARY KEY (`id`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {messages} (
+ `id` int(9) NOT NULL auto_increment,
+ `key` varchar(255) default NULL,
+ `severity` varchar(32) default NULL,
+ `value` text default NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY(`key`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {modules} (
+ `id` int(9) NOT NULL auto_increment,
+ `active` BOOLEAN default 0,
+ `name` varchar(64) default NULL,
+ `version` int(9) default NULL,
+ `weight` int(9) default NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY(`name`),
+ KEY (`weight`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {outgoing_translations} (
+ `id` int(9) NOT NULL auto_increment,
+ `base_revision` int(9) DEFAULT NULL,
+ `key` char(32) NOT NULL,
+ `locale` char(10) NOT NULL,
+ `message` text NOT NULL,
+ `translation` text,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY(`key`, `locale`),
+ KEY `locale_key` (`locale`, `key`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {permissions} (
+ `id` int(9) NOT NULL auto_increment,
+ `display_name` varchar(64) default NULL,
+ `name` varchar(64) default NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY(`name`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {sessions} (
+ `session_id` varchar(127) NOT NULL,
+ `data` text NOT NULL,
+ `last_activity` int(10) UNSIGNED NOT NULL,
+ PRIMARY KEY (`session_id`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {tasks} (
+ `id` int(9) NOT NULL auto_increment,
+ `callback` varchar(128) default NULL,
+ `context` text NOT NULL,
+ `done` boolean default 0,
+ `name` varchar(128) default NULL,
+ `owner_id` int(9) default NULL,
+ `percent_complete` int(9) default 0,
+ `state` varchar(32) default NULL,
+ `status` varchar(255) default NULL,
+ `updated` int(9) default NULL,
+ PRIMARY KEY (`id`),
+ KEY (`owner_id`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {themes} (
+ `id` int(9) NOT NULL auto_increment,
+ `name` varchar(64) default NULL,
+ `version` int(9) default NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY(`name`))
+ DEFAULT CHARSET=utf8;");
+
+ $db->query("CREATE TABLE {vars} (
+ `id` int(9) NOT NULL auto_increment,
+ `module_name` varchar(64) NOT NULL,
+ `name` varchar(64) NOT NULL,
+ `value` text,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY(`module_name`, `name`))
+ DEFAULT CHARSET=utf8;");
+
+ foreach (array("albums", "logs", "modules", "resizes", "thumbs", "tmp", "uploads") as $dir) {
+ @mkdir(VARPATH . $dir);
+ if (in_array($dir, array("logs", "tmp", "uploads"))) {
+ self::_protect_directory(VARPATH . $dir);
+ }
+ }
+
+ access::register_permission("view", "View");
+ access::register_permission("view_full", "View full size");
+ access::register_permission("edit", "Edit");
+ access::register_permission("add", "Add");
+
+ // Mark for translation (must be the same strings as used above)
+ t("View full size");
+ t("View");
+ t("Edit");
+ t("Add");
+
+ // Hardcode the first item to sidestep ORM validation rules
+ $now = time();
+ db::build()->insert(
+ "items",
+ array("created" => $now,
+ "description" => "",
+ "left_ptr" => 1,
+ "level" => 1,
+ "parent_id" => 0,
+ "resize_dirty" => 1,
+ "right_ptr" => 2,
+ "sort_column" => "weight",
+ "sort_order" => "ASC",
+ "thumb_dirty" => 1,
+ "title" => "Gallery",
+ "type" => "album",
+ "updated" => $now,
+ "weight" => 1))
+ ->execute();
+ $root = ORM::factory("item", 1);
+ access::add_item($root);
+
+ module::set_var("gallery", "active_site_theme", "wind");
+ module::set_var("gallery", "active_admin_theme", "admin_wind");
+ module::set_var("gallery", "page_size", 9);
+ module::set_var("gallery", "thumb_size", 200);
+ module::set_var("gallery", "resize_size", 640);
+ module::set_var("gallery", "default_locale", "en_US");
+ module::set_var("gallery", "image_quality", 75);
+ module::set_var("gallery", "image_sharpen", 15);
+ module::set_var("gallery", "upgrade_checker_auto_enabled", true);
+
+ // Add rules for generating our thumbnails and resizes
+ graphics::add_rule(
+ "gallery", "thumb", "gallery_graphics::resize",
+ array("width" => 200, "height" => 200, "master" => Image::AUTO),
+ 100);
+ graphics::add_rule(
+ "gallery", "resize", "gallery_graphics::resize",
+ array("width" => 640, "height" => 640, "master" => Image::AUTO),
+ 100);
+
+ // Instantiate default themes (site and admin)
+ foreach (array("wind", "admin_wind") as $theme_name) {
+ $theme_info = new ArrayObject(parse_ini_file(THEMEPATH . $theme_name . "/theme.info"),
+ ArrayObject::ARRAY_AS_PROPS);
+ $theme = ORM::factory("theme");
+ $theme->name = $theme_name;
+ $theme->version = $theme_info->version;
+ $theme->save();
+ }
+
+ block_manager::add("dashboard_sidebar", "gallery", "block_adder");
+ block_manager::add("dashboard_sidebar", "gallery", "stats");
+ block_manager::add("dashboard_sidebar", "gallery", "platform_info");
+ block_manager::add("dashboard_sidebar", "gallery", "project_news");
+ block_manager::add("dashboard_center", "gallery", "welcome");
+ block_manager::add("dashboard_center", "gallery", "upgrade_checker");
+ block_manager::add("dashboard_center", "gallery", "photo_stream");
+ block_manager::add("dashboard_center", "gallery", "log_entries");
+
+ module::set_var("gallery", "choose_default_tookit", 1);
+ module::set_var("gallery", "date_format", "Y-M-d");
+ module::set_var("gallery", "date_time_format", "Y-M-d H:i:s");
+ module::set_var("gallery", "time_format", "H:i:s");
+ module::set_var("gallery", "show_credits", 1);
+ // Mark string for translation
+ $powered_by_string = t("Powered by <a href=\"%url\">%gallery_version</a>",
+ array("locale" => "root"));
+ module::set_var("gallery", "credits", (string) $powered_by_string);
+ module::set_var("gallery", "simultaneous_upload_limit", 5);
+ module::set_var("gallery", "admin_area_timeout", 90 * 60);
+ module::set_var("gallery", "maintenance_mode", 0);
+ module::set_var("gallery", "visible_title_length", 15);
+ module::set_var("gallery", "favicon_url", "lib/images/favicon.ico");
+ module::set_var("gallery", "apple_touch_icon_url", "lib/images/apple-touch-icon.png");
+ module::set_var("gallery", "email_from", "");
+ module::set_var("gallery", "email_reply_to", "");
+ module::set_var("gallery", "email_line_length", 70);
+ module::set_var("gallery", "email_header_separator", serialize("\n"));
+ module::set_var("gallery", "show_user_profiles_to", "registered_users");
+ module::set_var("gallery", "extra_binary_paths", "/usr/local/bin:/opt/local/bin:/opt/bin");
+ module::set_var("gallery", "timezone", null);
+ module::set_var("gallery", "lock_timeout", 1);
+ module::set_var("gallery", "movie_extract_frame_time", 3);
+ module::set_var("gallery", "movie_allow_uploads", "autodetect");
+ }
+
+ static function upgrade($version) {
+ $db = Database::instance();
+ if ($version == 1) {
+ module::set_var("gallery", "date_format", "Y-M-d");
+ module::set_var("gallery", "date_time_format", "Y-M-d H:i:s");
+ module::set_var("gallery", "time_format", "H:i:s");
+ module::set_version("gallery", $version = 2);
+ }
+
+ if ($version == 2) {
+ module::set_var("gallery", "show_credits", 1);
+ module::set_version("gallery", $version = 3);
+ }
+
+ if ($version == 3) {
+ $db->query("CREATE TABLE {caches} (
+ `id` varchar(255) NOT NULL,
+ `tags` varchar(255),
+ `expiration` int(9) NOT NULL,
+ `cache` text,
+ PRIMARY KEY (`id`),
+ KEY (`tags`))
+ DEFAULT CHARSET=utf8;");
+ module::set_version("gallery", $version = 4);
+ }
+
+ if ($version == 4) {
+ Cache::instance()->delete_all();
+ $db->query("ALTER TABLE {caches} MODIFY COLUMN `cache` LONGBLOB");
+ module::set_version("gallery", $version = 5);
+ }
+
+ if ($version == 5) {
+ Cache::instance()->delete_all();
+ $db->query("ALTER TABLE {caches} DROP COLUMN `id`");
+ $db->query("ALTER TABLE {caches} ADD COLUMN `key` varchar(255) NOT NULL");
+ $db->query("ALTER TABLE {caches} ADD COLUMN `id` int(9) NOT NULL auto_increment PRIMARY KEY");
+ module::set_version("gallery", $version = 6);
+ }
+
+ if ($version == 6) {
+ module::clear_var("gallery", "version");
+ module::set_version("gallery", $version = 7);
+ }
+
+ if ($version == 7) {
+ $groups = identity::groups();
+ $permissions = ORM::factory("permission")->find_all();
+ foreach($groups as $group) {
+ foreach($permissions as $permission) {
+ // Update access intents
+ $db->query("ALTER TABLE {access_intents} MODIFY COLUMN {$permission->name}_{$group->id} BINARY(1) DEFAULT NULL");
+ // Update access cache
+ if ($permission->name === "view") {
+ $db->query("ALTER TABLE {items} MODIFY COLUMN {$permission->name}_{$group->id} BINARY(1) DEFAULT FALSE");
+ } else {
+ $db->query("ALTER TABLE {access_caches} MODIFY COLUMN {$permission->name}_{$group->id} BINARY(1) NOT NULL DEFAULT FALSE");
+ }
+ }
+ }
+ module::set_version("gallery", $version = 8);
+ }
+
+ if ($version == 8) {
+ $db->query("ALTER TABLE {items} CHANGE COLUMN `left` `left_ptr` INT(9) NOT NULL;");
+ $db->query("ALTER TABLE {items} CHANGE COLUMN `right` `right_ptr` INT(9) NOT NULL;");
+ module::set_version("gallery", $version = 9);
+ }
+
+ if ($version == 9) {
+ $db->query("ALTER TABLE {items} ADD KEY `weight` (`weight` DESC);");
+
+ module::set_version("gallery", $version = 10);
+ }
+
+ if ($version == 10) {
+ module::set_var("gallery", "image_sharpen", 15);
+
+ module::set_version("gallery", $version = 11);
+ }
+
+ if ($version == 11) {
+ $db->query("ALTER TABLE {items} ADD COLUMN `relative_url_cache` varchar(255) DEFAULT NULL");
+ $db->query("ALTER TABLE {items} ADD COLUMN `slug` varchar(255) DEFAULT NULL");
+
+ // This is imperfect since some of the slugs may contain invalid characters, but it'll do
+ // for now because we don't want a lengthy operation here.
+ $db->query("UPDATE {items} SET `slug` = `name`");
+
+ // Flush all path caches because we're going to start urlencoding them.
+ $db->query("UPDATE {items} SET `relative_url_cache` = NULL, `relative_path_cache` = NULL");
+ module::set_version("gallery", $version = 12);
+ }
+
+ if ($version == 12) {
+ if (module::get_var("gallery", "active_site_theme") == "default") {
+ module::set_var("gallery", "active_site_theme", "wind");
+ }
+ if (module::get_var("gallery", "active_admin_theme") == "admin_default") {
+ module::set_var("gallery", "active_admin_theme", "admin_wind");
+ }
+ module::set_version("gallery", $version = 13);
+ }
+
+ if ($version == 13) {
+ // Add rules for generating our thumbnails and resizes
+ Database::instance()->query(
+ "UPDATE {graphics_rules} SET `operation` = CONCAT('gallery_graphics::', `operation`);");
+ module::set_version("gallery", $version = 14);
+ }
+
+ if ($version == 14) {
+ $sidebar_blocks = block_manager::get_active("site_sidebar");
+ if (empty($sidebar_blocks)) {
+ $available_blocks = block_manager::get_available_site_blocks();
+ foreach (array_keys(block_manager::get_available_site_blocks()) as $id) {
+ $sidebar_blocks[] = explode(":", $id);
+ }
+ block_manager::set_active("site_sidebar", $sidebar_blocks);
+ }
+ module::set_version("gallery", $version = 15);
+ }
+
+ if ($version == 15) {
+ module::set_var("gallery", "identity_provider", "user");
+ module::set_version("gallery", $version = 16);
+ }
+
+ // Convert block keys to an md5 hash of the module and block name
+ if ($version == 16) {
+ foreach (array("dashboard_sidebar", "dashboard_center", "site_sidebar") as $location) {
+ $blocks = block_manager::get_active($location);
+ $new_blocks = array();
+ foreach ($blocks as $block) {
+ $new_blocks[md5("{$block[0]}:{$block[1]}")] = $block;
+ }
+ block_manager::set_active($location, $new_blocks);
+ }
+ module::set_version("gallery", $version = 17);
+ }
+
+ // We didn't like md5 hashes so convert block keys back to random keys to allow duplicates.
+ if ($version == 17) {
+ foreach (array("dashboard_sidebar", "dashboard_center", "site_sidebar") as $location) {
+ $blocks = block_manager::get_active($location);
+ $new_blocks = array();
+ foreach ($blocks as $block) {
+ $new_blocks[random::int()] = $block;
+ }
+ block_manager::set_active($location, $new_blocks);
+ }
+ module::set_version("gallery", $version = 18);
+ }
+
+ // Rename blocks_site.sidebar to blocks_site_sidebar
+ if ($version == 18) {
+ $blocks = block_manager::get_active("site.sidebar");
+ block_manager::set_active("site_sidebar", $blocks);
+ module::clear_var("gallery", "blocks_site.sidebar");
+ module::set_version("gallery", $version = 19);
+ }
+
+ // Set a default for the number of simultaneous uploads
+ // Version 20 was reverted in 57adefc5baa7a2b0dfcd3e736e80c2fa86d3bfa2, so skip it.
+ if ($version == 19 || $version == 20) {
+ module::set_var("gallery", "simultaneous_upload_limit", 5);
+ module::set_version("gallery", $version = 21);
+ }
+
+ // Update the graphics rules table so that the maximum height for resizes is 640 not 480.
+ // Fixes ticket #671
+ if ($version == 21) {
+ $resize_rule = ORM::factory("graphics_rule")
+ ->where("id", "=", "2")
+ ->find();
+ // make sure it hasn't been changed already
+ $args = unserialize($resize_rule->args);
+ if ($args["height"] == 480 && $args["width"] == 640) {
+ $args["height"] = 640;
+ $resize_rule->args = serialize($args);
+ $resize_rule->save();
+ }
+ module::set_version("gallery", $version = 22);
+ }
+
+ // Update slug values to be legal. We should have done this in the 11->12 upgrader, but I was
+ // lazy. Mea culpa!
+ if ($version == 22) {
+ foreach (db::build()
+ ->from("items")
+ ->select("id", "slug")
+ ->where(db::expr("`slug` REGEXP '[^_A-Za-z0-9-]'"), "=", 1)
+ ->execute() as $row) {
+ $new_slug = item::convert_filename_to_slug($row->slug);
+ if (empty($new_slug)) {
+ $new_slug = random::int();
+ }
+ db::build()
+ ->update("items")
+ ->set("slug", $new_slug)
+ ->set("relative_url_cache", null)
+ ->where("id", "=", $row->id)
+ ->execute();
+ }
+ module::set_version("gallery", $version = 23);
+ }
+
+ if ($version == 23) {
+ $db->query("CREATE TABLE {failed_logins} (
+ `id` int(9) NOT NULL auto_increment,
+ `count` int(9) NOT NULL,
+ `name` varchar(255) NOT NULL,
+ `time` int(9) NOT NULL,
+ PRIMARY KEY (`id`))
+ DEFAULT CHARSET=utf8;");
+ module::set_version("gallery", $version = 24);
+ }
+
+ if ($version == 24) {
+ foreach (array("logs", "tmp", "uploads") as $dir) {
+ self::_protect_directory(VARPATH . $dir);
+ }
+ module::set_version("gallery", $version = 25);
+ }
+
+ if ($version == 25) {
+ db::build()
+ ->update("items")
+ ->set("title", db::expr("`name`"))
+ ->and_open()
+ ->where("title", "IS", null)
+ ->or_where("title", "=", "")
+ ->close()
+ ->execute();
+ module::set_version("gallery", $version = 26);
+ }
+
+ if ($version == 26) {
+ if (in_array("failed_logins", Database::instance()->list_tables())) {
+ $db->query("RENAME TABLE {failed_logins} TO {failed_auths}");
+ }
+ module::set_version("gallery", $version = 27);
+ }
+
+ if ($version == 27) {
+ // Set the admin area timeout to 90 minutes
+ module::set_var("gallery", "admin_area_timeout", 90 * 60);
+ module::set_version("gallery", $version = 28);
+ }
+
+ if ($version == 28) {
+ module::set_var("gallery", "credits", "Powered by <a href=\"%url\">%gallery_version</a>");
+ module::set_version("gallery", $version = 29);
+ }
+
+ if ($version == 29) {
+ $db->query("ALTER TABLE {caches} ADD KEY (`key`);");
+ module::set_version("gallery", $version = 30);
+ }
+
+ if ($version == 30) {
+ module::set_var("gallery", "maintenance_mode", 0);
+ module::set_version("gallery", $version = 31);
+ }
+
+ if ($version == 31) {
+ $db->query("ALTER TABLE {modules} ADD COLUMN `weight` int(9) DEFAULT NULL");
+ $db->query("ALTER TABLE {modules} ADD KEY (`weight`)");
+ db::update("modules")
+ ->set("weight", db::expr("`id`"))
+ ->execute();
+ module::set_version("gallery", $version = 32);
+ }
+
+ if ($version == 32) {
+ $db->query("ALTER TABLE {items} ADD KEY (`left_ptr`)");
+ module::set_version("gallery", $version = 33);
+ }
+
+ if ($version == 33) {
+ $db->query("ALTER TABLE {access_caches} ADD KEY (`item_id`)");
+ module::set_version("gallery", $version = 34);
+ }
+
+ if ($version == 34) {
+ module::set_var("gallery", "visible_title_length", 15);
+ module::set_version("gallery", $version = 35);
+ }
+
+ if ($version == 35) {
+ module::set_var("gallery", "favicon_url", "lib/images/favicon.ico");
+ module::set_version("gallery", $version = 36);
+ }
+
+ if ($version == 36) {
+ module::set_var("gallery", "email_from", "admin@example.com");
+ module::set_var("gallery", "email_reply_to", "public@example.com");
+ module::set_var("gallery", "email_line_length", 70);
+ module::set_var("gallery", "email_header_separator", serialize("\n"));
+ module::set_version("gallery", $version = 37);
+ }
+
+ // Changed our minds and decided that the initial value should be empty
+ // But don't just reset it blindly, only do it if the value is version 37 default
+ if ($version == 37) {
+ $email = module::get_var("gallery", "email_from", "");
+ if ($email == "admin@example.com") {
+ module::set_var("gallery", "email_from", "");
+ }
+ $email = module::get_var("gallery", "email_reply_to", "");
+ if ($email == "admin@example.com") {
+ module::set_var("gallery", "email_reply_to", "");
+ }
+ module::set_version("gallery", $version = 38);
+ }
+
+ if ($version == 38) {
+ module::set_var("gallery", "show_user_profiles_to", "registered_users");
+ module::set_version("gallery", $version = 39);
+ }
+
+ if ($version == 39) {
+ module::set_var("gallery", "extra_binary_paths", "/usr/local/bin:/opt/local/bin:/opt/bin");
+ module::set_version("gallery", $version = 40);
+ }
+
+ if ($version == 40) {
+ module::clear_var("gallery", "_cache");
+ module::set_version("gallery", $version = 41);
+ }
+
+ if ($version == 41) {
+ $db->query("TRUNCATE TABLE {caches}");
+ $db->query("ALTER TABLE {caches} DROP INDEX `key`, ADD UNIQUE `key` (`key`)");
+ module::set_version("gallery", $version = 42);
+ }
+
+ if ($version == 42) {
+ $db->query("ALTER TABLE {items} CHANGE `description` `description` text DEFAULT NULL");
+ module::set_version("gallery", $version = 43);
+ }
+
+ if ($version == 43) {
+ $db->query("ALTER TABLE {items} CHANGE `rand_key` `rand_key` DECIMAL(11, 10)");
+ module::set_version("gallery", $version = 44);
+ }
+
+ if ($version == 44) {
+ $db->query("ALTER TABLE {messages} CHANGE `value` `value` text default NULL");
+ module::set_version("gallery", $version = 45);
+ }
+
+ if ($version == 45) {
+ // Splice the upgrade_checker block into the admin dashboard at the top
+ // of the page, but under the welcome block if it's in the first position.
+ $blocks = block_manager::get_active("dashboard_center");
+ $index = count($blocks) && current($blocks) == array("gallery", "welcome") ? 1 : 0;
+ array_splice($blocks, $index, 0, array(random::int() => array("gallery", "upgrade_checker")));
+ block_manager::set_active("dashboard_center", $blocks);
+
+ module::set_var("gallery", "upgrade_checker_auto_enabled", true);
+ module::set_version("gallery", $version = 46);
+ }
+
+ if ($version == 46) {
+ module::set_var("gallery", "apple_touch_icon_url", "lib/images/apple-touch-icon.png");
+ module::set_version("gallery", $version = 47);
+ }
+
+ if ($version == 47 || $version == 48) {
+ // Add configuration variable to set timezone. Defaults to the currently
+ // used timezone (from PHP configuration). Note that in v48 we were
+ // setting this value incorrectly, so we're going to stomp this value for v49.
+ module::set_var("gallery", "timezone", null);
+ module::set_version("gallery", $version = 49);
+ }
+
+ if ($version == 49) {
+ // In v49 we changed the Item_Model validation code to disallow files with two dots in them,
+ // but we didn't rename any files which fail to validate, so as soon as you do anything to
+ // change those files (eg. as a side effect of getting the url or file path) it fails to
+ // validate. Fix those here. This might be slow, but if it times out it can just pick up
+ // where it left off.
+ foreach (db::build()
+ ->from("items")
+ ->select("id")
+ ->where("type", "<>", "album")
+ ->where(db::expr("`name` REGEXP '\\\\..*\\\\.'"), "=", 1)
+ ->order_by("id", "asc")
+ ->execute() as $row) {
+ set_time_limit(30);
+ $item = ORM::factory("item", $row->id);
+ $item->name = legal_file::smash_extensions($item->name);
+ $item->save();
+ }
+ module::set_version("gallery", $version = 50);
+ }
+
+ if ($version == 50) {
+ // In v51, we added a lock_timeout variable so that administrators could edit the time out
+ // from 1 second to a higher variable if their system runs concurrent parallel uploads for
+ // instance.
+ module::set_var("gallery", "lock_timeout", 1);
+ module::set_version("gallery", $version = 51);
+ }
+
+ if ($version == 51) {
+ // In v52, we added functions to the legal_file helper that map photo and movie file
+ // extensions to their mime types (and allow extension of the list by other modules). During
+ // this process, we correctly mapped m4v files to video/x-m4v, correcting a previous error
+ // where they were mapped to video/mp4. This corrects the existing items.
+ db::build()
+ ->update("items")
+ ->set("mime_type", "video/x-m4v")
+ ->where("name", "REGEXP", "\.m4v$") // case insensitive since name column is utf8_general_ci
+ ->execute();
+ module::set_version("gallery", $version = 52);
+ }
+
+ if ($version == 52) {
+ // In v53, we added the ability to change the default time used when extracting frames from
+ // movies. Previously we hard-coded this at 3 seconds, so we use that as the default.
+ module::set_var("gallery", "movie_extract_frame_time", 3);
+ module::set_version("gallery", $version = 53);
+ }
+
+ if ($version == 53) {
+ // In v54, we changed how we check for name and slug conflicts in Item_Model. Previously,
+ // we checked the whole filename. As a result, "foo.jpg" and "foo.png" were not considered
+ // conflicting if their slugs were different (a rare case in practice since server_add and
+ // uploader would give them both the same slug "foo"). Now, we check the filename without its
+ // extension. This upgrade stanza fixes any conflicts where they were previously allowed.
+
+ // This might be slow, but if it times out it can just pick up where it left off.
+
+ // Find and loop through each conflict (e.g. "foo.jpg", "foo.png", and "foo.flv" are one
+ // conflict; "bar.jpg", "bar.png", and "bar.flv" are another)
+ foreach (db::build()
+ ->select_distinct(array("parent_base_name" =>
+ db::expr("CONCAT(`parent_id`, ':', LOWER(SUBSTR(`name`, 1, LOCATE('.', `name`) - 1)))")))
+ ->select(array("C" => "COUNT(\"*\")"))
+ ->from("items")
+ ->where("type", "<>", "album")
+ ->having("C", ">", 1)
+ ->group_by("parent_base_name")
+ ->execute() as $conflict) {
+ list ($parent_id, $base_name) = explode(":", $conflict->parent_base_name, 2);
+ $base_name_escaped = Database::escape_for_like($base_name);
+ // Loop through the items for each conflict
+ foreach (db::build()
+ ->from("items")
+ ->select("id")
+ ->where("type", "<>", "album")
+ ->where("parent_id", "=", $parent_id)
+ ->where("name", "LIKE", "{$base_name_escaped}.%")
+ ->limit(1000000) // required to satisfy SQL syntax (no offset without limit)
+ ->offset(1) // skips the 0th item
+ ->execute() as $row) {
+ set_time_limit(30);
+ $item = ORM::factory("item", $row->id);
+ $item->name = $item->name; // this will force Item_Model to check for conflicts on save
+ $item->save();
+ }
+ }
+ module::set_version("gallery", $version = 54);
+ }
+
+ if ($version == 54) {
+ $db->query("ALTER TABLE {items} ADD KEY `relative_path_cache` (`relative_path_cache`)");
+ module::set_version("gallery", $version = 55);
+ }
+
+ if ($version == 55) {
+ // In v56, we added the ability to change the default behavior regarding movie uploads. It
+ // can be set to "always", "never", or "autodetect" to match the previous behavior where they
+ // are allowed only if FFmpeg is found.
+ module::set_var("gallery", "movie_allow_uploads", "autodetect");
+ module::set_version("gallery", $version = 56);
+ }
+
+ if ($version == 56) {
+ // Cleanup possible instances where resize_dirty of albums or movies was set to 0. This is
+ // unlikely to have occurred, and doesn't currently matter much since albums and movies don't
+ // have resize images anyway. However, it may be useful to be consistent here going forward.
+ db::build()
+ ->update("items")
+ ->set("resize_dirty", 1)
+ ->where("type", "<>", "photo")
+ ->execute();
+ module::set_version("gallery", $version = 57);
+ }
+ }
+
+ static function uninstall() {
+ $db = Database::instance();
+ $db->query("DROP TABLE IF EXISTS {access_caches}");
+ $db->query("DROP TABLE IF EXISTS {access_intents}");
+ $db->query("DROP TABLE IF EXISTS {graphics_rules}");
+ $db->query("DROP TABLE IF EXISTS {incoming_translations}");
+ $db->query("DROP TABLE IF EXISTS {failed_auths}");
+ $db->query("DROP TABLE IF EXISTS {items}");
+ $db->query("DROP TABLE IF EXISTS {logs}");
+ $db->query("DROP TABLE IF EXISTS {modules}");
+ $db->query("DROP TABLE IF EXISTS {outgoing_translations}");
+ $db->query("DROP TABLE IF EXISTS {permissions}");
+ $db->query("DROP TABLE IF EXISTS {sessions}");
+ $db->query("DROP TABLE IF EXISTS {tasks}");
+ $db->query("DROP TABLE IF EXISTS {themes}");
+ $db->query("DROP TABLE IF EXISTS {vars}");
+ $db->query("DROP TABLE IF EXISTS {caches}");
+ foreach (array("albums", "resizes", "thumbs", "uploads",
+ "modules", "logs", "database.php") as $entry) {
+ system("/bin/rm -rf " . VARPATH . $entry);
+ }
+ }
+
+ static function _protect_directory($dir) {
+ $fp = @fopen("$dir/.htaccess", "w+");
+ fwrite($fp, "DirectoryIndex .htaccess\nSetHandler Gallery_Security_Do_Not_Remove\n" .
+ "Options None\n<IfModule mod_rewrite.c>\nRewriteEngine off\n</IfModule>\n" .
+ "Order allow,deny\nDeny from all\n");
+ fclose($fp);
+ }
+}
diff --git a/modules/gallery/helpers/gallery_rss.php b/modules/gallery/helpers/gallery_rss.php
new file mode 100644
index 0000000..d6b3302
--- /dev/null
+++ b/modules/gallery/helpers/gallery_rss.php
@@ -0,0 +1,76 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+class gallery_rss_Core {
+ static function available_feeds($item, $tag) {
+ $feeds["gallery/latest"] = t("Latest photos and movies");
+
+ if ($item) {
+ $feed_item = $item -> is_album() ? $item : $item->parent();
+
+ $feeds["gallery/album/{$feed_item->id}"] =
+ t("%title photos and movies", array("title" => $feed_item->title));
+ }
+
+ return $feeds;
+ }
+
+ static function feed($feed_id, $offset, $limit, $id) {
+ $feed = new stdClass();
+ switch ($feed_id) {
+ case "latest":
+ $feed->items = ORM::factory("item")
+ ->viewable()
+ ->where("type", "<>", "album")
+ ->order_by("created", "DESC")
+ ->find_all($limit, $offset);
+
+ $all_items = ORM::factory("item")
+ ->viewable()
+ ->where("type", "<>", "album")
+ ->order_by("created", "DESC");
+
+ $feed->max_pages = ceil($all_items->find_all()->count() / $limit);
+ $feed->title = t("%site_title - Recent updates", array("site_title" => item::root()->title));
+ $feed->description = t("Recent updates");
+ return $feed;
+
+ case "album":
+ $item = ORM::factory("item", $id);
+ access::required("view", $item);
+
+ $feed->items = $item
+ ->viewable()
+ ->descendants($limit, $offset, array(array("type", "=", "photo")));
+ $feed->max_pages = ceil(
+ $item->viewable()->descendants_count(array(array("type", "=", "photo"))) / $limit);
+ if ($item->id == item::root()->id) {
+ $feed->title = html::purify($item->title);
+ } else {
+ $feed->title = t("%site_title - %item_title",
+ array("site_title" => item::root()->title,
+ "item_title" => $item->title));
+ }
+ $feed->description = nl2br(html::purify($item->description));
+
+ return $feed;
+ }
+ }
+}
diff --git a/modules/gallery/helpers/gallery_task.php b/modules/gallery/helpers/gallery_task.php
new file mode 100644
index 0000000..618cf8f
--- /dev/null
+++ b/modules/gallery/helpers/gallery_task.php
@@ -0,0 +1,826 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class gallery_task_Core {
+ const FIX_STATE_START_MPTT = 0;
+ const FIX_STATE_RUN_MPTT = 1;
+ const FIX_STATE_START_ALBUMS = 2;
+ const FIX_STATE_RUN_ALBUMS = 3;
+ const FIX_STATE_START_DUPE_SLUGS = 4;
+ const FIX_STATE_RUN_DUPE_SLUGS = 5;
+ const FIX_STATE_START_DUPE_NAMES = 6;
+ const FIX_STATE_RUN_DUPE_NAMES = 7;
+ const FIX_STATE_START_DUPE_BASE_NAMES = 8;
+ const FIX_STATE_RUN_DUPE_BASE_NAMES = 9;
+ const FIX_STATE_START_REBUILD_ITEM_CACHES = 10;
+ const FIX_STATE_RUN_REBUILD_ITEM_CACHES = 11;
+ const FIX_STATE_START_MISSING_ACCESS_CACHES = 12;
+ const FIX_STATE_RUN_MISSING_ACCESS_CACHES = 13;
+ const FIX_STATE_DONE = 14;
+
+ static function available_tasks() {
+ $dirty_count = graphics::find_dirty_images_query()->count_records();
+ $tasks = array();
+ $tasks[] = Task_Definition::factory()
+ ->callback("gallery_task::rebuild_dirty_images")
+ ->name(t("Rebuild Images"))
+ ->description($dirty_count ?
+ t2("You have one out of date photo",
+ "You have %count out of date photos",
+ $dirty_count)
+ : t("All your photos are up to date"))
+ ->severity($dirty_count ? log::WARNING : log::SUCCESS);
+
+ $tasks[] = Task_Definition::factory()
+ ->callback("gallery_task::update_l10n")
+ ->name(t("Update translations"))
+ ->description(t("Download new and updated translated strings"))
+ ->severity(log::SUCCESS);
+
+ $tasks[] = Task_Definition::factory()
+ ->callback("gallery_task::file_cleanup")
+ ->name(t("Remove old files"))
+ ->description(t("Remove expired files from the logs and tmp directory"))
+ ->severity(log::SUCCESS);
+
+ $tasks[] = Task_Definition::factory()
+ ->callback("gallery_task::fix")
+ ->name(t("Fix your Gallery"))
+ ->description(t("Fix a variety of problems that might cause your Gallery to act strangely. Requires maintenance mode."))
+ ->severity(log::SUCCESS);
+
+ return $tasks;
+ }
+
+ /**
+ * Task that rebuilds all dirty images.
+ * @param Task_Model the task
+ */
+ static function rebuild_dirty_images($task) {
+ $errors = array();
+ try {
+ // Choose the dirty images in a random order so that if we run this task multiple times
+ // concurrently each task is rebuilding different images simultaneously.
+ $result = graphics::find_dirty_images_query()->select("id")
+ ->select(db::expr("RAND() as r"))
+ ->order_by("r", "ASC")
+ ->execute();
+ $total_count = $task->get("total_count", $result->count());
+ $mode = $task->get("mode", "init");
+ if ($mode == "init") {
+ $task->set("total_count", $total_count);
+ $task->set("mode", "process");
+ batch::start();
+ }
+
+ $completed = $task->get("completed", 0);
+ $ignored = $task->get("ignored", array());
+
+ $i = 0;
+
+ // If there's no work left to do, skip to the end. This can happen if we resume a task long
+ // after the work got done in some other task.
+ if (!$result->count()) {
+ $completed = $total_count;
+ }
+
+ foreach ($result as $row) {
+ if (array_key_exists($row->id, $ignored)) {
+ continue;
+ }
+
+ $item = ORM::factory("item", $row->id);
+ if ($item->loaded()) {
+ try {
+ graphics::generate($item);
+ $completed++;
+
+ $errors[] = t("Successfully rebuilt images for '%title'",
+ array("title" => html::purify($item->title)));
+ } catch (Exception $e) {
+ $errors[] = t("Unable to rebuild images for '%title'",
+ array("title" => html::purify($item->title)));
+ $errors[] = (string)$e;
+ $ignored[$item->id] = 1;
+ }
+ }
+
+ if (++$i == 2) {
+ break;
+ }
+ }
+
+ $task->status = t2("Updated: 1 image. Total: %total_count.",
+ "Updated: %count images. Total: %total_count.",
+ $completed,
+ array("total_count" => $total_count));
+
+ if ($completed < $total_count) {
+ $task->percent_complete = (int)(100 * ($completed + count($ignored)) / $total_count);
+ } else {
+ $task->percent_complete = 100;
+ }
+
+ $task->set("completed", $completed);
+ $task->set("ignored", $ignored);
+ if ($task->percent_complete == 100) {
+ $task->done = true;
+ $task->state = "success";
+ batch::stop();
+ site_status::clear("graphics_dirty");
+ }
+ } catch (Exception $e) {
+ Kohana_Log::add("error",(string)$e);
+ $task->done = true;
+ $task->state = "error";
+ $task->status = $e->getMessage();
+ $errors[] = (string)$e;
+ }
+ if ($errors) {
+ $task->log($errors);
+ }
+ }
+
+ static function update_l10n($task) {
+ $errors = array();
+ try {
+ $start = microtime(true);
+ $data = Cache::instance()->get("update_l10n_cache:{$task->id}");
+ if ($data) {
+ list($dirs, $files, $cache, $num_fetched) = unserialize($data);
+ }
+ $i = 0;
+
+ switch ($task->get("mode", "init")) {
+ case "init": // 0%
+ $dirs = array("gallery", "modules", "themes", "installer");
+ $files = $cache = array();
+ $num_fetched = 0;
+ $task->set("mode", "find_files");
+ $task->status = t("Finding files");
+ break;
+
+ case "find_files": // 0% - 10%
+ while (($dir = array_pop($dirs)) && microtime(true) - $start < 0.5) {
+ if (in_array(basename($dir), array("tests", "lib"))) {
+ continue;
+ }
+
+ foreach (glob(DOCROOT . "$dir/*") as $path) {
+ $relative_path = str_replace(DOCROOT, "", $path);
+ if (is_dir($path)) {
+ $dirs[] = $relative_path;
+ } else {
+ $files[] = $relative_path;
+ }
+ }
+ }
+
+ $task->status = t2("Finding files: found 1 file",
+ "Finding files: found %count files", count($files));
+
+ if (!$dirs) {
+ $task->set("mode", "scan_files");
+ $task->set("total_files", count($files));
+ $task->status = t("Scanning files");
+ $task->percent_complete = 10;
+ }
+ break;
+
+ case "scan_files": // 10% - 70%
+ while (($file = array_pop($files)) && microtime(true) - $start < 0.5) {
+ $file = DOCROOT . $file;
+ switch (pathinfo($file, PATHINFO_EXTENSION)) {
+ case "php":
+ l10n_scanner::scan_php_file($file, $cache);
+ break;
+
+ case "info":
+ l10n_scanner::scan_info_file($file, $cache);
+ break;
+ }
+ }
+
+ $total_files = $task->get("total_files");
+ $task->status = t2("Scanning files: scanned 1 file",
+ "Scanning files: scanned %count files", $total_files - count($files));
+
+ $task->percent_complete = 10 + 60 * ($total_files - count($files)) / $total_files;
+ if (empty($files)) {
+ $task->set("mode", "fetch_updates");
+ $task->status = t("Fetching updates");
+ $task->percent_complete = 70;
+ }
+ break;
+
+ case "fetch_updates": // 70% - 100%
+ // Send fetch requests in batches until we're done
+ $num_remaining = l10n_client::fetch_updates($num_fetched);
+ if ($num_remaining) {
+ $total = $num_fetched + $num_remaining;
+ $task->percent_complete = 70 + 30 * ((float) $num_fetched / $total);
+ } else {
+ Gallery_I18n::clear_cache();
+
+ $task->done = true;
+ $task->state = "success";
+ $task->status = t("Translations installed/updated");
+ $task->percent_complete = 100;
+ }
+ }
+
+ if (!$task->done) {
+ Cache::instance()->set("update_l10n_cache:{$task->id}",
+ serialize(array($dirs, $files, $cache, $num_fetched)),
+ array("l10n"));
+ } else {
+ Cache::instance()->delete("update_l10n_cache:{$task->id}");
+ }
+ } catch (Exception $e) {
+ Kohana_Log::add("error",(string)$e);
+ $task->done = true;
+ $task->state = "error";
+ $task->status = $e->getMessage();
+ $errors[] = (string)$e;
+ }
+ if ($errors) {
+ $task->log($errors);
+ }
+ }
+
+ /**
+ * Task that removes old files from var/logs and var/tmp.
+ * @param Task_Model the task
+ */
+ static function file_cleanup($task) {
+ $errors = array();
+ try {
+ $start = microtime(true);
+ $data = Cache::instance()->get("file_cleanup_cache:{$task->id}");
+ $files = $data ? unserialize($data) : array();
+ $i = 0;
+ $current = 0;
+ $total = 0;
+
+ switch ($task->get("mode", "init")) {
+ case "init":
+ $threshold = time() - 1209600; // older than 2 weeks
+ // Note that this code is roughly duplicated in gallery_event::gallery_shutdown
+ foreach(array("logs", "tmp") as $dir) {
+ $dir = VARPATH . $dir;
+ if ($dh = opendir($dir)) {
+ while (($file = readdir($dh)) !== false) {
+ if ($file[0] == ".") {
+ continue;
+ }
+
+ // Ignore directories for now, but we should really address them in the long term.
+ if (is_dir("$dir/$file")) {
+ continue;
+ }
+
+ if (filemtime("$dir/$file") <= $threshold) {
+ $files[] = "$dir/$file";
+ }
+ }
+ }
+ }
+ $task->set("mode", "delete_files");
+ $task->set("current", 0);
+ $task->set("total", count($files));
+ Cache::instance()->set("file_cleanup_cache:{$task->id}", serialize($files),
+ array("file_cleanup"));
+ if (count($files) == 0) {
+ break;
+ }
+
+ case "delete_files":
+ $current = $task->get("current");
+ $total = $task->get("total");
+ while ($current < $total && microtime(true) - $start < 1) {
+ @unlink($files[$current]);
+ $task->log(t("%file removed", array("file" => $files[$current++])));
+ }
+ $task->percent_complete = $current / $total * 100;
+ $task->set("current", $current);
+ }
+
+ $task->status = t2("Removed: 1 file. Total: %total_count.",
+ "Removed: %count files. Total: %total_count.",
+ $current, array("total_count" => $total));
+
+ if ($total == $current) {
+ $task->done = true;
+ $task->state = "success";
+ $task->percent_complete = 100;
+ }
+ } catch (Exception $e) {
+ Kohana_Log::add("error",(string)$e);
+ $task->done = true;
+ $task->state = "error";
+ $task->status = $e->getMessage();
+ $errors[] = (string)$e;
+ }
+ if ($errors) {
+ $task->log($errors);
+ }
+ }
+
+ static function fix($task) {
+ $start = microtime(true);
+
+ $total = $task->get("total");
+ if (empty($total)) {
+ $item_count = db::build()->count_records("items");
+ $total = 0;
+
+ // mptt: 2 operations for every item
+ $total += 2 * $item_count;
+
+ // album audit (permissions and bogus album covers): 1 operation for every album
+ $total += db::build()->where("type", "=", "album")->count_records("items");
+
+ // one operation for each dupe slug, dupe name, dupe base name, and missing access cache
+ foreach (array("find_dupe_slugs", "find_dupe_names", "find_dupe_base_names",
+ "find_missing_access_caches") as $func) {
+ foreach (self::$func() as $row) {
+ $total++;
+ }
+ }
+
+ // one operation to rebuild path and url caches;
+ $total += 1 * $item_count;
+
+ $task->set("total", $total);
+ $task->set("state", $state = self::FIX_STATE_START_MPTT);
+ $task->set("ptr", 1);
+ $task->set("completed", 0);
+ }
+
+ $completed = $task->get("completed");
+ $state = $task->get("state");
+
+ if (!module::get_var("gallery", "maintenance_mode")) {
+ module::set_var("gallery", "maintenance_mode", 1);
+ }
+
+ // This is a state machine that checks each item in the database. It verifies the following
+ // attributes for an item.
+ // 1. Left and right MPTT pointers are correct
+ // 2. The .htaccess permission files for restricted items exist and are well formed.
+ // 3. The relative_path_cache and relative_url_cache values are set to null.
+ // 4. there are no album_cover_item_ids pointing to missing items
+ //
+ // We'll do a depth-first tree walk over our hierarchy using only the adjacency data because
+ // we don't trust MPTT here (that might be what we're here to fix!). Avoid avoid using ORM
+ // calls as much as possible since they're expensive.
+ //
+ // NOTE: the MPTT check will only traverse items that have valid parents. It's possible that
+ // we have some tree corruption where there are items with parent ids to non-existent items.
+ // We should probably do something about that.
+ while ($state != self::FIX_STATE_DONE && microtime(true) - $start < 1.5) {
+ switch ($state) {
+ case self::FIX_STATE_START_MPTT:
+ $task->set("ptr", $ptr = 1);
+ $task->set("stack", item::root()->id . "L1");
+ $state = self::FIX_STATE_RUN_MPTT;
+ break;
+
+ case self::FIX_STATE_RUN_MPTT:
+ $ptr = $task->get("ptr");
+ $stack = explode(" ", $task->get("stack"));
+ preg_match("/([0-9]+)([A-Z])([0-9]+)/", array_pop($stack), $matches); // e.g. "12345L10"
+ list ( , $id, $ptr_mode, $level) = $matches; // Skip the 0th entry of matches.
+ switch ($ptr_mode) {
+ case "L":
+ // Albums could be parent nodes.
+ $stack[] = "{$id}R{$level}";
+ db::build()
+ ->update("items")
+ ->set("left_ptr", $ptr++)
+ ->where("id", "=", $id)
+ ->execute();
+
+ $level++;
+ foreach (db::build()
+ ->select(array("id", "type"))
+ ->from("items")
+ ->where("parent_id", "=", $id)
+ ->order_by("left_ptr", "DESC") // DESC since array_pop effectively reverses them
+ ->execute() as $child) {
+ $stack[] = ($child->type == "album") ? "{$child->id}L{$level}" : "{$child->id}B{$level}";
+ }
+ $completed++;
+ break;
+ case "B":
+ // Non-albums must be leaf nodes.
+ db::build()
+ ->update("items")
+ ->set("left_ptr", $ptr++)
+ ->set("right_ptr", $ptr++)
+ ->set("level", $level)
+ ->set("relative_path_cache", null)
+ ->set("relative_url_cache", null)
+ ->where("id", "=", $id)
+ ->execute();
+ $completed += 2; // we updated two pointers
+ break;
+ case "R":
+ db::build()
+ ->update("items")
+ ->set("right_ptr", $ptr++)
+ ->set("level", $level)
+ ->set("relative_path_cache", null)
+ ->set("relative_url_cache", null)
+ ->where("id", "=", $id)
+ ->execute();
+ $completed++;
+ }
+ $task->set("ptr", $ptr);
+ $task->set("stack", implode(" ", $stack));
+
+ if (empty($stack)) {
+ $state = self::FIX_STATE_START_DUPE_SLUGS;
+ }
+ break;
+
+
+ case self::FIX_STATE_START_DUPE_SLUGS:
+ $stack = array();
+ foreach (self::find_dupe_slugs() as $row) {
+ list ($parent_id, $slug) = explode(":", $row->parent_slug, 2);
+ $stack[] = join(":", array($parent_id, $slug));
+ }
+ if ($stack) {
+ $task->set("stack", implode(" ", $stack));
+ $state = self::FIX_STATE_RUN_DUPE_SLUGS;
+ } else {
+ $state = self::FIX_STATE_START_DUPE_NAMES;
+ }
+ break;
+
+ case self::FIX_STATE_RUN_DUPE_SLUGS:
+ $stack = explode(" ", $task->get("stack"));
+ list ($parent_id, $slug) = explode(":", array_pop($stack));
+
+ // We want to leave the first one alone and update all conflicts to be random values.
+ $fixed = 0;
+ $conflicts = ORM::factory("item")
+ ->where("parent_id", "=", $parent_id)
+ ->where("slug", "=", $slug)
+ ->find_all(1, 1);
+ if ($conflicts->count() && $conflict = $conflicts->current()) {
+ $task->log("Fixing conflicting slug for item id {$conflict->id}");
+ db::build()
+ ->update("items")
+ ->set("slug", $slug . "-" . (string)rand(1000, 9999))
+ ->where("id", "=", $conflict->id)
+ ->execute();
+
+ // We fixed one conflict, but there might be more so put this parent back on the stack
+ // and try again. We won't consider it completed when we don't fix a conflict. This
+ // guarantees that we won't spend too long fixing one set of conflicts, and that we
+ // won't stop before all are fixed.
+ $stack[] = "$parent_id:$slug";
+ break;
+ }
+ $task->set("stack", implode(" ", $stack));
+ $completed++;
+
+ if (empty($stack)) {
+ $state = self::FIX_STATE_START_DUPE_NAMES;
+ }
+ break;
+
+ case self::FIX_STATE_START_DUPE_NAMES:
+ $stack = array();
+ foreach (self::find_dupe_names() as $row) {
+ list ($parent_id, $name) = explode(":", $row->parent_name, 2);
+ $stack[] = join(":", array($parent_id, $name));
+ }
+ if ($stack) {
+ $task->set("stack", implode(" ", $stack));
+ $state = self::FIX_STATE_RUN_DUPE_NAMES;
+ } else {
+ $state = self::FIX_STATE_START_DUPE_BASE_NAMES;
+ }
+ break;
+
+ case self::FIX_STATE_RUN_DUPE_NAMES:
+ // NOTE: This does *not* attempt to fix the file system!
+ $stack = explode(" ", $task->get("stack"));
+ list ($parent_id, $name) = explode(":", array_pop($stack));
+
+ $fixed = 0;
+ // We want to leave the first one alone and update all conflicts to be random values.
+ $conflicts = ORM::factory("item")
+ ->where("parent_id", "=", $parent_id)
+ ->where("name", "=", $name)
+ ->find_all(1, 1);
+ if ($conflicts->count() && $conflict = $conflicts->current()) {
+ $task->log("Fixing conflicting name for item id {$conflict->id}");
+ if (!$conflict->is_album() && preg_match("/^(.*)(\.[^\.\/]*?)$/", $conflict->name, $matches)) {
+ $item_base_name = $matches[1];
+ $item_extension = $matches[2]; // includes a leading dot
+ } else {
+ $item_base_name = $conflict->name;
+ $item_extension = "";
+ }
+ db::build()
+ ->update("items")
+ ->set("name", $item_base_name . "-" . (string)rand(1000, 9999) . $item_extension)
+ ->where("id", "=", $conflict->id)
+ ->execute();
+
+ // We fixed one conflict, but there might be more so put this parent back on the stack
+ // and try again. We won't consider it completed when we don't fix a conflict. This
+ // guarantees that we won't spend too long fixing one set of conflicts, and that we
+ // won't stop before all are fixed.
+ $stack[] = "$parent_id:$name";
+ break;
+ }
+ $task->set("stack", implode(" ", $stack));
+ $completed++;
+
+ if (empty($stack)) {
+ $state = self::FIX_STATE_START_DUPE_BASE_NAMES;
+ }
+ break;
+
+ case self::FIX_STATE_START_DUPE_BASE_NAMES:
+ $stack = array();
+ foreach (self::find_dupe_base_names() as $row) {
+ list ($parent_id, $base_name) = explode(":", $row->parent_base_name, 2);
+ $stack[] = join(":", array($parent_id, $base_name));
+ }
+ if ($stack) {
+ $task->set("stack", implode(" ", $stack));
+ $state = self::FIX_STATE_RUN_DUPE_BASE_NAMES;
+ } else {
+ $state = self::FIX_STATE_START_ALBUMS;
+ }
+ break;
+
+ case self::FIX_STATE_RUN_DUPE_BASE_NAMES:
+ // NOTE: This *does* attempt to fix the file system! So, it must go *after* run_dupe_names.
+ $stack = explode(" ", $task->get("stack"));
+ list ($parent_id, $base_name) = explode(":", array_pop($stack));
+ $base_name_escaped = Database::escape_for_like($base_name);
+
+ $fixed = 0;
+ // We want to leave the first one alone and update all conflicts to be random values.
+ $conflicts = ORM::factory("item")
+ ->where("parent_id", "=", $parent_id)
+ ->where("name", "LIKE", "{$base_name_escaped}.%")
+ ->where("type", "<>", "album")
+ ->find_all(1, 1);
+ if ($conflicts->count() && $conflict = $conflicts->current()) {
+ $task->log("Fixing conflicting name for item id {$conflict->id}");
+ if (preg_match("/^(.*)(\.[^\.\/]*?)$/", $conflict->name, $matches)) {
+ $item_base_name = $matches[1]; // unlike $base_name, this always maintains capitalization
+ $item_extension = $matches[2]; // includes a leading dot
+ } else {
+ $item_base_name = $conflict->name;
+ $item_extension = "";
+ }
+ // Unlike conflicts found in run_dupe_names, these items are likely to have an intact
+ // file system. Let's use the item save logic to rebuild the paths and rename the files
+ // if possible.
+ try {
+ $conflict->name = $item_base_name . "-" . (string)rand(1000, 9999) . $item_extension;
+ $conflict->validate();
+ // If we get here, we're safe to proceed with save
+ $conflict->save();
+ } catch (Exception $e) {
+ // Didn't work. Edit database directly without fixing file system.
+ db::build()
+ ->update("items")
+ ->set("name", $item_base_name . "-" . (string)rand(1000, 9999) . $item_extension)
+ ->where("id", "=", $conflict->id)
+ ->execute();
+ }
+
+ // We fixed one conflict, but there might be more so put this parent back on the stack
+ // and try again. We won't consider it completed when we don't fix a conflict. This
+ // guarantees that we won't spend too long fixing one set of conflicts, and that we
+ // won't stop before all are fixed.
+ $stack[] = "$parent_id:$base_name";
+ break;
+ }
+ $task->set("stack", implode(" ", $stack));
+ $completed++;
+
+ if (empty($stack)) {
+ $state = self::FIX_STATE_START_ALBUMS;
+ }
+ break;
+
+ case self::FIX_STATE_START_ALBUMS:
+ $stack = array();
+ foreach (db::build()
+ ->select("id")
+ ->from("items")
+ ->where("type", "=", "album")
+ ->execute() as $row) {
+ $stack[] = $row->id;
+ }
+ $task->set("stack", implode(" ", $stack));
+ $state = self::FIX_STATE_RUN_ALBUMS;
+ break;
+
+ case self::FIX_STATE_RUN_ALBUMS:
+ $stack = explode(" ", $task->get("stack"));
+ $id = array_pop($stack);
+
+ $item = ORM::factory("item", $id);
+ if ($item->album_cover_item_id) {
+ $album_cover_item = ORM::factory("item", $item->album_cover_item_id);
+ if (!$album_cover_item->loaded()) {
+ $item->album_cover_item_id = null;
+ $item->save();
+ }
+ }
+
+ $everybody = identity::everybody();
+ $view_col = "view_{$everybody->id}";
+ $view_full_col = "view_full_{$everybody->id}";
+ $intent = ORM::factory("access_intent")->where("item_id", "=", $id)->find();
+ if ($intent->$view_col === access::DENY) {
+ access::update_htaccess_files($item, $everybody, "view", access::DENY);
+ }
+ if ($intent->$view_full_col === access::DENY) {
+ access::update_htaccess_files($item, $everybody, "view_full", access::DENY);
+ }
+ $task->set("stack", implode(" ", $stack));
+ $completed++;
+
+ if (empty($stack)) {
+ $state = self::FIX_STATE_START_REBUILD_ITEM_CACHES;
+ }
+ break;
+
+ case self::FIX_STATE_START_REBUILD_ITEM_CACHES:
+ $stack = array();
+ foreach (self::find_empty_item_caches(500) as $row) {
+ $stack[] = $row->id;
+ }
+ $task->set("stack", implode(" ", $stack));
+ $state = self::FIX_STATE_RUN_REBUILD_ITEM_CACHES;
+ break;
+
+ case self::FIX_STATE_RUN_REBUILD_ITEM_CACHES:
+ $stack = explode(" ", $task->get("stack"));
+ if (!empty($stack)) {
+ $id = array_pop($stack);
+ $item = ORM::factory("item", $id);
+ $item->relative_path(); // this rebuilds the cache and saves the item as a side-effect
+ $task->set("stack", implode(" ", $stack));
+ $completed++;
+ }
+
+ if (empty($stack)) {
+ // Try refilling the stack
+ foreach (self::find_empty_item_caches(500) as $row) {
+ $stack[] = $row->id;
+ }
+ $task->set("stack", implode(" ", $stack));
+
+ if (empty($stack)) {
+ $state = self::FIX_STATE_START_MISSING_ACCESS_CACHES;
+ }
+ }
+ break;
+
+ case self::FIX_STATE_START_MISSING_ACCESS_CACHES:
+ $stack = array();
+ foreach (self::find_missing_access_caches_limited(500) as $row) {
+ $stack[] = $row->id;
+ }
+ $task->set("stack", implode(" ", $stack));
+ $state = self::FIX_STATE_RUN_MISSING_ACCESS_CACHES;
+ break;
+
+ case self::FIX_STATE_RUN_MISSING_ACCESS_CACHES:
+ $stack = array_filter(explode(" ", $task->get("stack"))); // filter removes empty/zero ids
+ if (!empty($stack)) {
+ $id = array_pop($stack);
+ $access_cache = ORM::factory("access_cache");
+ $access_cache->item_id = $id;
+ $access_cache->save();
+ $task->set("stack", implode(" ", $stack));
+ $completed++;
+ }
+
+ if (empty($stack)) {
+ // Try refilling the stack
+ foreach (self::find_missing_access_caches_limited(500) as $row) {
+ $stack[] = $row->id;
+ }
+ $task->set("stack", implode(" ", $stack));
+
+ if (empty($stack)) {
+ // The new cache rows are there, but they're incorrectly populated so we have to fix
+ // them. If this turns out to be too slow, we'll have to refactor
+ // access::recalculate_permissions to allow us to do it in slices.
+ access::recalculate_album_permissions(item::root());
+ $state = self::FIX_STATE_DONE;
+ }
+ }
+ break;
+ }
+ }
+
+ $task->set("state", $state);
+ $task->set("completed", $completed);
+
+ if ($state == self::FIX_STATE_DONE) {
+ $task->done = true;
+ $task->state = "success";
+ $task->percent_complete = 100;
+ module::set_var("gallery", "maintenance_mode", 0);
+ } else {
+ $task->percent_complete = round(100 * $completed / $total);
+ }
+ $task->status = t2("One operation complete", "%count / %total operations complete", $completed,
+ array("total" => $total));
+ }
+
+ static function find_dupe_slugs() {
+ return db::build()
+ ->select_distinct(
+ array("parent_slug" => db::expr("CONCAT(`parent_id`, ':', LOWER(`slug`))")))
+ ->select("id")
+ ->select(array("C" => "COUNT(\"*\")"))
+ ->from("items")
+ ->having("C", ">", 1)
+ ->group_by("parent_slug")
+ ->execute();
+ }
+
+ static function find_dupe_names() {
+ // looking for photos, movies, and albums
+ return db::build()
+ ->select_distinct(
+ array("parent_name" => db::expr("CONCAT(`parent_id`, ':', LOWER(`name`))")))
+ ->select("id")
+ ->select(array("C" => "COUNT(\"*\")"))
+ ->from("items")
+ ->having("C", ">", 1)
+ ->group_by("parent_name")
+ ->execute();
+ }
+
+ static function find_dupe_base_names() {
+ // looking for photos or movies, not albums
+ return db::build()
+ ->select_distinct(
+ array("parent_base_name" => db::expr("CONCAT(`parent_id`, ':', LOWER(SUBSTR(`name`, 1, LOCATE('.', `name`) - 1)))")))
+ ->select("id")
+ ->select(array("C" => "COUNT(\"*\")"))
+ ->from("items")
+ ->where("type", "<>", "album")
+ ->having("C", ">", 1)
+ ->group_by("parent_base_name")
+ ->execute();
+ }
+
+ static function find_empty_item_caches($limit) {
+ return db::build()
+ ->select("items.id")
+ ->from("items")
+ ->where("relative_path_cache", "is", null)
+ ->or_where("relative_url_cache", "is", null)
+ ->limit($limit)
+ ->execute();
+ }
+
+ static function find_missing_access_caches() {
+ return self::find_missing_access_caches_limited(1 << 16);
+ }
+
+ static function find_missing_access_caches_limited($limit) {
+ return db::build()
+ ->select("items.id")
+ ->from("items")
+ ->join("access_caches", "items.id", "access_caches.item_id", "left")
+ ->where("access_caches.id", "is", null)
+ ->limit($limit)
+ ->execute();
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/gallery_theme.php b/modules/gallery/helpers/gallery_theme.php
new file mode 100644
index 0000000..3c6d71e
--- /dev/null
+++ b/modules/gallery/helpers/gallery_theme.php
@@ -0,0 +1,151 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class gallery_theme_Core {
+ static function head($theme) {
+ $session = Session::instance();
+ $buf = "";
+ $buf .= $theme->css("gallery.css");
+ if ($session->get("debug")) {
+ $buf .= $theme->css("debug.css");
+ }
+
+ if (module::is_active("rss")) {
+ if ($item = $theme->item()) {
+ if ($item->is_album()) {
+ $buf .= rss::feed_link("gallery/album/{$item->id}");
+ } else {
+ $buf .= rss::feed_link("gallery/album/{$item->parent()->id}");
+ }
+ } else if ($tag = $theme->tag()) {
+ $buf .= rss::feed_link("tag/tag/{$tag->id}");
+ }
+ }
+
+ if (count(locales::installed())) {
+ // Needed by the languages block
+ $buf .= $theme->script("jquery.cookie.js");
+ }
+
+ if ($session->get("l10n_mode", false)) {
+ $buf .= $theme->css("l10n_client.css")
+ . $theme->script("jquery.cookie.js")
+ . $theme->script("l10n_client.js");
+ }
+
+ $buf .= $theme->css("uploadify/uploadify.css");
+ return $buf;
+ }
+
+ static function admin_head($theme) {
+ $buf = $theme->css("gallery.css");
+ $buf .= $theme->script("gallery.panel.js");
+ $session = Session::instance();
+ if ($session->get("debug")) {
+ $buf .= $theme->css("debug.css");
+ }
+
+ if ($session->get("l10n_mode", false)) {
+ $buf .= $theme->css("l10n_client.css");
+ $buf .= $theme->script("jquery.cookie.js");
+ $buf .= $theme->script("l10n_client.js");
+ }
+ return $buf;
+ }
+
+ static function page_bottom($theme) {
+ $session = Session::instance();
+ if (gallery::show_profiler()) {
+ Profiler::enable();
+ $profiler = new Profiler();
+ $profiler->render();
+ }
+ $content = "";
+ if ($session->get("l10n_mode", false)) {
+ $content .= L10n_Client_Controller::l10n_form();
+ }
+
+ if ($session->get_once("after_install")) {
+ $content .= new View("welcome_message_loader.html");
+ }
+
+ if (identity::active_user()->admin && upgrade_checker::should_auto_check()) {
+ $content .= '<script type="text/javascript">
+ $.ajax({url: "' . url::site("admin/upgrade_checker/check_now?csrf=" .
+ access::csrf_token()) . '"});
+ </script>';
+ }
+ return $content;
+ }
+
+ static function admin_page_bottom($theme) {
+ $session = Session::instance();
+ if (gallery::show_profiler()) {
+ Profiler::enable();
+ $profiler = new Profiler();
+ $profiler->render();
+ }
+
+ // Redirect to the root album when the admin session expires.
+ $content = '<script type="text/javascript">
+ var adminReauthCheck = function() {
+ $.ajax({url: "' . url::site("admin?reauth_check=1") . '",
+ dataType: "json",
+ success: function(data){
+ if ("location" in data) {
+ document.location = data.location;
+ }
+ }});
+ };
+ setInterval("adminReauthCheck();", 60 * 1000);
+ </script>';
+
+ if (upgrade_checker::should_auto_check()) {
+ $content .= '<script type="text/javascript">
+ $.ajax({url: "' . url::site("admin/upgrade_checker/check_now?csrf=" .
+ access::csrf_token()) . '"});
+ </script>';
+ }
+
+ if ($session->get("l10n_mode", false)) {
+ $content .= "\n" . L10n_Client_Controller::l10n_form();
+ }
+ return $content;
+ }
+
+ static function credits() {
+ $version_string = SafeString::of_safe_html(
+ '<bdo dir="ltr">Gallery ' . gallery::version_string() . '</bdo>');
+ return "<li class=\"g-first\">" .
+ t(module::get_var("gallery", "credits"),
+ array("url" => "http://galleryproject.org",
+ "gallery_version" => $version_string)) .
+ "</li>";
+ }
+
+ static function admin_credits() {
+ return gallery_theme::credits();
+ }
+
+ static function body_attributes() {
+ if (locales::is_rtl()) {
+ return 'class="rtl"';
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/graphics.php b/modules/gallery/helpers/graphics.php
new file mode 100644
index 0000000..459784c
--- /dev/null
+++ b/modules/gallery/helpers/graphics.php
@@ -0,0 +1,546 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class graphics_Core {
+ private static $init;
+ private static $_rules_cache = array();
+
+ /**
+ * Add a new graphics rule.
+ *
+ * Rules are applied to targets (thumbnails and resizes) in priority order. Rules are functions
+ * in the graphics class. So for example, the following rule:
+ *
+ * graphics::add_rule("gallery", "thumb", "gallery_graphics::resize",
+ * array("width" => 200, "height" => 200, "master" => Image::AUTO), 100);
+ *
+ * Specifies that "gallery" is adding a rule to resize thumbnails down to a max of 200px on
+ * the longest side. The gallery module adds default rules at a priority of 100. You can set
+ * higher and lower priorities to perform operations before or after this fires.
+ *
+ * @param string $module_name the module that added the rule
+ * @param string $target the target for this operation ("thumb" or "resize")
+ * @param string $operation the name of the operation (<defining class>::method)
+ * @param array $args arguments to the operation
+ * @param integer $priority the priority for this rule (lower priorities are run first)
+ */
+ static function add_rule($module_name, $target, $operation, $args, $priority) {
+ $rule = ORM::factory("graphics_rule");
+ $rule->module_name = $module_name;
+ $rule->target = $target;
+ $rule->operation = $operation;
+ $rule->priority = $priority;
+ $rule->args = serialize($args);
+ $rule->active = true;
+ $rule->save();
+
+ graphics::mark_dirty($target == "thumb", $target == "resize");
+ }
+
+ /**
+ * Remove any matching graphics rules
+ * @param string $module_name the module that added the rule
+ * @param string $target the target for this operation ("thumb" or "resize")
+ * @param string $operation the name of the operation(<defining class>::method)
+ */
+ static function remove_rule($module_name, $target, $operation) {
+ db::build()
+ ->delete("graphics_rules")
+ ->where("module_name", "=", $module_name)
+ ->where("target", "=", $target)
+ ->where("operation", "=", $operation)
+ ->execute();
+
+ graphics::mark_dirty($target == "thumb", $target == "resize");
+ }
+
+ /**
+ * Remove all rules for this module
+ * @param string $module_name
+ */
+ static function remove_rules($module_name) {
+ $status = db::build()
+ ->delete("graphics_rules")
+ ->where("module_name", "=", $module_name)
+ ->execute();
+ if (count($status)) {
+ graphics::mark_dirty(true, true);
+ }
+ }
+
+ /**
+ * Activate the rules for this module, typically done when the module itself is deactivated.
+ * Note that this does not mark images as dirty so that if you deactivate and reactivate a
+ * module it won't cause all of your images to suddenly require a rebuild.
+ */
+ static function activate_rules($module_name) {
+ db::build()
+ ->update("graphics_rules")
+ ->set("active", true)
+ ->where("module_name", "=", $module_name)
+ ->execute();
+ }
+
+ /**
+ * Deactivate the rules for this module, typically done when the module itself is deactivated.
+ * Note that this does not mark images as dirty so that if you deactivate and reactivate a
+ * module it won't cause all of your images to suddenly require a rebuild.
+ */
+ static function deactivate_rules($module_name) {
+ db::build()
+ ->update("graphics_rules")
+ ->set("active", false)
+ ->where("module_name", "=", $module_name)
+ ->execute();
+ }
+
+ /**
+ * Rebuild the thumb and resize for the given item.
+ * @param Item_Model $item
+ */
+ static function generate($item) {
+ if ($item->thumb_dirty) {
+ $ops["thumb"] = $item->thumb_path();
+ }
+ if ($item->resize_dirty && $item->is_photo()) {
+ $ops["resize"] = $item->resize_path();
+ }
+
+ try {
+ foreach ($ops as $target => $output_file) {
+ $working_file = $item->file_path();
+ // Delete anything that might already be there
+ @unlink($output_file);
+ switch ($item->type) {
+ case "movie":
+ // Run movie_extract_frame events, which can either:
+ // - generate an output file, bypassing the ffmpeg-based movie::extract_frame
+ // - add to the options sent to movie::extract_frame (e.g. change frame extract time,
+ // add de-interlacing arguments to ffmpeg... see movie helper for more info)
+ // Note that the args are similar to those of the events in gallery_graphics
+ $movie_options_wrapper = new stdClass();
+ $movie_options_wrapper->movie_options = array();
+ module::event("movie_extract_frame", $working_file, $output_file,
+ $movie_options_wrapper, $item);
+ // If no output_file generated by events, run movie::extract_frame with movie_options
+ clearstatcache();
+ if (@filesize($output_file) == 0) {
+ try {
+ movie::extract_frame($working_file, $output_file, $movie_options_wrapper->movie_options);
+ // If we're here, we know ffmpeg is installed and the movie is valid. Because the
+ // user may not always have had ffmpeg installed, the movie's width, height, and
+ // mime type may need updating. Let's use this opportunity to make sure they're
+ // correct. It's not optimal to do it at this low level, but it's not trivial to find
+ // these cases quickly in an upgrade script.
+ list ($width, $height, $mime_type) = movie::get_file_metadata($working_file);
+ // Only set them if they need updating to avoid marking them as "changed"
+ if (($item->width != $width) || ($item->height != $height) ||
+ ($item->mime_type != $mime_type)) {
+ $item->width = $width;
+ $item->height = $height;
+ $item->mime_type = $mime_type;
+ }
+ } catch (Exception $e) {
+ // Didn't work, likely because of MISSING_FFMPEG - use placeholder
+ graphics::_replace_image_with_placeholder($item, $target);
+ break;
+ }
+ }
+ $working_file = $output_file;
+
+ case "photo":
+ // Run the graphics rules (for both movies and photos)
+ foreach (self::_get_rules($target) as $rule) {
+ $args = array($working_file, $output_file, unserialize($rule->args), $item);
+ call_user_func_array($rule->operation, $args);
+ $working_file = $output_file;
+ }
+ break;
+
+ case "album":
+ if (!$cover = $item->album_cover()) {
+ // This album has no cover; copy its placeholder image. Because of an old bug, it's
+ // possible that there's an album cover item id that points to an invalid item. In that
+ // case, just null out the album cover item id. It's not optimal to do that at this low
+ // level, but it's not trivial to find these cases quickly in an upgrade script and if we
+ // don't do this, the album may be permanently marked as "needs rebuilding"
+ //
+ // ref: http://sourceforge.net/apps/trac/gallery/ticket/1172
+ // http://galleryproject.org/node/96926
+ if ($item->album_cover_item_id) {
+ $item->album_cover_item_id = null;
+ $item->save();
+ }
+ graphics::_replace_image_with_placeholder($item, $target);
+ break;
+ }
+ if ($cover->thumb_dirty) {
+ graphics::generate($cover);
+ }
+ if (!$cover->thumb_dirty) {
+ // Make the album cover from the cover item's thumb. Run gallery_graphics::resize with
+ // null options and it will figure out if this is a direct copy or conversion to jpg.
+ $working_file = $cover->thumb_path();
+ gallery_graphics::resize($working_file, $output_file, null, $item);
+ }
+ break;
+ }
+ }
+
+ if (!empty($ops["thumb"])) {
+ if (file_exists($item->thumb_path())) {
+ $item->thumb_dirty = 0;
+ } else {
+ Kohana_Log::add("error", "Failed to rebuild thumb image: $item->title");
+ graphics::_replace_image_with_placeholder($item, "thumb");
+ }
+ }
+
+ if (!empty($ops["resize"])) {
+ if (file_exists($item->resize_path())) {
+ $item->resize_dirty = 0;
+ } else {
+ Kohana_Log::add("error", "Failed to rebuild resize image: $item->title");
+ graphics::_replace_image_with_placeholder($item, "resize");
+ }
+ }
+ graphics::_update_item_dimensions($item);
+ $item->save();
+ } catch (Exception $e) {
+ // Something went wrong rebuilding the image. Replace with the placeholder images,
+ // leave it dirty and move on.
+ Kohana_Log::add("error", "Caught exception rebuilding images: {$item->title}\n" .
+ $e->getMessage() . "\n" . $e->getTraceAsString());
+ if ($item->is_photo()) {
+ graphics::_replace_image_with_placeholder($item, "resize");
+ }
+ graphics::_replace_image_with_placeholder($item, "thumb");
+ try {
+ graphics::_update_item_dimensions($item);
+ } catch (Exception $e) {
+ // Looks like get_file_metadata couldn't identify our placeholders. We should never get
+ // here, but in the odd case we do, we need to do something. Let's put in hardcoded values.
+ if ($item->is_photo()) {
+ list ($item->resize_width, $item->resize_height) = array(200, 200);
+ }
+ list ($item->thumb_width, $item->thumb_height) = array(200, 200);
+ }
+ $item->save();
+ throw $e;
+ }
+ }
+
+ private static function _update_item_dimensions($item) {
+ if ($item->is_photo()) {
+ list ($item->resize_width, $item->resize_height) =
+ photo::get_file_metadata($item->resize_path());
+ }
+ list ($item->thumb_width, $item->thumb_height) =
+ photo::get_file_metadata($item->thumb_path());
+ }
+
+ private static function _replace_image_with_placeholder($item, $target) {
+ if ($item->is_album() && !$item->album_cover_item_id) {
+ $input_path = MODPATH . "gallery/images/missing_album_cover.jpg";
+ } else if ($item->is_movie() || ($item->is_album() && $item->album_cover()->is_movie())) {
+ $input_path = MODPATH . "gallery/images/missing_movie.jpg";
+ } else {
+ $input_path = MODPATH . "gallery/images/missing_photo.jpg";
+ }
+
+ if ($target == "thumb") {
+ $output_path = $item->thumb_path();
+ $size = module::get_var("gallery", "thumb_size", 200);
+ } else {
+ $output_path = $item->resize_path();
+ $size = module::get_var("gallery", "resize_size", 640);
+ }
+ $options = array("width" => $size, "height" => $size, "master" => Image::AUTO);
+
+ try {
+ // Copy/convert/resize placeholder as needed.
+ gallery_graphics::resize($input_path, $output_path, $options, null);
+ } catch (Exception $e) {
+ // Copy/convert/resize didn't work. Add to the log and copy the jpg version (which could have
+ // a non-jpg extension). This is less than ideal, but it's better than putting nothing
+ // there and causing theme views to act strangely because a file is missing.
+ // @todo we should handle this better.
+ Kohana_Log::add("error", "Caught exception converting placeholder for missing image: " .
+ $item->title . "\n" . $e->getMessage() . "\n" . $e->getTraceAsString());
+ copy($input_path, $output_path);
+ }
+
+ if (!file_exists($output_path)) {
+ // Copy/convert/resize didn't throw an exception, but still didn't work - do the same as above.
+ // @todo we should handle this better.
+ Kohana_Log::add("error", "Failed to convert placeholder for missing image: $item->title");
+ copy($input_path, $output_path);
+ }
+ }
+
+ private static function _get_rules($target) {
+ if (empty(self::$_rules_cache[$target])) {
+ $rules = array();
+ foreach (ORM::factory("graphics_rule")
+ ->where("target", "=", $target)
+ ->where("active", "=", true)
+ ->order_by("priority", "asc")
+ ->find_all() as $rule) {
+ $rules[] = (object)$rule->as_array();
+ }
+ self::$_rules_cache[$target] = $rules;
+ }
+ return self::$_rules_cache[$target];
+ }
+
+ /**
+ * Return a query result that locates all items with dirty images.
+ * @return Database_Result Query result
+ */
+ static function find_dirty_images_query() {
+ return db::build()
+ ->from("items")
+ ->and_open()
+ ->where("thumb_dirty", "=", 1)
+ ->and_open()
+ ->where("type", "<>", "album")
+ ->or_where("album_cover_item_id", "IS NOT", null)
+ ->close()
+ ->or_open()
+ ->where("resize_dirty", "=", 1)
+ ->where("type", "=", "photo")
+ ->close()
+ ->close();
+ }
+
+ /**
+ * Mark thumbnails and resizes as dirty. They will have to be rebuilt. Optionally, only those of
+ * a specified type and/or mime type can be marked (e.g. $type="movie" to rebuild movies only).
+ */
+ static function mark_dirty($thumbs, $resizes, $type=null, $mime_type=null) {
+ if ($thumbs || $resizes) {
+ $db = db::build()
+ ->update("items");
+ if ($type) {
+ $db->where("type", "=", $type);
+ }
+ if ($mime_type) {
+ $db->where("mime_type", "=", $mime_type);
+ }
+ if ($thumbs) {
+ $db->set("thumb_dirty", 1);
+ }
+ if ($resizes) {
+ $db->set("resize_dirty", 1);
+ }
+ $db->execute();
+ }
+
+ $count = graphics::find_dirty_images_query()->count_records();
+ if ($count) {
+ site_status::warning(
+ t2("One of your photos is out of date. <a %attrs>Click here to fix it</a>",
+ "%count of your photos are out of date. <a %attrs>Click here to fix them</a>",
+ $count,
+ array("attrs" => html::mark_clean(sprintf(
+ 'href="%s" class="g-dialog-link"',
+ url::site("admin/maintenance/start/gallery_task::rebuild_dirty_images?csrf=__CSRF__"))))),
+ "graphics_dirty");
+ }
+ }
+
+ /**
+ * Detect which graphics toolkits are available on this system. Return an array of key value
+ * pairs where the key is one of gd, imagemagick, graphicsmagick and the value is information
+ * about that toolkit. For GD we return the version string, and for ImageMagick and
+ * GraphicsMagick we return the path to the directory containing the appropriate binaries.
+ */
+ static function detect_toolkits() {
+ $toolkits = new stdClass();
+ $toolkits->gd = new stdClass();
+ $toolkits->imagemagick = new stdClass();
+ $toolkits->graphicsmagick = new stdClass();
+
+ // GD is special, it doesn't use exec()
+ $gd = function_exists("gd_info") ? gd_info() : array();
+ $toolkits->gd->name = "GD";
+ if (!isset($gd["GD Version"])) {
+ $toolkits->gd->installed = false;
+ $toolkits->gd->error = t("GD is not installed");
+ } else {
+ $toolkits->gd->installed = true;
+ $toolkits->gd->version = $gd["GD Version"];
+ $toolkits->gd->rotate = function_exists("imagerotate");
+ $toolkits->gd->sharpen = function_exists("imageconvolution");
+ $toolkits->gd->binary = "";
+ $toolkits->gd->dir = "";
+
+ if (!$toolkits->gd->rotate && !$toolkits->gd->sharpen) {
+ $toolkits->gd->error =
+ t("You have GD version %version, but it lacks image rotation and sharpening.",
+ array("version" => $gd["GD Version"]));
+ } else if (!$toolkits->gd->rotate) {
+ $toolkits->gd->error =
+ t("You have GD version %version, but it lacks image rotation.",
+ array("version" => $gd["GD Version"]));
+ } else if (!$toolkits->gd->sharpen) {
+ $toolkits->gd->error =
+ t("You have GD version %version, but it lacks image sharpening.",
+ array("version" => $gd["GD Version"]));
+ }
+ }
+
+ if (!function_exists("exec")) {
+ $toolkits->imagemagick->installed = false;
+ $toolkits->imagemagick->error = t("ImageMagick requires the <b>exec</b> function");
+
+ $toolkits->graphicsmagick->installed = false;
+ $toolkits->graphicsmagick->error = t("GraphicsMagick requires the <b>exec</b> function");
+ } else {
+ // ImageMagick & GraphicsMagick
+ $magick_kits = array(
+ "imagemagick" => array(
+ "name" => "ImageMagick", "binary" => "convert", "version_arg" => "-version",
+ "version_regex" => "/Version: \S+ (\S+)/"),
+ "graphicsmagick" => array(
+ "name" => "GraphicsMagick", "binary" => "gm", "version_arg" => "version",
+ "version_regex" => "/\S+ (\S+)/"));
+ // Loop through the kits
+ foreach ($magick_kits as $index => $settings) {
+ $path = system::find_binary(
+ $settings["binary"], module::get_var("gallery", "graphics_toolkit_path"));
+ $toolkits->$index->name = $settings["name"];
+ if ($path) {
+ if (@is_file($path) &&
+ preg_match(
+ $settings["version_regex"], shell_exec($path . " " . $settings["version_arg"]), $matches)) {
+ $version = $matches[1];
+
+ $toolkits->$index->installed = true;
+ $toolkits->$index->version = $version;
+ $toolkits->$index->binary = $path;
+ $toolkits->$index->dir = dirname($path);
+ $toolkits->$index->rotate = true;
+ $toolkits->$index->sharpen = true;
+ } else {
+ $toolkits->$index->installed = false;
+ $toolkits->$index->error =
+ t("%toolkit_name is installed, but PHP's open_basedir restriction prevents Gallery from using it.",
+ array("toolkit_name" => $settings["name"]));
+ }
+ } else {
+ $toolkits->$index->installed = false;
+ $toolkits->$index->error =
+ t("We could not locate %toolkit_name on your system.",
+ array("toolkit_name" => $settings["name"]));
+ }
+ }
+ }
+
+ return $toolkits;
+ }
+
+ /**
+ * This needs to be run once, after the initial install, to choose a graphics toolkit.
+ */
+ static function choose_default_toolkit() {
+ // Detect a graphics toolkit
+ $toolkits = graphics::detect_toolkits();
+ foreach (array("imagemagick", "graphicsmagick", "gd") as $tk) {
+ if ($toolkits->$tk->installed) {
+ module::set_var("gallery", "graphics_toolkit", $tk);
+ module::set_var("gallery", "graphics_toolkit_path", $toolkits->$tk->dir);
+ break;
+ }
+ }
+
+ if (!module::get_var("gallery", "graphics_toolkit")) {
+ site_status::warning(
+ t("Graphics toolkit missing! Please <a href=\"%url\">choose a toolkit</a>",
+ array("url" => html::mark_clean(url::site("admin/graphics")))),
+ "missing_graphics_toolkit");
+ }
+ }
+
+ /**
+ * Choose which driver the Kohana Image library uses.
+ */
+ static function init_toolkit() {
+ if (self::$init) {
+ return;
+ }
+ switch(module::get_var("gallery", "graphics_toolkit")) {
+ case "gd":
+ Kohana_Config::instance()->set("image.driver", "GD");
+ break;
+
+ case "imagemagick":
+ Kohana_Config::instance()->set("image.driver", "ImageMagick");
+ Kohana_Config::instance()->set(
+ "image.params.directory", module::get_var("gallery", "graphics_toolkit_path"));
+ break;
+
+ case "graphicsmagick":
+ Kohana_Config::instance()->set("image.driver", "GraphicsMagick");
+ Kohana_Config::instance()->set(
+ "image.params.directory", module::get_var("gallery", "graphics_toolkit_path"));
+ break;
+ }
+
+ self::$init = 1;
+ }
+
+ /**
+ * Verify that a specific graphics function is available with the active toolkit.
+ * @param string $func (eg rotate, sharpen)
+ * @return boolean
+ */
+ static function can($func) {
+ if (module::get_var("gallery", "graphics_toolkit") == "gd") {
+ switch ($func) {
+ case "rotate":
+ return function_exists("imagerotate");
+
+ case "sharpen":
+ return function_exists("imageconvolution");
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Return the max file size that this graphics toolkit can handle.
+ */
+ static function max_filesize() {
+ if (module::get_var("gallery", "graphics_toolkit") == "gd") {
+ $memory_limit = trim(ini_get("memory_limit"));
+ $memory_limit_bytes = num::convert_to_bytes($memory_limit);
+
+ // GD expands images in memory and uses 4 bytes of RAM for every byte
+ // in the file.
+ $max_filesize = $memory_limit_bytes / 4;
+ $max_filesize_human_readable = num::convert_to_human_readable($max_filesize);
+ return array($max_filesize, $max_filesize_human_readable);
+ }
+
+ // Some arbitrarily large size
+ return array(1000000000, "1G");
+ }
+}
diff --git a/modules/gallery/helpers/identity.php b/modules/gallery/helpers/identity.php
new file mode 100644
index 0000000..5fc0f2f
--- /dev/null
+++ b/modules/gallery/helpers/identity.php
@@ -0,0 +1,247 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+class identity_Core {
+ protected static $available;
+
+ /**
+ * Return a list of installed Identity Drivers.
+ *
+ * @return boolean true if the driver supports updates; false if read only
+ */
+ static function providers() {
+ if (empty(self::$available)) {
+ $drivers = new ArrayObject(array(), ArrayObject::ARRAY_AS_PROPS);
+ foreach (module::available() as $module_name => $module) {
+ if (file_exists(MODPATH . "{$module_name}/config/identity.php")) {
+ $drivers->$module_name = $module->description;
+ }
+ }
+ self::$available = $drivers;
+ }
+ return self::$available;
+ }
+
+ /**
+ * Frees the current instance of the identity provider so the next call to instance will reload
+ *
+ * @param string configuration
+ * @return Identity_Core
+ */
+ static function reset() {
+ IdentityProvider::reset();
+ }
+
+ /**
+ * Make sure that we have a session and group_ids cached in the session.
+ */
+ static function load_user() {
+ try {
+ // Call IdentityProvider::instance() now to force the load of the user interface classes.
+ // We are about to load the active user from the session and which needs the user definition
+ // class, which can't be reached by Kohana's heiracrchical lookup.
+ IdentityProvider::instance();
+
+ $session = Session::instance();
+ if (!($user = $session->get("user"))) {
+ identity::set_active_user($user = identity::guest());
+ }
+
+ // The installer cannot set a user into the session, so it just sets an id which we should
+ // upconvert into a user.
+ // @todo set the user name into the session instead of 2 and then use it to get the
+ // user object
+ if ($user === 2) {
+ $session->delete("user"); // delete it so that identity code isn't confused by the integer
+ auth::login(IdentityProvider::instance()->admin_user());
+ }
+
+ // Cache the group ids for a day to trade off performance for security updates.
+ if (!$session->get("group_ids") || $session->get("group_ids_timeout", 0) < time()) {
+ $ids = array();
+ foreach ($user->groups() as $group) {
+ $ids[] = $group->id;
+ }
+ $session->set("group_ids", $ids);
+ $session->set("group_ids_timeout", time() + 86400);
+ }
+ } catch (Exception $e) {
+ // Log it, so we at least have so notification that we swallowed the exception.
+ Kohana_Log::add("error", "load_user Exception: " .
+ $e->getMessage() . "\n" . $e->getTraceAsString());
+ try {
+ Session::instance()->destroy();
+ } catch (Exception $e) {
+ // We don't care if there was a problem destroying the session.
+ }
+ url::redirect(item::root()->abs_url());
+ }
+ }
+
+ /**
+ * Return the array of group ids this user belongs to
+ *
+ * @return array
+ */
+ static function group_ids_for_active_user() {
+ return Session::instance()->get("group_ids", array(1));
+ }
+
+ /**
+ * Return the active user. If there's no active user, return the guest user.
+ *
+ * @return User_Definition
+ */
+ static function active_user() {
+ // @todo (maybe) cache this object so we're not always doing session lookups.
+ $user = Session::instance()->get("user", null);
+ if (!isset($user)) {
+ // Don't do this as a fallback in the Session::get() call because it can trigger unnecessary
+ // work.
+ $user = identity::guest();
+ }
+ return $user;
+ }
+
+ /**
+ * Change the active user.
+ * @param User_Definition $user
+ */
+ static function set_active_user($user) {
+ $session = Session::instance();
+ $session->set("user", $user);
+ $session->delete("group_ids");
+ identity::load_user();
+ }
+
+ /**
+ * Determine if if the current driver supports updates.
+ *
+ * @return boolean true if the driver supports updates; false if read only
+ */
+ static function is_writable() {
+ return IdentityProvider::instance()->is_writable();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::guest.
+ */
+ static function guest() {
+ return IdentityProvider::instance()->guest();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::admin_user.
+ */
+ static function admin_user() {
+ return IdentityProvider::instance()->admin_user();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::create_user.
+ */
+ static function create_user($name, $full_name, $password, $email) {
+ return IdentityProvider::instance()->create_user($name, $full_name, $password, $email);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::is_correct_password.
+ */
+ static function is_correct_password($user, $password) {
+ return IdentityProvider::instance()->is_correct_password($user, $password);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::lookup_user.
+ */
+ static function lookup_user($id) {
+ return IdentityProvider::instance()->lookup_user($id);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::lookup_user_by_name.
+ */
+ static function lookup_user_by_name($name) {
+ return IdentityProvider::instance()->lookup_user_by_name($name);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::create_group.
+ */
+ static function create_group($name) {
+ return IdentityProvider::instance()->create_group($name);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::everybody.
+ */
+ static function everybody() {
+ return IdentityProvider::instance()->everybody();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::registered_users.
+ */
+ static function registered_users() {
+ return IdentityProvider::instance()->registered_users();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::lookup_group.
+ */
+ static function lookup_group($id) {
+ return IdentityProvider::instance()->lookup_group($id);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::lookup_group_by_name.
+ */
+ static function lookup_group_by_name($name) {
+ return IdentityProvider::instance()->lookup_group_by_name($name);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::get_user_list.
+ */
+ static function get_user_list($ids) {
+ return IdentityProvider::instance()->get_user_list($ids);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::groups.
+ */
+ static function groups() {
+ return IdentityProvider::instance()->groups();
+ }
+
+ /**
+ * @see IdentityProvider_Driver::add_user_to_group.
+ */
+ static function add_user_to_group($user, $group) {
+ return IdentityProvider::instance()->add_user_to_group($user, $group);
+ }
+
+ /**
+ * @see IdentityProvider_Driver::remove_user_to_group.
+ */
+ static function remove_user_from_group($user, $group) {
+ return IdentityProvider::instance()->remove_user_from_group($user, $group);
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/item.php b/modules/gallery/helpers/item.php
new file mode 100644
index 0000000..9882a9c
--- /dev/null
+++ b/modules/gallery/helpers/item.php
@@ -0,0 +1,433 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class item_Core {
+ static function move($source, $target) {
+ access::required("view", $source);
+ access::required("view", $target);
+ access::required("edit", $source);
+ access::required("edit", $target);
+
+ $parent = $source->parent();
+ if ($parent->album_cover_item_id == $source->id) {
+ if ($parent->children_count() > 1) {
+ foreach ($parent->children(2) as $child) {
+ if ($child->id != $source->id) {
+ $new_cover_item = $child;
+ break;
+ }
+ }
+ item::make_album_cover($new_cover_item);
+ } else {
+ item::remove_album_cover($parent);
+ }
+ }
+
+ $orig_name = $source->name;
+ $source->parent_id = $target->id;
+ $source->save();
+ if ($orig_name != $source->name) {
+ switch ($source->type) {
+ case "album":
+ message::info(
+ t("Album <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict",
+ array("old_name" => $orig_name, "new_name" => $source->name)));
+ break;
+
+ case "photo":
+ message::info(
+ t("Photo <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict",
+ array("old_name" => $orig_name, "new_name" => $source->name)));
+ break;
+
+ case "movie":
+ message::info(
+ t("Movie <b>%old_name</b> renamed to <b>%new_name</b> to avoid a conflict",
+ array("old_name" => $orig_name, "new_name" => $source->name)));
+ break;
+ }
+ }
+
+ // If the target has no cover item, make this it.
+ if ($target->album_cover_item_id == null) {
+ item::make_album_cover($source);
+ }
+ }
+
+ static function make_album_cover($item) {
+ $parent = $item->parent();
+ access::required("view", $item);
+ access::required("view", $parent);
+ access::required("edit", $parent);
+
+ $old_album_cover_id = $parent->album_cover_item_id;
+
+ model_cache::clear();
+ $parent->album_cover_item_id = $item->is_album() ? $item->album_cover_item_id : $item->id;
+ $parent->save();
+ graphics::generate($parent);
+
+ // Walk up the parent hierarchy and set album covers if necessary
+ $grand_parent = $parent->parent();
+ if ($grand_parent && access::can("edit", $grand_parent) &&
+ $grand_parent->album_cover_item_id == null) {
+ item::make_album_cover($parent);
+ }
+
+ // When albums are album covers themselves, we hotlink directly to the target item. This
+ // means that when we change an album cover, the grandparent may have a deep link to the old
+ // album cover. So find any parent albums that had the old item as their album cover and
+ // switch them over to the new item.
+ if ($old_album_cover_id) {
+ foreach ($item->parents(array(array("album_cover_item_id", "=", $old_album_cover_id)))
+ as $ancestor) {
+ if (access::can("edit", $ancestor)) {
+ $ancestor->album_cover_item_id = $parent->album_cover_item_id;
+ $ancestor->save();
+ graphics::generate($ancestor);
+ }
+ }
+ }
+ }
+
+ static function remove_album_cover($album) {
+ access::required("view", $album);
+ access::required("edit", $album);
+
+ model_cache::clear();
+ $album->album_cover_item_id = null;
+ $album->save();
+ graphics::generate($album);
+ }
+
+ /**
+ * Sanitize a filename into something presentable as an item title
+ * @param string $filename
+ * @return string title
+ */
+ static function convert_filename_to_title($filename) {
+ $title = strtr($filename, "_", " ");
+ $title = preg_replace("/\..{3,4}$/", "", $title);
+ $title = preg_replace("/ +/", " ", $title);
+ return $title;
+ }
+
+ /**
+ * Convert a filename into something we can use as a url component.
+ * @param string $filename
+ */
+ static function convert_filename_to_slug($filename) {
+ $result = str_replace("&", "-and-", $filename);
+ $result = str_replace(" ", "-", $result);
+
+ // It's not easy to extend the text helper since it's called by the Input class which is
+ // referenced in hooks/init_gallery, so it's
+ if (class_exists("transliterate")) {
+ $result = transliterate::utf8_to_ascii($result);
+ } else {
+ $result = text::transliterate_to_ascii($result);
+ }
+ $result = preg_replace("/[^A-Za-z0-9-_]+/", "-", $result);
+ $result = preg_replace("/-+/", "-", $result);
+ return trim($result, "-");
+ }
+
+ /**
+ * Display delete confirmation message and form
+ * @param object $item
+ * @return string form
+ */
+ static function get_delete_form($item) {
+ $page_type = Input::instance()->get("page_type");
+ $from_id = Input::instance()->get("from_id");
+ $form = new Forge(
+ "quick/delete/$item->id?page_type=$page_type&from_id=$from_id", "",
+ "post", array("id" => "g-confirm-delete"));
+ $group = $form->group("confirm_delete")->label(t("Confirm Deletion"));
+ $group->submit("")->value(t("Delete"));
+ $form->script("")
+ ->url(url::abs_file("modules/gallery/js/item_form_delete.js"));
+ return $form;
+ }
+
+ /**
+ * Get the next weight value
+ */
+ static function get_max_weight() {
+ // Guard against an empty result when we create the first item. It's unfortunate that we
+ // have to check this every time.
+ // @todo: figure out a better way to bootstrap the weight.
+ $result = db::build()
+ ->select("weight")->from("items")
+ ->order_by("weight", "desc")->limit(1)
+ ->execute()->current();
+ return ($result ? $result->weight : 0) + 1;
+ }
+
+ /**
+ * Add a set of restrictions to any following queries to restrict access only to items
+ * viewable by the active user.
+ * @chainable
+ */
+ static function viewable($model) {
+ $view_restrictions = array();
+ if (!identity::active_user()->admin) {
+ foreach (identity::group_ids_for_active_user() as $id) {
+ $view_restrictions[] = array("items.view_$id", "=", access::ALLOW);
+ }
+ }
+
+ if (count($view_restrictions)) {
+ $model->and_open()->merge_or_where($view_restrictions)->close();
+ }
+
+ return $model;
+ }
+
+ /**
+ * Find an item by its path. If there's no match, return an empty Item_Model.
+ * NOTE: the caller is responsible for performing security checks on the resulting item.
+ * @param string $path
+ * @return object Item_Model
+ */
+ static function find_by_path($path) {
+ $path = trim($path, "/");
+
+ // The root path name is NULL not "", hence this workaround.
+ if ($path == "") {
+ return item::root();
+ }
+
+ // Check to see if there's an item in the database with a matching relative_path_cache value.
+ // Since that field is urlencoded, we must urlencoded the components of the path.
+ foreach (explode("/", $path) as $part) {
+ $encoded_array[] = rawurlencode($part);
+ }
+ $encoded_path = join("/", $encoded_array);
+ $item = ORM::factory("item")
+ ->where("relative_path_cache", "=", $encoded_path)
+ ->find();
+ if ($item->loaded()) {
+ return $item;
+ }
+
+ // Since the relative_path_cache field is a cache, it can be unavailable. If we don't find
+ // anything, fall back to checking the path the hard way.
+ $paths = explode("/", $path);
+ foreach (ORM::factory("item")
+ ->where("name", "=", end($paths))
+ ->where("level", "=", count($paths) + 1)
+ ->find_all() as $item) {
+ if (urldecode($item->relative_path()) == $path) {
+ return $item;
+ }
+ }
+
+ return new Item_Model();
+ }
+
+
+ /**
+ * Locate an item using the URL. We assume that the url is in the form /a/b/c where each
+ * component matches up with an item slug. If there's no match, return an empty Item_Model
+ * NOTE: the caller is responsible for performing security checks on the resulting item.
+ * @param string $url the relative url fragment
+ * @return Item_Model
+ */
+ static function find_by_relative_url($relative_url) {
+ // In most cases, we'll have an exact match in the relative_url_cache item field.
+ // but failing that, walk down the tree until we find it. The fallback code will fix caches
+ // as it goes, so it'll never be run frequently.
+ $item = ORM::factory("item")->where("relative_url_cache", "=", $relative_url)->find();
+ if (!$item->loaded()) {
+ $segments = explode("/", $relative_url);
+ foreach (ORM::factory("item")
+ ->where("slug", "=", end($segments))
+ ->where("level", "=", count($segments) + 1)
+ ->find_all() as $match) {
+ if ($match->relative_url() == $relative_url) {
+ $item = $match;
+ }
+ }
+ }
+ return $item;
+ }
+
+ /**
+ * Return the root Item_Model
+ * @return Item_Model
+ */
+ static function root() {
+ return model_cache::get("item", 1);
+ }
+
+ /**
+ * Return a query to get a random Item_Model, with optional filters.
+ * Usage: item::random_query()->execute();
+ *
+ * Note: You can add your own ->where() clauses but if your Gallery is
+ * small or your where clauses are over-constrained you may wind up with
+ * no item. You should try running this a few times in a loop if you
+ * don't get an item back.
+ */
+ static function random_query() {
+ // Pick a random number and find the item that's got nearest smaller number.
+ // This approach works best when the random numbers in the system are roughly evenly
+ // distributed so this is going to be more efficient with larger data sets.
+ return ORM::factory("item")
+ ->viewable()
+ ->where("rand_key", "<", random::percent())
+ ->order_by("rand_key", "DESC");
+ }
+
+ /**
+ * Find the position of the given item in its parent album. The resulting
+ * value is 1-indexed, so the first child in the album is at position 1.
+ *
+ * @param Item_Model $item
+ * @param array $where an array of arrays, each compatible with ORM::where()
+ */
+ static function get_position($item, $where=array()) {
+ $album = $item->parent();
+
+ if (!strcasecmp($album->sort_order, "DESC")) {
+ $comp = ">";
+ } else {
+ $comp = "<";
+ }
+ $query_model = ORM::factory("item");
+
+ // If the comparison column has NULLs in it, we can't use comparators on it
+ // and will have to deal with it the hard way.
+ $count = $query_model->viewable()
+ ->where("parent_id", "=", $album->id)
+ ->where($album->sort_column, "IS", null)
+ ->merge_where($where)
+ ->count_all();
+
+ if (empty($count)) {
+ // There are no NULLs in the sort column, so we can just use it directly.
+ $sort_column = $album->sort_column;
+
+ $position = $query_model->viewable()
+ ->where("parent_id", "=", $album->id)
+ ->where($sort_column, $comp, $item->$sort_column)
+ ->merge_where($where)
+ ->count_all();
+
+ // We stopped short of our target value in the sort (notice that we're
+ // using a inequality comparator above) because it's possible that we have
+ // duplicate values in the sort column. An equality check would just
+ // arbitrarily pick one of those multiple possible equivalent columns,
+ // which would mean that if you choose a sort order that has duplicates,
+ // it'd pick any one of them as the child's "position".
+ //
+ // Fix this by doing a 2nd query where we iterate over the equivalent
+ // columns and add them to our position count.
+ foreach ($query_model->viewable()
+ ->select("id")
+ ->where("parent_id", "=", $album->id)
+ ->where($sort_column, "=", $item->$sort_column)
+ ->merge_where($where)
+ ->order_by(array("id" => "ASC"))
+ ->find_all() as $row) {
+ $position++;
+ if ($row->id == $item->id) {
+ break;
+ }
+ }
+ } else {
+ // There are NULLs in the sort column, so we can't use MySQL comparators.
+ // Fall back to iterating over every child row to get to the current one.
+ // This can be wildly inefficient for really large albums, but it should
+ // be a rare case that the user is sorting an album with null values in
+ // the sort column.
+ //
+ // Reproduce the children() functionality here using Database directly to
+ // avoid loading the whole ORM for each row.
+ $order_by = array($album->sort_column => $album->sort_order);
+ // Use id as a tie breaker
+ if ($album->sort_column != "id") {
+ $order_by["id"] = "ASC";
+ }
+
+ $position = 0;
+ foreach ($query_model->viewable()
+ ->select("id")
+ ->where("parent_id", "=", $album->id)
+ ->merge_where($where)
+ ->order_by($order_by)
+ ->find_all() as $row) {
+ $position++;
+ if ($row->id == $item->id) {
+ break;
+ }
+ }
+ }
+
+ return $position;
+ }
+
+ /**
+ * Set the display context callback for any future item renders.
+ */
+ static function set_display_context_callback() {
+ if (!request::user_agent("robot")) {
+ $args = func_get_args();
+ Cache::instance()->set("display_context_" . $sid = Session::instance()->id(), $args,
+ array("display_context"));
+ }
+ }
+
+ /**
+ * Get rid of the display context callback
+ */
+ static function clear_display_context_callback() {
+ Cache::instance()->delete("display_context_" . $sid = Session::instance()->id());
+ }
+
+ /**
+ * Call the display context callback for the given item
+ */
+ static function get_display_context($item) {
+ if (!request::user_agent("robot")) {
+ $args = Cache::instance()->get("display_context_" . $sid = Session::instance()->id());
+ $callback = $args[0];
+ $args[0] = $item;
+ }
+
+ if (empty($callback)) {
+ $callback = "Albums_Controller::get_display_context";
+ $args = array($item);
+ }
+ return call_user_func_array($callback, $args);
+ }
+
+ /**
+ * Reset all child weights of a given album to a monotonically increasing sequence based on the
+ * current sort order of the album.
+ */
+ static function resequence_child_weights($album) {
+ $weight = 0;
+ foreach ($album->children() as $child) {
+ $child->weight = ++$weight;
+ $child->save();
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/item_rest.php b/modules/gallery/helpers/item_rest.php
new file mode 100644
index 0000000..efeba2e
--- /dev/null
+++ b/modules/gallery/helpers/item_rest.php
@@ -0,0 +1,210 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class item_rest_Core {
+ /**
+ * For items that are collections, you can specify the following additional query parameters to
+ * query the collection. You can specify them in any combination.
+ *
+ * scope=direct
+ * Only return items that are immediately under this one
+ * scope=all
+ * Return items anywhere under this one
+ *
+ * name=<substring>
+ * Only return items where the name contains this substring
+ *
+ * random=true
+ * Return a single random item
+ *
+ * type=<comma separate list of photo, movie or album>
+ * Limit the type to types in this list, eg: "type=photo,movie".
+ * Also limits the types returned in the member collections (same behaviour as item_rest).
+ */
+ static function get($request) {
+ $item = rest::resolve($request->url);
+ access::required("view", $item);
+
+ $p = $request->params;
+ if (isset($p->random)) {
+ $orm = item::random_query()->offset(0)->limit(1);
+ } else {
+ $orm = ORM::factory("item")->viewable();
+ }
+
+ if (empty($p->scope)) {
+ $p->scope = "direct";
+ }
+
+ if (!in_array($p->scope, array("direct", "all"))) {
+ throw new Rest_Exception("Bad Request", 400);
+ }
+
+ if ($p->scope == "direct") {
+ $orm->where("parent_id", "=", $item->id);
+ } else {
+ $orm->where("left_ptr", ">", $item->left_ptr);
+ $orm->where("right_ptr", "<", $item->right_ptr);
+ }
+
+ if (isset($p->name)) {
+ $orm->where("name", "LIKE", "%" . Database::escape_for_like($p->name) . "%");
+ }
+
+ if (isset($p->type)) {
+ $orm->where("type", "IN", explode(",", $p->type));
+ }
+
+ // Apply the item's sort order, using id as the tie breaker.
+ // See Item_Model::children()
+ $order_by = array($item->sort_column => $item->sort_order);
+ if ($item->sort_column != "id") {
+ $order_by["id"] = "ASC";
+ }
+ $orm->order_by($order_by);
+
+ $result = array(
+ "url" => $request->url,
+ "entity" => $item->as_restful_array(),
+ "relationships" => rest::relationships("item", $item));
+ if ($item->is_album()) {
+ $result["members"] = array();
+ foreach ($orm->find_all() as $child) {
+ $result["members"][] = rest::url("item", $child);
+ }
+ }
+
+ return $result;
+ }
+
+ static function put($request) {
+ $item = rest::resolve($request->url);
+ access::required("edit", $item);
+
+ if ($entity = $request->params->entity) {
+ // Only change fields from a whitelist.
+ foreach (array("album_cover", "captured", "description",
+ "height", "mime_type", "name", "parent", "rand_key", "resize_dirty",
+ "resize_height", "resize_width", "slug", "sort_column", "sort_order",
+ "thumb_dirty", "thumb_height", "thumb_width", "title", "view_count",
+ "width") as $key) {
+ switch ($key) {
+ case "album_cover":
+ if (property_exists($entity, "album_cover")) {
+ $album_cover_item = rest::resolve($entity->album_cover);
+ access::required("view", $album_cover_item);
+ $item->album_cover_item_id = $album_cover_item->id;
+ }
+ break;
+
+ case "parent":
+ if (property_exists($entity, "parent")) {
+ $parent = rest::resolve($entity->parent);
+ access::required("edit", $parent);
+ $item->parent_id = $parent->id;
+ }
+ break;
+ default:
+ if (property_exists($entity, $key)) {
+ $item->$key = $entity->$key;
+ }
+ }
+ }
+ }
+
+ // Replace the data file, if required
+ if (($item->is_photo() || $item->is_movie()) && isset($request->file)) {
+ $item->set_data_file($request->file);
+ }
+
+ $item->save();
+
+ if (isset($request->params->members) && $item->sort_column == "weight") {
+ $weight = 0;
+ foreach ($request->params->members as $url) {
+ $child = rest::resolve($url);
+ if ($child->parent_id == $item->id && $child->weight != $weight) {
+ $child->weight = $weight;
+ $child->save();
+ }
+ $weight++;
+ }
+ }
+ }
+
+ static function post($request) {
+ $parent = rest::resolve($request->url);
+ access::required("add", $parent);
+
+ $entity = $request->params->entity;
+ $item = ORM::factory("item");
+ switch ($entity->type) {
+ case "album":
+ $item->type = "album";
+ $item->parent_id = $parent->id;
+ $item->name = $entity->name;
+ $item->title = isset($entity->title) ? $entity->title : $entity->name;
+ $item->description = isset($entity->description) ? $entity->description : null;
+ $item->slug = isset($entity->slug) ? $entity->slug : null;
+ $item->save();
+ break;
+
+ case "photo":
+ case "movie":
+ if (empty($request->file)) {
+ throw new Rest_Exception(
+ "Bad Request", 400, array("errors" => array("file" => t("Upload failed"))));
+ }
+ $item->type = $entity->type;
+ $item->parent_id = $parent->id;
+ $item->set_data_file($request->file);
+ $item->name = $entity->name;
+ $item->title = isset($entity->title) ? $entity->title : $entity->name;
+ $item->description = isset($entity->description) ? $entity->description : null;
+ $item->slug = isset($entity->slug) ? $entity->slug : null;
+ $item->save();
+ break;
+
+ default:
+ throw new Rest_Exception(
+ "Bad Request", 400, array("errors" => array("type" => "invalid")));
+ }
+
+ return array("url" => rest::url("item", $item));
+ }
+
+ static function delete($request) {
+ $item = rest::resolve($request->url);
+ access::required("edit", $item);
+
+ $item->delete();
+ }
+
+ static function resolve($id) {
+ $item = ORM::factory("item", $id);
+ if (!access::can("view", $item)) {
+ throw new Kohana_404_Exception();
+ }
+ return $item;
+ }
+
+ static function url($item) {
+ return url::abs_site("rest/item/{$item->id}");
+ }
+}
diff --git a/modules/gallery/helpers/items_rest.php b/modules/gallery/helpers/items_rest.php
new file mode 100644
index 0000000..7622c53
--- /dev/null
+++ b/modules/gallery/helpers/items_rest.php
@@ -0,0 +1,96 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class items_rest_Core {
+ /**
+ * To retrieve a collection of items, you can specify the following query parameters to specify
+ * the type of the collection. If both are specified, then the url parameter is used and the
+ * ancestors_for is ignored. Specifying the "type" parameter with the urls parameter, will
+ * filter the results based on the specified type. Using the type parameter with the
+ * ancestors_for parameter makes no sense and will be ignored.
+ *
+ * urls=["url1","url2","url3"]
+ * Return items that match the specified urls. Typically used to return the member detail
+ *
+ * ancestors_for=url
+ * Return the ancestors of the specified item
+ *
+ * type=<comma separate list of photo, movie or album>
+ * Limit the type to types in this list, eg: "type=photo,movie".
+ * Also limits the types returned in the member collections (same behaviour as item_rest).
+ * Ignored if ancestors_for is set.
+ */
+ static function get($request) {
+ $items = array();
+ $types = array();
+
+ if (isset($request->params->urls)) {
+ if (isset($request->params->type)) {
+ $types = explode(",", $request->params->type);
+ }
+
+ foreach (json_decode($request->params->urls) as $url) {
+ $item = rest::resolve($url);
+ if (!access::can("view", $item)) {
+ continue;
+ }
+
+ if (empty($types) || in_array($item->type, $types)) {
+ $items[] = items_rest::_format_restful_item($item, $types);
+ }
+ }
+ } else if (isset($request->params->ancestors_for)) {
+ $item = rest::resolve($request->params->ancestors_for);
+ if (!access::can("view", $item)) {
+ throw new Kohana_404_Exception();
+ }
+ $items[] = items_rest::_format_restful_item($item, $types);
+ while (($item = $item->parent()) != null) {
+ array_unshift($items, items_rest::_format_restful_item($item, $types));
+ };
+ }
+
+ return $items;
+ }
+
+ static function resolve($id) {
+ $item = ORM::factory("item", $id);
+ if (!access::can("view", $item)) {
+ throw new Kohana_404_Exception();
+ }
+ return $item;
+ }
+
+ private static function _format_restful_item($item, $types) {
+ $item_rest = array("url" => rest::url("item", $item),
+ "entity" => $item->as_restful_array(),
+ "relationships" => rest::relationships("item", $item));
+ if ($item->type == "album") {
+ $members = array();
+ foreach ($item->viewable()->children() as $child) {
+ if (empty($types) || in_array($child->type, $types)) {
+ $members[] = rest::url("item", $child);
+ }
+ }
+ $item_rest["members"] = $members;
+ }
+
+ return $item_rest;
+ }
+}
diff --git a/modules/gallery/helpers/json.php b/modules/gallery/helpers/json.php
new file mode 100644
index 0000000..b56f269
--- /dev/null
+++ b/modules/gallery/helpers/json.php
@@ -0,0 +1,31 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class json_Core {
+ /**
+ * JSON Encode a reply to the browser and set the content type to specify that it's a JSON
+ * payload.
+ *
+ * @param mixed $message string or object to json encode and print
+ */
+ static function reply($message) {
+ header("Content-Type: application/json; charset=" . Kohana::CHARSET);
+ print json_encode($message);
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/l10n_client.php b/modules/gallery/helpers/l10n_client.php
new file mode 100644
index 0000000..2a1be2f
--- /dev/null
+++ b/modules/gallery/helpers/l10n_client.php
@@ -0,0 +1,323 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+class l10n_client_Core {
+
+ private static function _server_url($path) {
+ return "http://galleryproject.org/translations/$path";
+ }
+
+ static function server_api_key_url() {
+ return self::_server_url("userkey/" . self::client_token());
+ }
+
+ static function client_token() {
+ return md5("l10n_client_client_token" . access::private_key());
+ }
+
+ static function api_key($api_key=null) {
+ if ($api_key !== null) {
+ module::set_var("gallery", "l10n_client_key", $api_key);
+ }
+ return module::get_var("gallery", "l10n_client_key", "");
+ }
+
+ static function server_uid($api_key=null) {
+ $api_key = $api_key == null ? l10n_client::api_key() : $api_key;
+ $parts = explode(":", $api_key);
+ return empty($parts) ? 0 : $parts[0];
+ }
+
+ private static function _sign($payload, $api_key=null) {
+ $api_key = $api_key == null ? l10n_client::api_key() : $api_key;
+ return md5($api_key . $payload . l10n_client::client_token());
+ }
+
+ static function validate_api_key($api_key) {
+ $version = "1.0";
+ $url = self::_server_url("status");
+ $signature = self::_sign($version, $api_key);
+
+ try {
+ list ($response_data, $response_status) = remote::post(
+ $url, array("version" => $version,
+ "client_token" => l10n_client::client_token(),
+ "signature" => $signature,
+ "uid" => l10n_client::server_uid($api_key)));
+ } catch (ErrorException $e) {
+ // Log the error, but then return a "can't make connection" error
+ Kohana_Log::add("error", $e->getMessage() . "\n" . $e->getTraceAsString());
+ }
+ if (!isset($response_data) && !isset($response_status)) {
+ return array(false, false);
+ }
+
+ if (!remote::success($response_status)) {
+ return array(true, false);
+ }
+ return array(true, true);
+ }
+
+ /**
+ * Fetches translations for l10n messages. Must be called repeatedly
+ * until 0 is returned (which is a countdown indicating progress).
+ *
+ * @param $num_fetched in/out parameter to specify which batch of
+ * messages to fetch translations for.
+ * @return The number of messages for which we didn't fetch
+ * translations for.
+ */
+ static function fetch_updates(&$num_fetched) {
+ $request = new stdClass();
+ $request->locales = array();
+ $request->messages = new stdClass();
+
+ $locales = locales::installed();
+ foreach ($locales as $locale => $locale_data) {
+ $request->locales[] = $locale;
+ }
+
+ // See the server side code for how we arrive at this
+ // number as a good limit for #locales * #messages.
+ $max_messages = 2000 / count($locales);
+ $num_messages = 0;
+ $rows = db::build()
+ ->select("key", "locale", "revision", "translation")
+ ->from("incoming_translations")
+ ->order_by("key")
+ ->limit(1000000) // ignore, just there to satisfy SQL syntax
+ ->offset($num_fetched)
+ ->execute();
+ $num_remaining = $rows->count();
+ foreach ($rows as $row) {
+ if (!isset($request->messages->{$row->key})) {
+ if ($num_messages >= $max_messages) {
+ break;
+ }
+ $request->messages->{$row->key} = 1;
+ $num_messages++;
+ }
+ if (!empty($row->revision) && !empty($row->translation) &&
+ isset($locales[$row->locale])) {
+ if (!is_object($request->messages->{$row->key})) {
+ $request->messages->{$row->key} = new stdClass();
+ }
+ $request->messages->{$row->key}->{$row->locale} = (int) $row->revision;
+ }
+ $num_fetched++;
+ $num_remaining--;
+ }
+ // @todo Include messages from outgoing_translations?
+
+ if (!$num_messages) {
+ return $num_remaining;
+ }
+
+ $request_data = json_encode($request);
+ $url = self::_server_url("fetch");
+ list ($response_data, $response_status) = remote::post($url, array("data" => $request_data));
+ if (!remote::success($response_status)) {
+ throw new Exception("@todo TRANSLATIONS_FETCH_REQUEST_FAILED " . $response_status);
+ }
+ if (empty($response_data)) {
+ return $num_remaining;
+ }
+
+ $response = json_decode($response_data);
+
+ // Response format (JSON payload):
+ // [{key:<key_1>, translation: <JSON encoded translation>, rev:<rev>, locale:<locale>},
+ // {key:<key_2>, ...}
+ // ]
+ foreach ($response as $message_data) {
+ // @todo Better input validation
+ if (empty($message_data->key) || empty($message_data->translation) ||
+ empty($message_data->locale) || empty($message_data->rev)) {
+ throw new Exception("@todo TRANSLATIONS_FETCH_REQUEST_FAILED: Invalid response data");
+ }
+ $key = $message_data->key;
+ $locale = $message_data->locale;
+ $revision = $message_data->rev;
+ $translation = json_decode($message_data->translation);
+ if (!is_string($translation)) {
+ // Normalize stdclass to array
+ $translation = (array) $translation;
+ }
+ $translation = serialize($translation);
+
+ // @todo Should we normalize the incoming_translations table into messages(id, key, message)
+ // and incoming_translations(id, translation, locale, revision)? Or just allow
+ // incoming_translations.message to be NULL?
+ $locale = $message_data->locale;
+ $entry = ORM::factory("incoming_translation")
+ ->where("key", "=", $key)
+ ->where("locale", "=", $locale)
+ ->find();
+ if (!$entry->loaded()) {
+ // @todo Load a message key -> message (text) dict into memory outside of this loop
+ $root_entry = ORM::factory("incoming_translation")
+ ->where("key", "=", $key)
+ ->where("locale", "=", "root")
+ ->find();
+ $entry->key = $key;
+ $entry->message = $root_entry->message;
+ $entry->locale = $locale;
+ }
+ $entry->revision = $revision;
+ $entry->translation = $translation;
+ $entry->save();
+ }
+
+ return $num_remaining;
+ }
+
+ static function submit_translations() {
+ // Request format (HTTP POST):
+ // client_token = <client_token>
+ // uid = <l10n server user id>
+ // signature = md5(user_api_key($uid, $client_token) . $data . $client_token))
+ // data = // JSON payload
+ //
+ // {<key_1>: {message: <JSON encoded message>
+ // translations: {<locale_1>: <JSON encoded translation>,
+ // <locale_2>: ...}},
+ // <key_2>: {...}
+ // }
+
+ // @todo Batch requests (max request size)
+ // @todo include base_revision in submission / how to handle resubmissions / edit fights?
+ $request = new stdClass();
+ foreach (db::build()
+ ->select("key", "message", "locale", "base_revision", "translation")
+ ->from("outgoing_translations")
+ ->execute() as $row) {
+ $key = $row->key;
+ if (!isset($request->{$key})) {
+ $request->{$key} = new stdClass();
+ $request->{$key}->translations = new stdClass();
+ $request->{$key}->message = json_encode(unserialize($row->message));
+ }
+ $request->{$key}->translations->{$row->locale} = json_encode(unserialize($row->translation));
+ }
+
+ // @todo reduce memory consumption, e.g. free $request
+ $request_data = json_encode($request);
+ $url = self::_server_url("submit");
+ $signature = self::_sign($request_data);
+
+ list ($response_data, $response_status) = remote::post(
+ $url, array("data" => $request_data,
+ "client_token" => l10n_client::client_token(),
+ "signature" => $signature,
+ "uid" => l10n_client::server_uid()));
+
+ if (!remote::success($response_status)) {
+ throw new Exception("@todo TRANSLATIONS_SUBMISSION_FAILED " . $response_status);
+ }
+ if (empty($response_data)) {
+ return;
+ }
+
+ $response = json_decode($response_data);
+ // Response format (JSON payload):
+ // [{key:<key_1>, locale:<locale_1>, rev:<rev_1>, status:<rejected|accepted|pending>},
+ // {key:<key_2>, ...}
+ // ]
+
+ // @todo Move messages out of outgoing into incoming, using new rev?
+ // @todo show which messages have been rejected / are pending?
+ }
+
+ /**
+ * Plural forms.
+ */
+ static function plural_forms($locale) {
+ $parts = explode('_', $locale);
+ $language = $parts[0];
+
+ // Data from CLDR 1.6 (http://unicode.org/cldr/data/common/supplemental/plurals.xml).
+ // Docs: http://www.unicode.org/cldr/data/charts/supplemental/language_plural_rules.html
+ switch ($language) {
+ case 'az':
+ case 'fa':
+ case 'hu':
+ case 'ja':
+ case 'ko':
+ case 'my':
+ case 'to':
+ case 'tr':
+ case 'vi':
+ case 'yo':
+ case 'zh':
+ case 'bo':
+ case 'dz':
+ case 'id':
+ case 'jv':
+ case 'ka':
+ case 'km':
+ case 'kn':
+ case 'ms':
+ case 'th':
+ return array('other');
+
+ case 'ar':
+ return array('zero', 'one', 'two', 'few', 'many', 'other');
+
+ case 'lv':
+ return array('zero', 'one', 'other');
+
+ case 'ga':
+ case 'se':
+ case 'sma':
+ case 'smi':
+ case 'smj':
+ case 'smn':
+ case 'sms':
+ return array('one', 'two', 'other');
+
+ case 'ro':
+ case 'mo':
+ case 'lt':
+ case 'cs':
+ case 'sk':
+ case 'pl':
+ return array('one', 'few', 'other');
+
+ case 'hr':
+ case 'ru':
+ case 'sr':
+ case 'uk':
+ case 'be':
+ case 'bs':
+ case 'sh':
+ case 'mt':
+ return array('one', 'few', 'many', 'other');
+
+ case 'sl':
+ return array('one', 'two', 'few', 'other');
+
+ case 'cy':
+ return array('one', 'two', 'many', 'other');
+
+ default: // en, de, etc.
+ return array('one', 'other');
+ }
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/l10n_scanner.php b/modules/gallery/helpers/l10n_scanner.php
new file mode 100644
index 0000000..5980ebe
--- /dev/null
+++ b/modules/gallery/helpers/l10n_scanner.php
@@ -0,0 +1,178 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * Scans all source code for messages that need to be localized.
+ */
+class l10n_scanner_Core {
+ // Based on Drupal's potx module, originally written by:
+ // G‡bor Hojtsy http://drupal.org/user/4166
+ public static $cache;
+
+ static function process_message($message, &$cache) {
+ if (empty($cache)) {
+ foreach (db::build()
+ ->select("key")
+ ->from("incoming_translations")
+ ->where("locale", "=", "root")
+ ->execute() as $row) {
+ $cache[$row->key] = true;
+ }
+ }
+
+ $key = Gallery_I18n::get_message_key($message);
+ if (array_key_exists($key, $cache)) {
+ return $cache[$key];
+ }
+
+ $entry = ORM::factory("incoming_translation")->where("key", "=", $key)->find();
+ if (!$entry->loaded()) {
+ $entry->key = $key;
+ $entry->message = serialize($message);
+ $entry->locale = "root";
+ $entry->save();
+ }
+ }
+
+ static function scan_php_file($file, &$cache) {
+ $code = file_get_contents($file);
+ $raw_tokens = token_get_all($code);
+ unset($code);
+
+ $tokens = array();
+ $func_token_list = array("t" => array(), "t2" => array());
+ $token_number = 0;
+ // Filter out HTML / whitespace, and build a lookup for global function calls.
+ foreach ($raw_tokens as $token) {
+ if ((!is_array($token)) || (($token[0] != T_WHITESPACE) && ($token[0] != T_INLINE_HTML))) {
+ if (is_array($token)) {
+ if ($token[0] == T_STRING && in_array($token[1], array("t", "t2"))) {
+ $func_token_list[$token[1]][] = $token_number;
+ }
+ }
+ $tokens[] = $token;
+ $token_number++;
+ }
+ }
+ unset($raw_tokens);
+
+ if (!empty($func_token_list["t"])) {
+ $errors = l10n_scanner::_parse_t_calls($tokens, $func_token_list["t"], $cache);
+ foreach ($errors as $line => $error) {
+ Kohana_Log::add(
+ "error", "Translation scanner error. " .
+ "file: " . substr($file, strlen(DOCROOT)) . ", line: $line, context: $error");
+ }
+ }
+
+ if (!empty($func_token_list["t2"])) {
+ $errors = l10n_scanner::_parse_plural_calls($tokens, $func_token_list["t2"], $cache);
+ foreach ($errors as $line => $error) {
+ Kohana_Log::add(
+ "error", "Translation scanner error. " .
+ "file: " . substr($file, strlen(DOCROOT)) . ", line: $line, context: $error");
+ }
+ }
+ }
+
+ static function scan_info_file($file, &$cache) {
+ $info = new ArrayObject(parse_ini_file($file), ArrayObject::ARRAY_AS_PROPS);
+ foreach (array('name', 'description') as $property) {
+ if (isset($info->$property)) {
+ l10n_scanner::process_message($info->$property, $cache);
+ }
+ }
+ }
+
+ private static function _parse_t_calls(&$tokens, &$call_list, &$cache) {
+ $errors = array();
+ foreach ($call_list as $index) {
+ $function_name = $tokens[$index++];
+ $parens = $tokens[$index++];
+ $first_param = $tokens[$index++];
+ $next_token = $tokens[$index];
+
+ if ($parens == "(") {
+ if (in_array($next_token, array(")", ","))
+ && (is_array($first_param) && ($first_param[0] == T_CONSTANT_ENCAPSED_STRING))) {
+ $message = self::_escape_quoted_string($first_param[1]);
+ l10n_scanner::process_message($message, $cache);
+ } else {
+ if (is_array($first_param) && ($first_param[0] == T_CONSTANT_ENCAPSED_STRING)) {
+ // Malformed string literals; escalate this
+ $errors[$first_param[2]] =
+ var_export(array($function_name, $parens, $first_param, $next_token), 1);
+ } else {
+ // t() found, but inside is something which is not a string literal. That's fine.
+ }
+ }
+ }
+ }
+ return $errors;
+ }
+
+ private static function _parse_plural_calls(&$tokens, &$call_list, &$cache) {
+ $errors = array();
+ foreach ($call_list as $index) {
+ $function_name = $tokens[$index++];
+ $parens = $tokens[$index++];
+ $first_param = $tokens[$index++];
+ $first_separator = $tokens[$index++];
+ $second_param = $tokens[$index++];
+ $next_token = $tokens[$index];
+
+ if ($parens == "(") {
+ if ($first_separator == "," && $next_token == ","
+ && is_array($first_param) && $first_param[0] == T_CONSTANT_ENCAPSED_STRING
+ && is_array($second_param) && $second_param[0] == T_CONSTANT_ENCAPSED_STRING) {
+ $singular = self::_escape_quoted_string($first_param[1]);
+ $plural = self::_escape_quoted_string($second_param[1]);
+ l10n_scanner::process_message(array("one" => $singular, "other" => $plural), $cache);
+ } else {
+ if (is_array($first_param) && $first_param[0] == T_CONSTANT_ENCAPSED_STRING) {
+ $errors[$first_param[2]] = var_export(
+ array($function_name, $parens, $first_param,
+ $first_separator, $second_param, $next_token), 1);
+ } else {
+ // t2() found, but inside is something which is not a string literal. That's fine.
+ }
+ }
+ }
+ }
+ return $errors;
+ }
+
+ /**
+ * Escape quotes in a strings depending on the surrounding
+ * quote type used.
+ *
+ * @param $str The strings to escape
+ */
+ private static function _escape_quoted_string($str) {
+ $quo = substr($str, 0, 1);
+ $str = substr($str, 1, -1);
+ if ($quo == '"') {
+ $str = stripcslashes($str);
+ } else {
+ $str = strtr($str, array("\\'" => "'", "\\\\" => "\\"));
+ }
+ return $str;
+ }
+}
diff --git a/modules/gallery/helpers/legal_file.php b/modules/gallery/helpers/legal_file.php
new file mode 100644
index 0000000..eb9c25d
--- /dev/null
+++ b/modules/gallery/helpers/legal_file.php
@@ -0,0 +1,310 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class legal_file_Core {
+ private static $photo_types_by_extension;
+ private static $movie_types_by_extension;
+ private static $photo_extensions;
+ private static $movie_extensions;
+ private static $photo_types;
+ private static $movie_types;
+ private static $blacklist = array("php", "php3", "php4", "php5", "phtml", "phtm", "shtml", "shtm",
+ "pl", "cgi", "asp", "sh", "py", "c", "js");
+
+ /**
+ * Create a default list of allowed photo MIME types paired with their extensions and then let
+ * modules modify it. This is an ordered map, mapping extensions to their MIME types.
+ * Extensions cannot be duplicated, but MIMEs can (e.g. jpeg and jpg both map to image/jpeg).
+ *
+ * @param string $extension (opt.) - return MIME of extension; if not given, return complete array
+ */
+ static function get_photo_types_by_extension($extension=null) {
+ if (empty(self::$photo_types_by_extension)) {
+ $types_by_extension_wrapper = new stdClass();
+ $types_by_extension_wrapper->types_by_extension = array(
+ "jpg" => "image/jpeg", "jpeg" => "image/jpeg", "gif" => "image/gif", "png" => "image/png");
+ module::event("photo_types_by_extension", $types_by_extension_wrapper);
+ foreach (self::$blacklist as $key) {
+ unset($types_by_extension_wrapper->types_by_extension[$key]);
+ }
+ self::$photo_types_by_extension = $types_by_extension_wrapper->types_by_extension;
+ }
+ if ($extension) {
+ // return matching MIME type
+ $extension = strtolower($extension);
+ if (isset(self::$photo_types_by_extension[$extension])) {
+ return self::$photo_types_by_extension[$extension];
+ } else {
+ return null;
+ }
+ } else {
+ // return complete array
+ return self::$photo_types_by_extension;
+ }
+ }
+
+ /**
+ * Create a default list of allowed movie MIME types paired with their extensions and then let
+ * modules modify it. This is an ordered map, mapping extensions to their MIME types.
+ * Extensions cannot be duplicated, but MIMEs can (e.g. jpeg and jpg both map to image/jpeg).
+ *
+ * @param string $extension (opt.) - return MIME of extension; if not given, return complete array
+ */
+ static function get_movie_types_by_extension($extension=null) {
+ if (empty(self::$movie_types_by_extension)) {
+ $types_by_extension_wrapper = new stdClass();
+ $types_by_extension_wrapper->types_by_extension = array(
+ "flv" => "video/x-flv", "mp4" => "video/mp4", "m4v" => "video/x-m4v");
+ module::event("movie_types_by_extension", $types_by_extension_wrapper);
+ foreach (self::$blacklist as $key) {
+ unset($types_by_extension_wrapper->types_by_extension[$key]);
+ }
+ self::$movie_types_by_extension = $types_by_extension_wrapper->types_by_extension;
+ }
+ if ($extension) {
+ // return matching MIME type
+ $extension = strtolower($extension);
+ if (isset(self::$movie_types_by_extension[$extension])) {
+ return self::$movie_types_by_extension[$extension];
+ } else {
+ return null;
+ }
+ } else {
+ // return complete array
+ return self::$movie_types_by_extension;
+ }
+ }
+
+ /**
+ * Create a merged list of all allowed photo and movie MIME types paired with their extensions.
+ *
+ * @param string $extension (opt.) - return MIME of extension; if not given, return complete array
+ */
+ static function get_types_by_extension($extension=null) {
+ $types_by_extension = legal_file::get_photo_types_by_extension();
+ if (movie::allow_uploads()) {
+ $types_by_extension = array_merge($types_by_extension,
+ legal_file::get_movie_types_by_extension());
+ }
+ if ($extension) {
+ // return matching MIME type
+ $extension = strtolower($extension);
+ if (isset($types_by_extension[$extension])) {
+ return $types_by_extension[$extension];
+ } else {
+ return null;
+ }
+ } else {
+ // return complete array
+ return $types_by_extension;
+ }
+ }
+
+ /**
+ * Create a default list of allowed photo extensions and then let modules modify it.
+ *
+ * @param string $extension (opt.) - return true if allowed; if not given, return complete array
+ */
+ static function get_photo_extensions($extension=null) {
+ if (empty(self::$photo_extensions)) {
+ $extensions_wrapper = new stdClass();
+ $extensions_wrapper->extensions = array_keys(legal_file::get_photo_types_by_extension());
+ module::event("legal_photo_extensions", $extensions_wrapper);
+ self::$photo_extensions = array_diff($extensions_wrapper->extensions, self::$blacklist);
+ }
+ if ($extension) {
+ // return true if in array, false if not
+ return in_array(strtolower($extension), self::$photo_extensions);
+ } else {
+ // return complete array
+ return self::$photo_extensions;
+ }
+ }
+
+ /**
+ * Create a default list of allowed movie extensions and then let modules modify it.
+ *
+ * @param string $extension (opt.) - return true if allowed; if not given, return complete array
+ */
+ static function get_movie_extensions($extension=null) {
+ if (empty(self::$movie_extensions)) {
+ $extensions_wrapper = new stdClass();
+ $extensions_wrapper->extensions = array_keys(legal_file::get_movie_types_by_extension());
+ module::event("legal_movie_extensions", $extensions_wrapper);
+ self::$movie_extensions = array_diff($extensions_wrapper->extensions, self::$blacklist);
+ }
+ if ($extension) {
+ // return true if in array, false if not
+ return in_array(strtolower($extension), self::$movie_extensions);
+ } else {
+ // return complete array
+ return self::$movie_extensions;
+ }
+ }
+
+ /**
+ * Create a merged list of all allowed photo and movie extensions.
+ *
+ * @param string $extension (opt.) - return true if allowed; if not given, return complete array
+ */
+ static function get_extensions($extension=null) {
+ $extensions = legal_file::get_photo_extensions();
+ if (movie::allow_uploads()) {
+ $extensions = array_merge($extensions, legal_file::get_movie_extensions());
+ }
+ if ($extension) {
+ // return true if in array, false if not
+ return in_array(strtolower($extension), $extensions);
+ } else {
+ // return complete array
+ return $extensions;
+ }
+ }
+
+ /**
+ * Create a merged list of all photo and movie filename filters,
+ * (e.g. "*.gif"), based on allowed extensions.
+ */
+ static function get_filters() {
+ $filters = array();
+ foreach (legal_file::get_extensions() as $extension) {
+ array_push($filters, "*." . $extension, "*." . strtoupper($extension));
+ }
+ return $filters;
+ }
+
+ /**
+ * Create a default list of allowed photo MIME types and then let modules modify it.
+ * Can be used to add legal alternatives for default MIME types.
+ * (e.g. flv maps to video/x-flv by default, but video/flv is still legal).
+ */
+ static function get_photo_types() {
+ if (empty(self::$photo_types)) {
+ $types_wrapper = new stdClass();
+ // Need array_unique since types_by_extension can be many-to-one (e.g. jpeg and jpg).
+ $types_wrapper->types = array_unique(array_values(legal_file::get_photo_types_by_extension()));
+ module::event("legal_photo_types", $types_wrapper);
+ self::$photo_types = $types_wrapper->types;
+ }
+ return self::$photo_types;
+ }
+
+ /**
+ * Create a default list of allowed movie MIME types and then let modules modify it.
+ * Can be used to add legal alternatives for default MIME types.
+ * (e.g. flv maps to video/x-flv by default, but video/flv is still legal).
+ */
+ static function get_movie_types() {
+ if (empty(self::$movie_types)) {
+ $types_wrapper = new stdClass();
+ // Need array_unique since types_by_extension can be many-to-one (e.g. jpeg and jpg).
+ $types_wrapper->types = array_unique(array_values(legal_file::get_movie_types_by_extension()));
+ $types_wrapper->types[] = "video/flv";
+ module::event("legal_movie_types", $types_wrapper);
+ self::$movie_types = $types_wrapper->types;
+ }
+ return self::$movie_types;
+ }
+
+ /**
+ * Change the extension of a filename. If the original filename has no
+ * extension, add the new one to the end.
+ */
+ static function change_extension($filename, $new_ext) {
+ $filename_no_ext = preg_replace("/\.[^\.\/]*?$/", "", $filename);
+ return "{$filename_no_ext}.{$new_ext}";
+ }
+
+ /**
+ * Reduce the given file to having a single extension.
+ */
+ static function smash_extensions($filename) {
+ if (!$filename) {
+ // It's harmless, so return it before it causes issues with pathinfo.
+ return $filename;
+ }
+ $parts = pathinfo($filename);
+ $result = "";
+ if ($parts["dirname"] != ".") {
+ $result .= $parts["dirname"] . "/";
+ }
+ $parts["filename"] = str_replace(".", "_", $parts["filename"]);
+ $parts["filename"] = preg_replace("/[_]+/", "_", $parts["filename"]);
+ $parts["filename"] = trim($parts["filename"], "_");
+ $result .= isset($parts["extension"]) ? "{$parts['filename']}.{$parts['extension']}" : $parts["filename"];
+ return $result;
+ }
+
+ /**
+ * Sanitize a filename for a given type (given as "photo" or "movie") and a target file format
+ * (given as an extension). This returns a completely legal and valid filename,
+ * or throws an exception if the type or extension given is invalid or illegal. It tries to
+ * maintain the filename's original extension even if it's not identical to the given extension
+ * (e.g. don't change "JPG" or "jpeg" to "jpg").
+ *
+ * Note: it is not okay if the extension given is legal but does not match the type (e.g. if
+ * extension is "mp4" and type is "photo", it will throw an exception)
+ *
+ * @param string $filename (with no directory)
+ * @param string $extension (can be uppercase or lowercase)
+ * @param string $type (as "photo" or "movie")
+ * @return string sanitized filename (or null if bad extension argument)
+ */
+ static function sanitize_filename($filename, $extension, $type) {
+ // Check if the type is valid - if so, get the mime types of the
+ // original and target extensions; if not, throw an exception.
+ $original_extension = pathinfo($filename, PATHINFO_EXTENSION);
+ switch ($type) {
+ case "photo":
+ $mime_type = legal_file::get_photo_types_by_extension($extension);
+ $original_mime_type = legal_file::get_photo_types_by_extension($original_extension);
+ break;
+ case "movie":
+ $mime_type = legal_file::get_movie_types_by_extension($extension);
+ $original_mime_type = legal_file::get_movie_types_by_extension($original_extension);
+ break;
+ default:
+ throw new Exception("@todo INVALID_TYPE");
+ }
+
+ // Check if the target extension is blank or invalid - if so, throw an exception.
+ if (!$extension || !$mime_type) {
+ throw new Exception("@todo ILLEGAL_EXTENSION");
+ }
+
+ // Check if the mime types of the original and target extensions match - if not, fix it.
+ if (!$original_extension || ($mime_type != $original_mime_type)) {
+ $filename = legal_file::change_extension($filename, $extension);
+ }
+
+ // It should be a filename without a directory - remove all slashes (and backslashes).
+ $filename = str_replace("/", "_", $filename);
+ $filename = str_replace("\\", "_", $filename);
+
+ // Remove extra dots from the filename. This will also remove extraneous underscores.
+ $filename = legal_file::smash_extensions($filename);
+
+ // It's possible that the filename has no base (e.g. ".jpg") - if so, give it a generic one.
+ if (empty($filename) || (substr($filename, 0, 1) == ".")) {
+ $filename = $type . $filename; // e.g. "photo.jpg" or "movie.mp4"
+ }
+
+ return $filename;
+ }
+}
diff --git a/modules/gallery/helpers/locales.php b/modules/gallery/helpers/locales.php
new file mode 100644
index 0000000..8ca25c3
--- /dev/null
+++ b/modules/gallery/helpers/locales.php
@@ -0,0 +1,264 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * This is the API for handling locales.
+ */
+class locales_Core {
+ private static $locales;
+ private static $language_subtag_to_locale;
+
+ /**
+ * Return the list of available locales.
+ */
+ static function available() {
+ if (empty(self::$locales)) {
+ self::_init_language_data();
+ }
+
+ return self::$locales;
+ }
+
+ static function installed() {
+ $available = self::available();
+ $default = module::get_var("gallery", "default_locale");
+ $codes = explode("|", module::get_var("gallery", "installed_locales", $default));
+ foreach ($codes as $code) {
+ if (isset($available[$code])) {
+ $installed[$code] = $available[$code];
+ }
+ }
+ return $installed;
+ }
+
+ static function update_installed($locales) {
+ // Ensure that the default is included...
+ $default = module::get_var("gallery", "default_locale");
+ $locales = in_array($default, $locales)
+ ? $locales
+ : array_merge($locales, array($default));
+
+ module::set_var("gallery", "installed_locales", join("|", $locales));
+
+ // Clear the cache
+ self::$locales = null;
+ }
+
+ // @todo Might want to add a localizable language name as well.
+ // ref: http://cldr.unicode.org/
+ // ref: http://cldr.unicode.org/index/cldr-spec/picking-the-right-language-code
+ // ref: http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/likely_subtags.html
+ private static function _init_language_data() {
+ $l["af_ZA"] = "Afrikaans"; // Afrikaans
+ $l["ar_SA"] = "العربية"; // Arabic
+ $l["be_BY"] = "Беларускі"; // Belarusian
+ $l["bg_BG"] = "български"; // Bulgarian
+ $l["bn_BD"] = "বাংলা"; // Bengali
+ $l["ca_ES"] = "Catalan"; // Catalan
+ $l["cs_CZ"] = "čeština"; // Czech
+ $l["da_DK"] = "Dansk"; // Danish
+ $l["de_DE"] = "Deutsch"; // German
+ $l["el_GR"] = "Greek"; // Greek
+ $l["en_GB"] = "English (UK)"; // English (UK)
+ $l["en_US"] = "English (US)"; // English (US)
+ $l["es_AR"] = "Español (AR)"; // Spanish (AR)
+ $l["es_ES"] = "Español"; // Spanish (ES)
+ $l["es_MX"] = "Español (MX)"; // Spanish (MX)
+ $l["et_EE"] = "Eesti"; // Estonian
+ $l["eu_ES"] = "Euskara"; // Basque
+ $l["fa_IR"] = "فارس"; // Farsi
+ $l["fi_FI"] = "Suomi"; // Finnish
+ $l["fo_FO"] = "Føroyskt"; // Faroese
+ $l["fr_FR"] = "Français"; // French
+ $l["ga_IE"] = "Gaeilge"; // Irish
+ $l["he_IL"] = "עברית"; // Hebrew
+ $l["hr_HR"] = "hr̀vātskī"; // Croatian
+ $l["hu_HU"] = "Magyar"; // Hungarian
+ $l["is_IS"] = "Icelandic"; // Icelandic
+ $l["it_IT"] = "Italiano"; // Italian
+ $l["ja_JP"] = "日本語"; // Japanese
+ $l["ko_KR"] = "한국어"; // Korean
+ $l["lt_LT"] = "Lietuvių"; // Lithuanian
+ $l["lv_LV"] = "Latviešu"; // Latvian
+ $l["ms_MY"] = "Bahasa Melayu"; // Malay
+ $l["mk_MK"] = "Македонски јазик"; // Macedonian
+ $l["nl_NL"] = "Nederlands"; // Dutch
+ $l["no_NO"] = "Norsk bokmål"; // Norwegian
+ $l["pl_PL"] = "Polski"; // Polish
+ $l["pt_BR"] = "Português do Brasil"; // Portuguese (BR)
+ $l["pt_PT"] = "Português ibérico"; // Portuguese (PT)
+ $l["ro_RO"] = "Română"; // Romanian
+ $l["ru_RU"] = "Русский"; // Russian
+ $l["sk_SK"] = "Slovenčina"; // Slovak
+ $l["sl_SI"] = "Slovenščina"; // Slovenian
+ $l["sr_CS"] = "Srpski"; // Serbian
+ $l["sv_SE"] = "Svenska"; // Swedish
+ $l["th_TH"] = "ภาษาไทย"; // Thai
+ $l["tn_ZA"] = "Setswana"; // Setswana
+ $l["tr_TR"] = "Türkçe"; // Turkish
+ $l["uk_UA"] = "українська"; // Ukrainian
+ $l["vi_VN"] = "Tiếng Việt"; // Vietnamese
+ $l["zh_CN"] = "简体中文"; // Chinese (CN)
+ $l["zh_TW"] = "繁體中文"; // Chinese (TW)
+ asort($l, SORT_LOCALE_STRING);
+ self::$locales = $l;
+
+ // Language subtag to (default) locale mapping
+ foreach ($l as $locale => $name) {
+ list ($language) = explode("_", $locale . "_");
+ // The first one mentioned is the default
+ if (!isset($d[$language])) {
+ $d[$language] = $locale;
+ }
+ }
+ self::$language_subtag_to_locale = $d;
+ }
+
+ static function display_name($locale=null) {
+ if (empty(self::$locales)) {
+ self::_init_language_data();
+ }
+ $locale or $locale = Gallery_I18n::instance()->locale();
+
+ return self::$locales[$locale];
+ }
+
+ static function is_rtl($locale=null) {
+ return Gallery_I18n::instance()->is_rtl($locale);
+ }
+
+ /**
+ * Returns the best match comparing the HTTP accept-language header
+ * with the installed locales.
+ * @todo replace this with request::accepts_language() when we upgrade to Kohana 2.4
+ */
+ static function locale_from_http_request() {
+ $http_accept_language = Input::instance()->server("HTTP_ACCEPT_LANGUAGE");
+ if ($http_accept_language) {
+ // Parse the HTTP header and build a preference list
+ // Example value: "de,en-us;q=0.7,en-uk,fr-fr;q=0.2"
+ $locale_preferences = array();
+ foreach (explode(",", $http_accept_language) as $code) {
+ list ($requested_locale, $qvalue) = explode(";", $code . ";");
+ $requested_locale = trim($requested_locale);
+ $qvalue = trim($qvalue);
+ if (preg_match("/^([a-z]{2,3})(?:[_-]([a-zA-Z]{2}))?/", $requested_locale, $matches)) {
+ $requested_locale = strtolower($matches[1]);
+ if (!empty($matches[2])) {
+ $requested_locale .= "_" . strtoupper($matches[2]);
+ }
+ $requested_locale = trim(str_replace("-", "_", $requested_locale));
+ if (!strlen($qvalue)) {
+ // If not specified, default to 1.
+ $qvalue = 1;
+ } else {
+ // qvalue is expected to be something like "q=0.7"
+ list ($ignored, $qvalue) = explode("=", $qvalue . "==");
+ $qvalue = floatval($qvalue);
+ }
+ // Group by language to boost inexact same-language matches
+ list ($language) = explode("_", $requested_locale . "_");
+ if (!isset($locale_preferences[$language])) {
+ $locale_preferences[$language] = array();
+ }
+ $locale_preferences[$language][$requested_locale] = $qvalue;
+ }
+ }
+
+ // Compare and score requested locales with installed ones
+ $scored_locales = array();
+ foreach ($locale_preferences as $language => $requested_locales) {
+ // Inexact match adjustment (same language, different region)
+ $fallback_adjustment_factor = 0.95;
+ if (count($requested_locales) > 1) {
+ // Sort by qvalue, descending
+ $qvalues = array_values($requested_locales);
+ rsort($qvalues);
+ // Ensure inexact match scores worse than 2nd preference in same language.
+ $fallback_adjustment_factor *= $qvalues[1];
+ }
+ foreach ($requested_locales as $requested_locale => $qvalue) {
+ list ($matched_locale, $match_score) =
+ self::_locale_match_score($requested_locale, $qvalue, $fallback_adjustment_factor);
+ if ($matched_locale &&
+ (!isset($scored_locales[$matched_locale]) ||
+ $match_score > $scored_locales[$matched_locale])) {
+ $scored_locales[$matched_locale] = $match_score;
+ }
+ }
+ }
+
+ arsort($scored_locales);
+
+ list ($locale) = each($scored_locales);
+ return $locale;
+ }
+
+ return null;
+ }
+
+ private static function _locale_match_score($requested_locale, $qvalue, $adjustment_factor) {
+ $installed = locales::installed();
+ if (isset($installed[$requested_locale])) {
+ return array($requested_locale, $qvalue);
+ }
+ list ($language) = explode("_", $requested_locale . "_");
+ if (isset(self::$language_subtag_to_locale[$language]) &&
+ isset($installed[self::$language_subtag_to_locale[$language]])) {
+ $score = $adjustment_factor * $qvalue;
+ return array(self::$language_subtag_to_locale[$language], $score);
+ }
+ return array(null, 0);
+ }
+
+ static function set_request_locale() {
+ // 1. Check the session specific preference (cookie)
+ $locale = locales::cookie_locale();
+ // 2. Check the user's preference
+ if (!$locale) {
+ $locale = identity::active_user()->locale;
+ }
+ // 3. Check the browser's / OS' preference
+ if (!$locale) {
+ $locale = locales::locale_from_http_request();
+ }
+ // If we have any preference, override the site's default locale
+ if ($locale) {
+ Gallery_I18n::instance()->locale($locale);
+ }
+ }
+
+ static function cookie_locale() {
+ // Can't use Input framework for client side cookies since
+ // they're not signed.
+ $cookie_data = isset($_COOKIE["g_locale"]) ? $_COOKIE["g_locale"] : null;
+ $locale = null;
+ if ($cookie_data) {
+ if (preg_match("/^([a-z]{2,3}(?:_[A-Z]{2})?)$/", trim($cookie_data), $matches)) {
+ $requested_locale = $matches[1];
+ $installed_locales = locales::installed();
+ if (isset($installed_locales[$requested_locale])) {
+ $locale = $requested_locale;
+ }
+ }
+ }
+ return $locale;
+ }
+}
diff --git a/modules/gallery/helpers/log.php b/modules/gallery/helpers/log.php
new file mode 100644
index 0000000..cd554b5
--- /dev/null
+++ b/modules/gallery/helpers/log.php
@@ -0,0 +1,108 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class log_Core {
+ const SUCCESS = 1;
+ const INFO = 2;
+ const WARNING = 3;
+ const ERROR = 4;
+
+ /**
+ * Report a successful event.
+ * @param string $category an arbitrary category we can use to filter log messages
+ * @param string $message a detailed log message
+ * @param string $html an html snippet presented alongside the log message to aid the admin
+ */
+ static function success($category, $message, $html="") {
+ self::_add($category, $message, $html, log::SUCCESS);
+ }
+
+ /**
+ * Report an informational event.
+ * @param string $category an arbitrary category we can use to filter log messages
+ * @param string $message a detailed log message
+ * @param string $html an html snippet presented alongside the log message to aid the admin
+ */
+ static function info($category, $message, $html="") {
+ self::_add($category, $message, $html, log::INFO);
+ }
+
+ /**
+ * Report that something went wrong, not fatal, but worth investigation.
+ * @param string $category an arbitrary category we can use to filter log messages
+ * @param string $message a detailed log message
+ * @param string $html an html snippet presented alongside the log message to aid the admin
+ */
+ static function warning($category, $message, $html="") {
+ self::_add($category, $message, $html, log::WARNING);
+ }
+
+ /**
+ * Report that something went wrong that should be fixed.
+ * @param string $category an arbitrary category we can use to filter log messages
+ * @param string $message a detailed log message
+ * @param string $html an html snippet presented alongside the log message to aid the admin
+ */
+ static function error($category, $message, $html="") {
+ self::_add($category, $message, $html, log::ERROR);
+ }
+
+ /**
+ * Add a log entry.
+ *
+ * @param string $category an arbitrary category we can use to filter log messages
+ * @param string $message a detailed log message
+ * @param integer $severity INFO, WARNING or ERROR
+ * @param string $html an html snippet presented alongside the log message to aid the admin
+ */
+ private static function _add($category, $message, $html, $severity) {
+ $log = ORM::factory("log");
+ $log->category = $category;
+ $log->message = $message;
+ $log->severity = $severity;
+ $log->html = $html;
+ $log->url = substr(url::abs_current(true), 0, 255);
+ $log->referer = request::referrer(null);
+ $log->timestamp = time();
+ $log->user_id = identity::active_user()->id;
+ $log->save();
+ }
+
+
+ /**
+ * Convert a message severity to a CSS class
+ * @param integer $severity
+ * @return string
+ */
+ static function severity_class($severity) {
+ switch($severity) {
+ case log::SUCCESS:
+ return "g-success";
+
+ case log::INFO:
+ return "g-info";
+
+ case log::WARNING:
+ return "g-warning";
+
+ case log::ERROR:
+ return "g-error";
+ }
+ }
+}
diff --git a/modules/gallery/helpers/message.php b/modules/gallery/helpers/message.php
new file mode 100644
index 0000000..d109995
--- /dev/null
+++ b/modules/gallery/helpers/message.php
@@ -0,0 +1,109 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class message_Core {
+ const SUCCESS = 1;
+ const INFO = 2;
+ const WARNING = 3;
+ const ERROR = 4;
+
+ /**
+ * Report a successful event.
+ * @param string $msg a detailed message
+ */
+ static function success($msg) {
+ self::_add($msg, message::SUCCESS);
+ }
+
+ /**
+ * Report an informational event.
+ * @param string $msg a detailed message
+ */
+ static function info($msg) {
+ self::_add($msg, message::INFO);
+ }
+
+ /**
+ * Report that something went wrong, not fatal, but worth investigation.
+ * @param string $msg a detailed message
+ */
+ static function warning($msg) {
+ self::_add($msg, message::WARNING);
+ }
+
+ /**
+ * Report that something went wrong that should be fixed.
+ * @param string $msg a detailed message
+ */
+ static function error($msg) {
+ self::_add($msg, message::ERROR);
+ }
+
+ /**
+ * Save a message in the session for our next page load.
+ * @param string $msg a detailed message
+ * @param integer $severity one of the severity constants
+ */
+ private static function _add($msg, $severity) {
+ $session = Session::instance();
+ $status = $session->get("messages");
+ $status[] = array($msg, $severity);
+ $session->set("messages", $status);
+ }
+
+ /**
+ * Get any pending messages. There are two types of messages, transient and permanent.
+ * Permanent messages are used to let the admin know that there are pending administrative
+ * issues that need to be resolved. Transient ones are only displayed once.
+ * @return html text
+ */
+ static function get() {
+ $buf = array();
+
+ $messages = Session::instance()->get_once("messages", array());
+ foreach ($messages as $msg) {
+ $msg[0] = str_replace("__CSRF__", access::csrf_token(), $msg[0]);
+ $buf[] = "<li class=\"" . message::severity_class($msg[1]) . "\">$msg[0]</li>";
+ }
+ if ($buf) {
+ return "<ul id=\"g-action-status\" class=\"g-message-block\">" . implode("", $buf) . "</ul>";
+ }
+ }
+
+ /**
+ * Convert a message severity to a CSS class
+ * @param integer $severity
+ * @return string
+ */
+ static function severity_class($severity) {
+ switch($severity) {
+ case message::SUCCESS:
+ return "g-success";
+
+ case message::INFO:
+ return "g-info";
+
+ case message::WARNING:
+ return "g-warning";
+
+ case message::ERROR:
+ return "g-error";
+ }
+ }
+}
diff --git a/modules/gallery/helpers/model_cache.php b/modules/gallery/helpers/model_cache.php
new file mode 100644
index 0000000..7cff68d
--- /dev/null
+++ b/modules/gallery/helpers/model_cache.php
@@ -0,0 +1,42 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class model_cache_Core {
+ private static $cache = array();
+
+ static function get($model_name, $id, $field_name="id") {
+ if (TEST_MODE || empty(self::$cache[$model_name][$field_name][$id])) {
+ $model = ORM::factory($model_name)->where($field_name, "=", $id)->find();
+ if (!$model->loaded()) {
+ throw new Exception("@todo MISSING_MODEL $model_name:$id");
+ }
+ self::$cache[$model_name][$field_name][$id] = $model;
+ }
+
+ return self::$cache[$model_name][$field_name][$id];
+ }
+
+ static function clear() {
+ self::$cache = array();
+ }
+
+ static function set($model) {
+ self::$cache[$model->object_name][$model->primary_key][$model->{$model->primary_key}] = $model;
+ }
+}
diff --git a/modules/gallery/helpers/module.php b/modules/gallery/helpers/module.php
new file mode 100644
index 0000000..1b6c8d1
--- /dev/null
+++ b/modules/gallery/helpers/module.php
@@ -0,0 +1,594 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * This is the API for handling modules.
+ *
+ * Note: by design, this class does not do any permission checking.
+ */
+class module_Core {
+ public static $active = array();
+ public static $modules = array();
+ public static $var_cache = null;
+ public static $available = array();
+
+ /**
+ * Set the version of the corresponding Module_Model
+ * @param string $module_name
+ * @param integer $version
+ */
+ static function set_version($module_name, $version) {
+ $module = module::get($module_name);
+ if (!$module->loaded()) {
+ $module->name = $module_name;
+ $module->active = $module_name == "gallery"; // only gallery is active by default
+ }
+ $module->version = $version;
+ $module->save();
+ Kohana_Log::add("debug", "$module_name: version is now $version");
+ }
+
+ /**
+ * Load the corresponding Module_Model
+ * @param string $module_name
+ */
+ static function get($module_name) {
+ if (empty(self::$modules[$module_name])) {
+ return ORM::factory("module")->where("name", "=", $module_name)->find();
+ }
+ return self::$modules[$module_name];
+ }
+
+ /**
+ * Get the information about a module
+ * @returns ArrayObject containing the module information from the module.info file or false if
+ * not found
+ */
+ static function info($module_name) {
+ $module_list = module::available();
+ return isset($module_list->$module_name) ? $module_list->$module_name : false;
+ }
+
+ /**
+ * Check to see if a module is installed
+ * @param string $module_name
+ */
+ static function is_installed($module_name) {
+ return array_key_exists($module_name, self::$modules);
+ }
+
+ /**
+ * Check to see if a module is active
+ * @param string $module_name
+ */
+ static function is_active($module_name) {
+ return array_key_exists($module_name, self::$modules) &&
+ self::$modules[$module_name]->active;
+ }
+
+ /**
+ * Return the list of available modules, including uninstalled modules.
+ */
+ static function available() {
+ if (empty(self::$available)) {
+ $modules = new ArrayObject(array(), ArrayObject::ARRAY_AS_PROPS);
+ foreach (glob(MODPATH . "*/module.info") as $file) {
+ $module_name = basename(dirname($file));
+ $modules->$module_name =
+ new ArrayObject(parse_ini_file($file), ArrayObject::ARRAY_AS_PROPS);
+ $m =& $modules->$module_name;
+ $m->installed = module::is_installed($module_name);
+ $m->active = module::is_active($module_name);
+ $m->code_version = $m->version;
+ $m->version = module::get_version($module_name);
+ $m->locked = false;
+
+ if ($m->active && $m->version != $m->code_version) {
+ site_status::warning(t("Some of your modules are out of date. <a href=\"%upgrader_url\">Upgrade now!</a>", array("upgrader_url" => url::abs_site("upgrader"))), "upgrade_now");
+ }
+ }
+
+ // Lock certain modules
+ $modules->gallery->locked = true;
+ $identity_module = module::get_var("gallery", "identity_provider", "user");
+ $modules->$identity_module->locked = true;
+
+ $modules->uasort(array("module", "module_comparator"));
+ self::$available = $modules;
+ }
+
+ return self::$available;
+ }
+
+ /**
+ * Natural name sort comparator
+ */
+ static function module_comparator($a, $b) {
+ return strnatcasecmp($a->name, $b->name);
+ }
+
+ /**
+ * Return a list of all the active modules in no particular order.
+ */
+ static function active() {
+ return self::$active;
+ }
+
+ /**
+ * Check that the module can be activated. (i.e. all the prerequistes exist)
+ * @param string $module_name
+ * @return array an array of warning or error messages to be displayed
+ */
+ static function can_activate($module_name) {
+ module::_add_to_path($module_name);
+ $messages = array();
+
+ $installer_class = "{$module_name}_installer";
+ if (class_exists($installer_class) && method_exists($installer_class, "can_activate")) {
+ $messages = call_user_func(array($installer_class, "can_activate"));
+ }
+
+ // Remove it from the active path
+ module::_remove_from_path($module_name);
+ return $messages;
+ }
+
+ /**
+ * Allow modules to indicate the impact of deactivating the specified module
+ * @param string $module_name
+ * @return array an array of warning or error messages to be displayed
+ */
+ static function can_deactivate($module_name) {
+ $data = (object)array("module" => $module_name, "messages" => array());
+
+ module::event("pre_deactivate", $data);
+
+ return $data->messages;
+ }
+
+ /**
+ * Install a module. This will call <module>_installer::install(), which is responsible for
+ * creating database tables, setting module variables and calling module::set_version().
+ * Note that after installing, the module must be activated before it is available for use.
+ * @param string $module_name
+ */
+ static function install($module_name) {
+ module::_add_to_path($module_name);
+
+ $installer_class = "{$module_name}_installer";
+ if (class_exists($installer_class) && method_exists($installer_class, "install")) {
+ call_user_func_array(array($installer_class, "install"), array());
+ }
+ module::set_version($module_name, module::available()->$module_name->code_version);
+
+ // Set the weight of the new module, which controls the order in which the modules are
+ // loaded. By default, new modules are installed at the end of the priority list. Since the
+ // id field is monotonically increasing, the easiest way to guarantee that is to set the weight
+ // the same as the id. We don't know that until we save it for the first time
+ $module = ORM::factory("module")->where("name", "=", $module_name)->find();
+ if ($module->loaded()) {
+ $module->weight = $module->id;
+ $module->save();
+ }
+ module::load_modules();
+
+ // Now the module is installed but inactive, so don't leave it in the active path
+ module::_remove_from_path($module_name);
+
+ log::success(
+ "module", t("Installed module %module_name", array("module_name" => $module_name)));
+ }
+
+ private static function _add_to_path($module_name) {
+ $config = Kohana_Config::instance();
+ $kohana_modules = $config->get("core.modules");
+ array_unshift($kohana_modules, MODPATH . $module_name);
+ $config->set("core.modules", $kohana_modules);
+ // Rebuild the include path so the module installer can benefit from auto loading
+ Kohana::include_paths(true);
+ }
+
+ private static function _remove_from_path($module_name) {
+ $config = Kohana_Config::instance();
+ $kohana_modules = $config->get("core.modules");
+ if (($key = array_search(MODPATH . $module_name, $kohana_modules)) !== false) {
+ unset($kohana_modules[$key]);
+ $kohana_modules = array_values($kohana_modules); // reindex
+ }
+ $config->set("core.modules", $kohana_modules);
+ Kohana::include_paths(true);
+ }
+
+ /**
+ * Upgrade a module. This will call <module>_installer::upgrade(), which is responsible for
+ * modifying database tables, changing module variables and calling module::set_version().
+ * Note that after upgrading, the module must be activated before it is available for use.
+ * @param string $module_name
+ */
+ static function upgrade($module_name) {
+ $version_before = module::get_version($module_name);
+ $installer_class = "{$module_name}_installer";
+ $available = module::available();
+ if (class_exists($installer_class) && method_exists($installer_class, "upgrade")) {
+ call_user_func_array(array($installer_class, "upgrade"), array($version_before));
+ } else {
+ if (isset($available->$module_name->code_version)) {
+ module::set_version($module_name, $available->$module_name->code_version);
+ } else {
+ throw new Exception("@todo UNKNOWN_MODULE");
+ }
+ }
+ module::load_modules();
+
+ $version_after = module::get_version($module_name);
+ if ($version_before != $version_after) {
+ log::success(
+ "module", t("Upgraded module %module_name from %version_before to %version_after",
+ array("module_name" => $module_name,
+ "version_before" => $version_before,
+ "version_after" => $version_after)));
+ }
+
+ if ($version_after != $available->$module_name->code_version) {
+ throw new Exception("@todo MODULE_FAILED_TO_UPGRADE");
+ }
+ }
+
+ /**
+ * Activate an installed module. This will call <module>_installer::activate() which should take
+ * any steps to make sure that the module is ready for use. This will also activate any
+ * existing graphics rules for this module.
+ * @param string $module_name
+ */
+ static function activate($module_name) {
+ module::_add_to_path($module_name);
+
+ $installer_class = "{$module_name}_installer";
+ if (class_exists($installer_class) && method_exists($installer_class, "activate")) {
+ call_user_func_array(array($installer_class, "activate"), array());
+ }
+
+ $module = module::get($module_name);
+ if ($module->loaded()) {
+ $module->active = true;
+ $module->save();
+ }
+ module::load_modules();
+
+ graphics::activate_rules($module_name);
+
+ block_manager::activate_blocks($module_name);
+
+ log::success(
+ "module", t("Activated module %module_name", array("module_name" => $module_name)));
+ }
+
+ /**
+ * Deactivate an installed module. This will call <module>_installer::deactivate() which should
+ * take any cleanup steps to make sure that the module isn't visible in any way. Note that the
+ * module remains available in Kohana's cascading file system until the end of the request!
+ * @param string $module_name
+ */
+ static function deactivate($module_name) {
+ $installer_class = "{$module_name}_installer";
+ if (class_exists($installer_class) && method_exists($installer_class, "deactivate")) {
+ call_user_func_array(array($installer_class, "deactivate"), array());
+ }
+
+ $module = module::get($module_name);
+ if ($module->loaded()) {
+ $module->active = false;
+ $module->save();
+ }
+ module::load_modules();
+
+ graphics::deactivate_rules($module_name);
+
+ block_manager::deactivate_blocks($module_name);
+
+ if (module::info($module_name)) {
+ log::success(
+ "module", t("Deactivated module %module_name", array("module_name" => $module_name)));
+ } else {
+ log::success(
+ "module", t("Deactivated missing module %module_name", array("module_name" => $module_name)));
+ }
+ }
+
+ /**
+ * Deactivate modules that are unavailable or missing, yet still active.
+ * This happens when a user deletes a module without deactivating it.
+ */
+ static function deactivate_missing_modules() {
+ foreach (self::$modules as $module_name => $module) {
+ if (module::is_active($module_name) && !module::info($module_name)) {
+ module::deactivate($module_name);
+ }
+ }
+ }
+
+ /**
+ * Uninstall a deactivated module. This will call <module>_installer::uninstall() which should
+ * take whatever steps necessary to make sure that all traces of a module are gone.
+ * @param string $module_name
+ */
+ static function uninstall($module_name) {
+ $installer_class = "{$module_name}_installer";
+ if (class_exists($installer_class) && method_exists($installer_class, "uninstall")) {
+ call_user_func(array($installer_class, "uninstall"));
+ }
+
+ graphics::remove_rules($module_name);
+ $module = module::get($module_name);
+ if ($module->loaded()) {
+ $module->delete();
+ }
+ module::load_modules();
+
+ // We could delete the module vars here too, but it's nice to leave them around
+ // in case the module gets reinstalled.
+
+ log::success(
+ "module", t("Uninstalled module %module_name", array("module_name" => $module_name)));
+ }
+
+ /**
+ * Load the active modules. This is called at bootstrap time.
+ */
+ static function load_modules() {
+ self::$modules = array();
+ self::$active = array();
+ $kohana_modules = array();
+
+ // In version 32 we introduced a weight column so we can specify the module order
+ // If we try to use that blindly, we'll break earlier versions before they can even
+ // run the upgrader.
+ $modules = module::get_version("gallery") < 32 ?
+ ORM::factory("module")->find_all():
+ ORM::factory("module")->order_by("weight")->find_all();
+
+ foreach ($modules as $module) {
+ self::$modules[$module->name] = $module;
+ if (!$module->active) {
+ continue;
+ }
+
+ if ($module->name == "gallery") {
+ $gallery = $module;
+ } else {
+ self::$active[] = $module;
+ $kohana_modules[] = MODPATH . $module->name;
+ }
+ }
+ self::$active[] = $gallery; // put gallery last in the module list to match core.modules
+ $config = Kohana_Config::instance();
+ $config->set(
+ "core.modules", array_merge($kohana_modules, $config->get("core.modules")));
+ }
+
+ /**
+ * Run a specific event on all active modules.
+ * @param string $name the event name
+ * @param mixed $data data to pass to each event handler
+ */
+ static function event($name, &$data=null) {
+ $args = func_get_args();
+ array_shift($args);
+ $function = str_replace(".", "_", $name);
+
+ if (method_exists("gallery_event", $function)) {
+ switch (count($args)) {
+ case 0:
+ gallery_event::$function();
+ break;
+ case 1:
+ gallery_event::$function($args[0]);
+ break;
+ case 2:
+ gallery_event::$function($args[0], $args[1]);
+ break;
+ case 3:
+ gallery_event::$function($args[0], $args[1], $args[2]);
+ break;
+ case 4: // Context menu events have 4 arguments so lets optimize them
+ gallery_event::$function($args[0], $args[1], $args[2], $args[3]);
+ break;
+ default:
+ call_user_func_array(array("gallery_event", $function), $args);
+ }
+ }
+
+ foreach (self::$active as $module) {
+ if ($module->name == "gallery") {
+ continue;
+ }
+ $class = "{$module->name}_event";
+ if (class_exists($class) && method_exists($class, $function)) {
+ call_user_func_array(array($class, $function), $args);
+ }
+ }
+
+ // Give the admin theme a chance to respond, if we're in admin mode.
+ if (theme::$is_admin) {
+ $class = theme::$admin_theme_name . "_event";
+ if (class_exists($class) && method_exists($class, $function)) {
+ call_user_func_array(array($class, $function), $args);
+ }
+ }
+
+ // Give the site theme a chance to respond as well. It gets a chance even in admin mode, as
+ // long as the theme has an admin subdir.
+ $class = theme::$site_theme_name . "_event";
+ if (class_exists($class) && method_exists($class, $function)) {
+ call_user_func_array(array($class, $function), $args);
+ }
+ }
+
+ /**
+ * Get a variable from this module
+ * @param string $module_name
+ * @param string $name
+ * @param string $default_value
+ * @return the value
+ */
+ static function get_var($module_name, $name, $default_value=null) {
+ // We cache vars so we can load them all at once for performance.
+ if (empty(self::$var_cache)) {
+ self::$var_cache = Cache::instance()->get("var_cache");
+ if (empty(self::$var_cache)) {
+ // Cache doesn't exist, create it now.
+ foreach (db::build()
+ ->select("module_name", "name", "value")
+ ->from("vars")
+ ->order_by("module_name")
+ ->order_by("name")
+ ->execute() as $row) {
+ // Mute the "Creating default object from empty value" warning below
+ @self::$var_cache->{$row->module_name}->{$row->name} = $row->value;
+ }
+ Cache::instance()->set("var_cache", self::$var_cache, array("vars"));
+ }
+ }
+
+ if (isset(self::$var_cache->$module_name->$name)) {
+ return self::$var_cache->$module_name->$name;
+ } else {
+ return $default_value;
+ }
+ }
+
+ /**
+ * Store a variable for this module
+ * @param string $module_name
+ * @param string $name
+ * @param string $value
+ */
+ static function set_var($module_name, $name, $value) {
+ $var = ORM::factory("var")
+ ->where("module_name", "=", $module_name)
+ ->where("name", "=", $name)
+ ->find();
+ if (!$var->loaded()) {
+ $var->module_name = $module_name;
+ $var->name = $name;
+ }
+ $var->value = $value;
+ $var->save();
+
+ Cache::instance()->delete("var_cache");
+ self::$var_cache = null;
+ }
+
+ /**
+ * Increment the value of a variable for this module
+ *
+ * Note: Frequently updating counters is very inefficient because it invalidates the cache value
+ * which has to be rebuilt every time we make a change.
+ *
+ * @todo Get rid of this and find an alternate approach for all callers (currently only Akismet)
+ *
+ * @deprecated
+ * @param string $module_name
+ * @param string $name
+ * @param string $increment (optional, default is 1)
+ */
+ static function incr_var($module_name, $name, $increment=1) {
+ db::build()
+ ->update("vars")
+ ->set("value", db::expr("`value` + $increment"))
+ ->where("module_name", "=", $module_name)
+ ->where("name", "=", $name)
+ ->execute();
+
+ Cache::instance()->delete("var_cache");
+ self::$var_cache = null;
+ }
+
+ /**
+ * Remove a variable for this module.
+ * @param string $module_name
+ * @param string $name
+ */
+ static function clear_var($module_name, $name) {
+ db::build()
+ ->delete("vars")
+ ->where("module_name", "=", $module_name)
+ ->where("name", "=", $name)
+ ->execute();
+
+ Cache::instance()->delete("var_cache");
+ self::$var_cache = null;
+ }
+
+ /**
+ * Remove all variables for this module.
+ * @param string $module_name
+ */
+ static function clear_all_vars($module_name) {
+ db::build()
+ ->delete("vars")
+ ->where("module_name", "=", $module_name)
+ ->execute();
+
+ Cache::instance()->delete("var_cache");
+ self::$var_cache = null;
+ }
+
+ /**
+ * Return the version of the installed module.
+ * @param string $module_name
+ */
+ static function get_version($module_name) {
+ return module::get($module_name)->version;
+ }
+
+ /**
+ * Check if obsolete modules are active and, if so, return a warning message.
+ * If none are found, return null.
+ */
+ static function get_obsolete_modules_message() {
+ // This is the obsolete modules list. Any active module that's on the list
+ // with version number at or below the one given will be considered obsolete.
+ // It is hard-coded here, and may be updated with future releases of Gallery.
+ $obsolete_modules = array("videos" => 4, "noffmpeg" => 1, "videodimensions" => 1,
+ "digibug" => 2);
+
+ // Before we check the active modules, deactivate any that are missing.
+ module::deactivate_missing_modules();
+
+ $modules_found = array();
+ foreach ($obsolete_modules as $module => $version) {
+ if (module::is_active($module) && (module::get_version($module) <= $version)) {
+ $modules_found[] = $module;
+ }
+ }
+
+ if ($modules_found) {
+ // Need this to be on one super-long line or else the localization scanner may not work.
+ // (ref: http://sourceforge.net/apps/trac/gallery/ticket/1321)
+ return t("Recent upgrades to Gallery have made the following modules obsolete: %modules. We recommend that you <a href=\"%url_mod\">deactivate</a> the module(s). For more information, please see the <a href=\"%url_doc\">documentation page</a>.",
+ array("modules" => implode(", ", $modules_found),
+ "url_mod" => url::site("admin/modules"),
+ "url_doc" => "http://codex.galleryproject.org/Gallery3:User_guide:Obsolete_modules"));
+ }
+
+ return null;
+ }
+}
diff --git a/modules/gallery/helpers/movie.php b/modules/gallery/helpers/movie.php
new file mode 100644
index 0000000..2f19088
--- /dev/null
+++ b/modules/gallery/helpers/movie.php
@@ -0,0 +1,282 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * This is the API for handling movies.
+ *
+ * Note: by design, this class does not do any permission checking.
+ */
+class movie_Core {
+ private static $allow_uploads;
+
+ static function get_edit_form($movie) {
+ $form = new Forge("movies/update/$movie->id", "", "post", array("id" => "g-edit-movie-form"));
+ $form->hidden("from_id")->value($movie->id);
+ $group = $form->group("edit_item")->label(t("Edit Movie"));
+ $group->input("title")->label(t("Title"))->value($movie->title)
+ ->error_messages("required", t("You must provide a title"))
+ ->error_messages("length", t("Your title is too long"));
+ $group->textarea("description")->label(t("Description"))->value($movie->description);
+ $group->input("name")->label(t("Filename"))->value($movie->name)
+ ->error_messages(
+ "conflict", t("There is already a movie, photo or album with this name"))
+ ->error_messages("no_slashes", t("The movie name can't contain a \"/\""))
+ ->error_messages("no_trailing_period", t("The movie name can't end in \".\""))
+ ->error_messages("illegal_data_file_extension", t("You cannot change the movie file extension"))
+ ->error_messages("required", t("You must provide a movie file name"))
+ ->error_messages("length", t("Your movie file name is too long"));
+ $group->input("slug")->label(t("Internet Address"))->value($movie->slug)
+ ->error_messages(
+ "conflict", t("There is already a movie, photo or album with this internet address"))
+ ->error_messages(
+ "not_url_safe",
+ t("The internet address should contain only letters, numbers, hyphens and underscores"))
+ ->error_messages("required", t("You must provide an internet address"))
+ ->error_messages("length", t("Your internet address is too long"));
+
+ module::event("item_edit_form", $movie, $form);
+
+ $group = $form->group("buttons")->label("");
+ $group->submit("")->value(t("Modify"));
+
+ return $form;
+ }
+
+ /**
+ * Extract a frame from a movie file. Valid movie_options are start_time (in seconds),
+ * input_args (extra ffmpeg input args) and output_args (extra ffmpeg output args). Extra args
+ * are added at the end of the list, so they can override any prior args.
+ *
+ * @param string $input_file
+ * @param string $output_file
+ * @param array $movie_options (optional)
+ */
+ static function extract_frame($input_file, $output_file, $movie_options=null) {
+ $ffmpeg = movie::find_ffmpeg();
+ if (empty($ffmpeg)) {
+ throw new Exception("@todo MISSING_FFMPEG");
+ }
+
+ list($width, $height, $mime_type, $extension, $duration) = movie::get_file_metadata($input_file);
+
+ if (isset($movie_options["start_time"]) && is_numeric($movie_options["start_time"])) {
+ $start_time = max(0, $movie_options["start_time"]); // ensure it's non-negative
+ } else {
+ $start_time = module::get_var("gallery", "movie_extract_frame_time", 3); // use default
+ }
+ // extract frame at start_time, unless movie is too short
+ $start_time_arg = ($duration >= $start_time + 0.1) ?
+ "-ss " . movie::seconds_to_hhmmssdd($start_time) : "";
+
+ $input_args = isset($movie_options["input_args"]) ? $movie_options["input_args"] : "";
+ $output_args = isset($movie_options["output_args"]) ? $movie_options["output_args"] : "";
+
+ $cmd = escapeshellcmd($ffmpeg) . " $input_args -i " . escapeshellarg($input_file) .
+ " -an $start_time_arg -an -r 1 -vframes 1" .
+ " -s {$width}x{$height}" .
+ " -y -f mjpeg $output_args " . escapeshellarg($output_file) . " 2>&1";
+ exec($cmd, $exec_output, $exec_return);
+
+ clearstatcache(); // use $filename parameter when PHP_version is 5.3+
+ if (filesize($output_file) == 0 || $exec_return) {
+ // Maybe the movie needs the "-threads 1" argument added
+ // (see http://sourceforge.net/apps/trac/gallery/ticket/1924)
+ $cmd = escapeshellcmd($ffmpeg) . " -threads 1 $input_args -i " . escapeshellarg($input_file) .
+ " -an $start_time_arg -an -r 1 -vframes 1" .
+ " -s {$width}x{$height}" .
+ " -y -f mjpeg $output_args " . escapeshellarg($output_file) . " 2>&1";
+ exec($cmd, $exec_output, $exec_return);
+
+ clearstatcache();
+ if (filesize($output_file) == 0 || $exec_return) {
+ throw new Exception("@todo FFMPEG_FAILED");
+ }
+ }
+ }
+
+ /**
+ * Return true if movie uploads are allowed, false if not. This is based on the
+ * "movie_allow_uploads" Gallery variable as well as whether or not ffmpeg is found.
+ */
+ static function allow_uploads() {
+ if (empty(self::$allow_uploads)) {
+ // Refresh ffmpeg settings
+ $ffmpeg = movie::find_ffmpeg();
+ switch (module::get_var("gallery", "movie_allow_uploads", "autodetect")) {
+ case "always":
+ self::$allow_uploads = true;
+ break;
+ case "never":
+ self::$allow_uploads = false;
+ break;
+ default:
+ self::$allow_uploads = !empty($ffmpeg);
+ break;
+ }
+ }
+ return self::$allow_uploads;
+ }
+
+ /**
+ * Return the path to the ffmpeg binary if one exists and is executable, or null.
+ */
+ static function find_ffmpeg() {
+ if (!($ffmpeg_path = module::get_var("gallery", "ffmpeg_path")) ||
+ !@is_executable($ffmpeg_path)) {
+ $ffmpeg_path = system::find_binary(
+ "ffmpeg", module::get_var("gallery", "graphics_toolkit_path"));
+ module::set_var("gallery", "ffmpeg_path", $ffmpeg_path);
+ }
+ return $ffmpeg_path;
+ }
+
+ /**
+ * Return version number and build date of ffmpeg if found, empty string(s) if not. When using
+ * static builds that aren't official releases, the version numbers are strange, hence why the
+ * date can be useful.
+ */
+ static function get_ffmpeg_version() {
+ $ffmpeg = movie::find_ffmpeg();
+ if (empty($ffmpeg)) {
+ return array("", "");
+ }
+
+ // Find version using -h argument since -version wasn't available in early versions.
+ // To keep the preg_match searches quick, we'll trim the (otherwise long) result.
+ $cmd = escapeshellcmd($ffmpeg) . " -h 2>&1";
+ $result = substr(`$cmd`, 0, 1000);
+ if (preg_match("/ffmpeg version (\S+)/i", $result, $matches_version)) {
+ // Version number found - see if we can get the build date or copyright year as well.
+ if (preg_match("/built on (\S+\s\S+\s\S+)/i", $result, $matches_build_date)) {
+ return array(trim($matches_version[1], ","), trim($matches_build_date[1], ","));
+ } else if (preg_match("/copyright \S*\s?2000-(\d{4})/i", $result, $matches_copyright_date)) {
+ return array(trim($matches_version[1], ","), $matches_copyright_date[1]);
+ } else {
+ return array(trim($matches_version[1], ","), "");
+ }
+ }
+ return array("", "");
+ }
+
+ /**
+ * Return the width, height, mime_type, extension and duration of the given movie file.
+ * Metadata is first generated using ffmpeg (or set to defaults if it fails),
+ * then can be modified by other modules using movie_get_file_metadata events.
+ *
+ * This function and its use cases are symmetric to those of photo::get_file_metadata.
+ *
+ * @param string $file_path
+ * @return array array($width, $height, $mime_type, $extension, $duration)
+ *
+ * Use cases in detail:
+ * Input is standard movie type (flv/mp4/m4v)
+ * -> return metadata from ffmpeg
+ * Input is *not* standard movie type that is supported by ffmpeg (e.g. avi, mts...)
+ * -> return metadata from ffmpeg
+ * Input is *not* standard movie type that is *not* supported by ffmpeg but is legal
+ * -> return zero width, height, and duration; mime type and extension according to legal_file
+ * Input is illegal, unidentifiable, unreadable, or does not exist
+ * -> throw exception
+ * Note: movie_get_file_metadata events can change any of the above cases (except the last one).
+ */
+ static function get_file_metadata($file_path) {
+ if (!is_readable($file_path)) {
+ throw new Exception("@todo UNREADABLE_FILE");
+ }
+
+ $metadata = new stdClass();
+ $ffmpeg = movie::find_ffmpeg();
+ if (!empty($ffmpeg)) {
+ // ffmpeg found - use it to get width, height, and duration.
+ $cmd = escapeshellcmd($ffmpeg) . " -i " . escapeshellarg($file_path) . " 2>&1";
+ $result = `$cmd`;
+ if (preg_match("/Stream.*?Video:.*?, (\d+)x(\d+)/", $result, $matches_res)) {
+ if (preg_match("/Stream.*?Video:.*? \[.*?DAR (\d+):(\d+).*?\]/", $result, $matches_dar) &&
+ $matches_dar[1] >= 1 && $matches_dar[2] >= 1) {
+ // DAR is defined - determine width based on height and DAR
+ // (should always be int, but adding round to be sure)
+ $matches_res[1] = round($matches_res[2] * $matches_dar[1] / $matches_dar[2]);
+ }
+ list ($metadata->width, $metadata->height) = array($matches_res[1], $matches_res[2]);
+ } else {
+ list ($metadata->width, $metadata->height) = array(0, 0);
+ }
+
+ if (preg_match("/Duration: (\d+:\d+:\d+\.\d+)/", $result, $matches)) {
+ $metadata->duration = movie::hhmmssdd_to_seconds($matches[1]);
+ } else if (preg_match("/duration.*?:.*?(\d+)/", $result, $matches)) {
+ $metadata->duration = $matches[1];
+ } else {
+ $metadata->duration = 0;
+ }
+ } else {
+ // ffmpeg not found - set width, height, and duration to zero.
+ $metadata->width = 0;
+ $metadata->height = 0;
+ $metadata->duration = 0;
+ }
+
+ $extension = pathinfo($file_path, PATHINFO_EXTENSION);
+ if (!$extension ||
+ (!$metadata->mime_type = legal_file::get_movie_types_by_extension($extension))) {
+ // Extension is empty or illegal.
+ $metadata->extension = null;
+ $metadata->mime_type = null;
+ } else {
+ // Extension is legal (and mime is already set above).
+ $metadata->extension = strtolower($extension);
+ }
+
+ // Run movie_get_file_metadata events which can modify the class.
+ module::event("movie_get_file_metadata", $file_path, $metadata);
+
+ // If the post-events results are invalid, throw an exception. Note that, unlike photos, having
+ // zero width and height isn't considered invalid (as is the case when FFmpeg isn't installed).
+ if (!$metadata->mime_type || !$metadata->extension ||
+ ($metadata->mime_type != legal_file::get_movie_types_by_extension($metadata->extension))) {
+ throw new Exception("@todo ILLEGAL_OR_UNINDENTIFIABLE_FILE");
+ }
+
+ return array($metadata->width, $metadata->height, $metadata->mime_type,
+ $metadata->extension, $metadata->duration);
+ }
+
+ /**
+ * Return the time/duration formatted in hh:mm:ss.dd from a number of seconds.
+ * Useful for inputs to ffmpeg.
+ *
+ * Note that this is similar to date("H:i:s", mktime(0,0,$seconds,0,0,0,0)), but unlike this
+ * approach avoids potential issues with time zone and DST mismatch and/or using deprecated
+ * features (the last argument of mkdate above, which disables DST, is deprecated as of PHP 5.3).
+ */
+ static function seconds_to_hhmmssdd($seconds) {
+ return sprintf("%02d:%02d:%05.2f", floor($seconds / 3600), floor(($seconds % 3600) / 60),
+ floor(100 * $seconds % 6000) / 100);
+ }
+
+ /**
+ * Return the number of seconds from a time/duration formatted in hh:mm:ss.dd.
+ * Useful for outputs from ffmpeg.
+ */
+ static function hhmmssdd_to_seconds($hhmmssdd) {
+ preg_match("/(\d+):(\d+):(\d+\.\d+)/", $hhmmssdd, $matches);
+ return 3600 * $matches[1] + 60 * $matches[2] + $matches[3];
+ }
+}
diff --git a/modules/gallery/helpers/photo.php b/modules/gallery/helpers/photo.php
new file mode 100644
index 0000000..004cc7c
--- /dev/null
+++ b/modules/gallery/helpers/photo.php
@@ -0,0 +1,145 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * This is the API for handling photos.
+ *
+ * Note: by design, this class does not do any permission checking.
+ */
+class photo_Core {
+ static function get_edit_form($photo) {
+ $form = new Forge("photos/update/$photo->id", "", "post", array("id" => "g-edit-photo-form"));
+ $form->hidden("from_id")->value($photo->id);
+ $group = $form->group("edit_item")->label(t("Edit Photo"));
+ $group->input("title")->label(t("Title"))->value($photo->title)
+ ->error_messages("required", t("You must provide a title"))
+ ->error_messages("length", t("Your title is too long"));
+ $group->textarea("description")->label(t("Description"))->value($photo->description);
+ $group->input("name")->label(t("Filename"))->value($photo->name)
+ ->error_messages("conflict", t("There is already a movie, photo or album with this name"))
+ ->error_messages("no_slashes", t("The photo name can't contain a \"/\""))
+ ->error_messages("no_trailing_period", t("The photo name can't end in \".\""))
+ ->error_messages("illegal_data_file_extension", t("You cannot change the photo file extension"))
+ ->error_messages("required", t("You must provide a photo file name"))
+ ->error_messages("length", t("Your photo file name is too long"));
+ $group->input("slug")->label(t("Internet Address"))->value($photo->slug)
+ ->error_messages(
+ "conflict", t("There is already a movie, photo or album with this internet address"))
+ ->error_messages(
+ "not_url_safe",
+ t("The internet address should contain only letters, numbers, hyphens and underscores"))
+ ->error_messages("required", t("You must provide an internet address"))
+ ->error_messages("length", t("Your internet address is too long"));
+
+ module::event("item_edit_form", $photo, $form);
+
+ $group = $form->group("buttons")->label("");
+ $group->submit("")->value(t("Modify"));
+ return $form;
+ }
+
+ /**
+ * Return scaled width and height.
+ *
+ * @param integer $width
+ * @param integer $height
+ * @param integer $max the target size for the largest dimension
+ * @param string $format the output format using %d placeholders for width and height
+ */
+ static function img_dimensions($width, $height, $max, $format="width=\"%d\" height=\"%d\"") {
+ if (!$width || !$height) {
+ return "";
+ }
+
+ if ($width > $height) {
+ $new_width = $max;
+ $new_height = (int)$max * ($height / $width);
+ } else {
+ $new_height = $max;
+ $new_width = (int)$max * ($width / $height);
+ }
+ return sprintf($format, $new_width, $new_height);
+ }
+
+ /**
+ * Return the width, height, mime_type and extension of the given image file.
+ * Metadata is first generated using getimagesize (or the legal_file mapping if it fails),
+ * then can be modified by other modules using photo_get_file_metadata events.
+ *
+ * This function and its use cases are symmetric to those of photo::get_file_metadata.
+ *
+ * @param string $file_path
+ * @return array array($width, $height, $mime_type, $extension)
+ *
+ * Use cases in detail:
+ * Input is standard photo type (jpg/png/gif)
+ * -> return metadata from getimagesize()
+ * Input is *not* standard photo type that is supported by getimagesize (e.g. tif, bmp...)
+ * -> return metadata from getimagesize()
+ * Input is *not* standard photo type that is *not* supported by getimagesize but is legal
+ * -> return metadata if found by photo_get_file_metadata events
+ * Input is illegal, unidentifiable, unreadable, or does not exist
+ * -> throw exception
+ * Note: photo_get_file_metadata events can change any of the above cases (except the last one).
+ */
+ static function get_file_metadata($file_path) {
+ if (!is_readable($file_path)) {
+ throw new Exception("@todo UNREADABLE_FILE");
+ }
+
+ $metadata = new stdClass();
+ if ($image_info = getimagesize($file_path)) {
+ // getimagesize worked - use its results.
+ $metadata->width = $image_info[0];
+ $metadata->height = $image_info[1];
+ $metadata->mime_type = $image_info["mime"];
+ $metadata->extension = image_type_to_extension($image_info[2], false);
+ // We prefer jpg instead of jpeg (which is returned by image_type_to_extension).
+ if ($metadata->extension == "jpeg") {
+ $metadata->extension = "jpg";
+ }
+ } else {
+ // getimagesize failed - try to use legal_file mapping instead.
+ $extension = pathinfo($file_path, PATHINFO_EXTENSION);
+ if (!$extension ||
+ (!$metadata->mime_type = legal_file::get_photo_types_by_extension($extension))) {
+ // Extension is empty or illegal.
+ $metadata->extension = null;
+ $metadata->mime_type = null;
+ } else {
+ // Extension is legal (and mime is already set above).
+ $metadata->extension = strtolower($extension);
+ }
+ $metadata->width = 0;
+ $metadata->height = 0;
+ }
+
+ // Run photo_get_file_metadata events which can modify the class.
+ module::event("photo_get_file_metadata", $file_path, $metadata);
+
+ // If the post-events results are invalid, throw an exception.
+ if (!$metadata->width || !$metadata->height || !$metadata->mime_type || !$metadata->extension ||
+ ($metadata->mime_type != legal_file::get_photo_types_by_extension($metadata->extension))) {
+ throw new Exception("@todo ILLEGAL_OR_UNINDENTIFIABLE_FILE");
+ }
+
+ return array($metadata->width, $metadata->height, $metadata->mime_type, $metadata->extension);
+ }
+}
diff --git a/modules/gallery/helpers/random.php b/modules/gallery/helpers/random.php
new file mode 100644
index 0000000..d40bfb5
--- /dev/null
+++ b/modules/gallery/helpers/random.php
@@ -0,0 +1,48 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+class random_Core {
+ /**
+ * Return a random 32 byte hash value.
+ * @param string extra entropy data
+ */
+ static function hash($length=32) {
+ require_once(MODPATH . "gallery/vendor/joomla/crypt.php");
+ return md5(JCrypt::genRandomBytes($length));
+ }
+
+ /**
+ * Return a random floating point number between 0 and 1
+ */
+ static function percent() {
+ return ((float)mt_rand()) / (float)mt_getrandmax();
+ }
+
+ /**
+ * Return a random number between $min and $max. If $min and $max are not specified,
+ * return a random number between 0 and mt_getrandmax()
+ */
+ static function int($min=null, $max=null) {
+ if ($min || $max) {
+ return mt_rand($min, $max);
+ }
+ return mt_rand();
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/site_status.php b/modules/gallery/helpers/site_status.php
new file mode 100644
index 0000000..e1a8163
--- /dev/null
+++ b/modules/gallery/helpers/site_status.php
@@ -0,0 +1,132 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class site_status_Core {
+ const SUCCESS = 1;
+ const INFO = 2;
+ const WARNING = 3;
+ const ERROR = 4;
+
+ /**
+ * Report a successful event.
+ * @param string $msg a detailed message
+ * @param string $permanent_key make this message permanent and store it under this key
+ */
+ static function success($msg, $permanent_key) {
+ self::_add($msg, self::SUCCESS, $permanent_key);
+ }
+
+ /**
+ * Report an informational event.
+ * @param string $msg a detailed message
+ * @param string $permanent_key make this message permanent and store it under this key
+ */
+ static function info($msg, $permanent_key) {
+ self::_add($msg, self::INFO, $permanent_key);
+ }
+
+ /**
+ * Report that something went wrong, not fatal, but worth investigation.
+ * @param string $msg a detailed message
+ * @param string $permanent_key make this message permanent and store it under this key
+ */
+ static function warning($msg, $permanent_key) {
+ self::_add($msg, self::WARNING, $permanent_key);
+ }
+
+ /**
+ * Report that something went wrong that should be fixed.
+ * @param string $msg a detailed message
+ * @param string $permanent_key make this message permanent and store it under this key
+ */
+ static function error($msg, $permanent_key) {
+ self::_add($msg, self::ERROR, $permanent_key);
+ }
+
+ /**
+ * Save a message in the session for our next page load.
+ * @param string $msg a detailed message
+ * @param integer $severity one of the severity constants
+ * @param string $permanent_key make this message permanent and store it under this key
+ */
+ private static function _add($msg, $severity, $permanent_key) {
+ $message = ORM::factory("message")
+ ->where("key", "=", $permanent_key)
+ ->find();
+ if (!$message->loaded()) {
+ $message->key = $permanent_key;
+ }
+ $message->severity = $severity;
+ $message->value = $msg;
+ $message->save();
+ }
+
+ /**
+ * Remove any permanent message by key.
+ * @param string $permanent_key
+ */
+ static function clear($permanent_key) {
+ $message = ORM::factory("message")->where("key", "=", $permanent_key)->find();
+ if ($message->loaded()) {
+ $message->delete();
+ }
+ }
+
+ /**
+ * Get any pending messages. There are two types of messages, transient and permanent.
+ * Permanent messages are used to let the admin know that there are pending administrative
+ * issues that need to be resolved. Transient ones are only displayed once.
+ * @return html text
+ */
+ static function get() {
+ if (!identity::active_user()->admin) {
+ return;
+ }
+ $buf = array();
+ foreach (ORM::factory("message")->find_all() as $msg) {
+ $value = str_replace("__CSRF__", access::csrf_token(), $msg->value);
+ $buf[] = "<li class=\"" . site_status::severity_class($msg->severity) . "\">$value</li>";
+ }
+
+ if ($buf) {
+ return "<ul id=\"g-site-status\">" . implode("", $buf) . "</ul>";
+ }
+ }
+
+ /**
+ * Convert a message severity to a CSS class
+ * @param integer $severity
+ * @return string
+ */
+ static function severity_class($severity) {
+ switch($severity) {
+ case site_status::SUCCESS:
+ return "g-success";
+
+ case site_status::INFO:
+ return "g-info";
+
+ case site_status::WARNING:
+ return "g-warning";
+
+ case site_status::ERROR:
+ return "g-error";
+ }
+ }
+}
diff --git a/modules/gallery/helpers/system.php b/modules/gallery/helpers/system.php
new file mode 100644
index 0000000..f0879d6
--- /dev/null
+++ b/modules/gallery/helpers/system.php
@@ -0,0 +1,113 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class system_Core {
+ private static $files_marked_for_deletion = array();
+
+ /**
+ * Return the path to an executable version of the named binary, or null.
+ * The paths are traversed in the following order:
+ * 1. $priority_path (if specified)
+ * 2. Gallery's own bin directory (DOCROOT . "bin")
+ * 3. PATH environment variable
+ * 4. extra_binary_paths Gallery variable (if specified)
+ * In addition, if the file is found inside Gallery's bin directory but
+ * it's not executable, we try to change its permissions to 0755.
+ *
+ * @param string $binary
+ * @param string $priority_path (optional)
+ * @return string path to binary if found; null if not found
+ */
+ static function find_binary($binary, $priority_path=null) {
+ $bin_path = DOCROOT . "bin";
+
+ if ($priority_path) {
+ $paths = array($priority_path, $bin_path);
+ } else {
+ $paths = array($bin_path);
+ }
+ $paths = array_merge($paths,
+ explode(":", getenv("PATH")),
+ explode(":", module::get_var("gallery", "extra_binary_paths")));
+
+ foreach ($paths as $path) {
+ $path = rtrim($path, "/");
+ $candidate = "$path/$binary";
+ // @suppress errors below to avoid open_basedir issues
+ if (@file_exists($candidate)) {
+ if (!@is_executable($candidate) &&
+ (substr_compare($bin_path, $candidate, 0, strlen($bin_path)) == 0)) {
+ // Binary isn't executable but is in Gallery's bin directory - try fixing permissions.
+ @chmod($candidate, 0755);
+ }
+ if (@is_executable($candidate)) {
+ return $candidate;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Create a file with a unique file name.
+ * This helper is similar to the built-in tempnam.
+ * It allows the caller to specify a prefix and an extension.
+ * It always places the file in TMPPATH.
+ * Unless specified with the $delete_later argument, it will be marked
+ * for deletion at shutdown using system::delete_later.
+ */
+ static function temp_filename($prefix="", $extension="", $delete_later=true) {
+ do {
+ $basename = tempnam(TMPPATH, $prefix);
+ if (!$basename) {
+ return false;
+ }
+ $filename = "$basename.$extension";
+ $success = !file_exists($filename) && @rename($basename, $filename);
+ if (!$success) {
+ @unlink($basename);
+ }
+ } while (!$success);
+
+ if ($delete_later) {
+ system::delete_later($filename);
+ }
+
+ return $filename;
+ }
+
+ /**
+ * Mark a file for deletion at shutdown time. This is useful for temp files, where we can delay
+ * the deletion time until shutdown to keep page load time quick.
+ */
+ static function delete_later($filename) {
+ self::$files_marked_for_deletion[] = $filename;
+ }
+
+ /**
+ * Delete all files marked using system::delete_later. This is called at gallery shutdown.
+ */
+ static function delete_marked_files() {
+ foreach (self::$files_marked_for_deletion as $filename) {
+ // We want to suppress all errors, as it's possible that some of these
+ // files may have been deleted/moved before we got here.
+ @unlink($filename);
+ }
+ }
+}
diff --git a/modules/gallery/helpers/task.php b/modules/gallery/helpers/task.php
new file mode 100644
index 0000000..5638faf
--- /dev/null
+++ b/modules/gallery/helpers/task.php
@@ -0,0 +1,113 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class task_Core {
+ /**
+ * Get all available tasks
+ */
+ static function get_definitions() {
+ $tasks = array();
+ foreach (module::active() as $module) {
+ $class_name = "{$module->name}_task";
+ if (class_exists($class_name) && method_exists($class_name, "available_tasks")) {
+ foreach (call_user_func(array($class_name, "available_tasks")) as $task) {
+ $tasks[$task->callback] = $task;
+ }
+ }
+ }
+
+ return $tasks;
+ }
+
+ static function start($task_callback, $context=array()) {
+ $tasks = task::get_definitions();
+ $task = task::create($tasks[$task_callback], array());
+
+ $task->log(t("Task %task_name started (task id %task_id)",
+ array("task_name" => $task->name, "task_id" => $task->id)));
+ return $task;
+ }
+
+ static function create($task_def, $context) {
+ $task = ORM::factory("task");
+ $task->callback = $task_def->callback;
+ $task->name = $task_def->name;
+ $task->percent_complete = 0;
+ $task->status = "";
+ $task->state = "started";
+ $task->owner_id = identity::active_user()->id;
+ $task->context = serialize($context);
+ $task->save();
+
+ return $task;
+ }
+
+ static function cancel($task_id) {
+ $task = ORM::factory("task", $task_id);
+ if (!$task->loaded()) {
+ throw new Exception("@todo MISSING_TASK");
+ }
+ $task->done = 1;
+ $task->state = "cancelled";
+ $task->log(t("Task %task_name cancelled (task id %task_id)",
+ array("task_name" => $task->name, "task_id" => $task->id)));
+ $task->save();
+
+ return $task;
+ }
+
+ static function remove($task_id) {
+ $task = ORM::factory("task", $task_id);
+ if ($task->loaded()) {
+ $task->delete();
+ }
+ }
+
+ static function run($task_id) {
+ $task = ORM::factory("task", $task_id);
+ if (!$task->loaded()) {
+ throw new Exception("@todo MISSING_TASK");
+ }
+
+ try {
+ $task->state = "running";
+ call_user_func_array($task->callback, array(&$task));
+ if ($task->done) {
+ $task->log($task->status);
+ }
+ $task->save();
+ } catch (Exception $e) {
+ Kohana_Log::add("error", (string)$e);
+
+ // Ugh. I hate to use instanceof, But this beats catching the exception separately since
+ // we mostly want to treat it the same way as all other exceptions
+ if ($e instanceof ORM_Validation_Exception) {
+ Kohana_Log::add("error", "Validation errors: " . print_r($e->validation->errors(), 1));
+ }
+
+ $task->log((string)$e);
+ $task->state = "error";
+ $task->done = true;
+ $task->status = substr($e->getMessage(), 0, 255);
+ $task->save();
+ }
+
+ return $task;
+ }
+} \ No newline at end of file
diff --git a/modules/gallery/helpers/theme.php b/modules/gallery/helpers/theme.php
new file mode 100644
index 0000000..072f98a
--- /dev/null
+++ b/modules/gallery/helpers/theme.php
@@ -0,0 +1,113 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+/**
+ * This is the API for handling themes.
+ *
+ * Note: by design, this class does not do any permission checking.
+ */
+class theme_Core {
+ public static $admin_theme_name;
+ public static $site_theme_name;
+ public static $is_admin;
+
+ /**
+ * Load the active theme. This is called at bootstrap time. We will only ever have one theme
+ * active for any given request.
+ */
+ static function load_themes() {
+ $input = Input::instance();
+ $path = $input->server("PATH_INFO");
+ if (empty($path)) {
+ $path = "/" . $input->get("kohana_uri");
+ }
+
+ $config = Kohana_Config::instance();
+ $modules = $config->get("core.modules");
+
+ // Normally Router::find_uri() strips off the url suffix for us, but we're working off of the
+ // PATH_INFO here so we need to strip it off manually
+ if ($suffix = Kohana::config("core.url_suffix")) {
+ $path = preg_replace("#" . preg_quote($suffix) . "$#u", "", $path);
+ }
+
+ self::$is_admin = $path == "/admin" || !strncmp($path, "/admin/", 7);
+ self::$site_theme_name = module::get_var("gallery", "active_site_theme");
+
+ // If the site theme doesn't exist, fall back to wind.
+ if (!file_exists(THEMEPATH . self::$site_theme_name . "/theme.info")) {
+ site_status::error(t("Theme '%name' is missing. Falling back to the Wind theme.",
+ array("name" => self::$site_theme_name)), "missing_site_theme");
+ module::set_var("gallery", "active_site_theme", self::$site_theme_name = "wind");
+ }
+
+ if (self::$is_admin) {
+ // Load the admin theme
+ self::$admin_theme_name = module::get_var("gallery", "active_admin_theme");
+
+ // If the admin theme doesn't exist, fall back to admin_wind.
+ if (!file_exists(THEMEPATH . self::$admin_theme_name . "/theme.info")) {
+ site_status::error(t("Admin theme '%name' is missing! Falling back to the Wind theme.",
+ array("name" => self::$admin_theme_name)), "missing_admin_theme");
+ module::set_var("gallery", "active_admin_theme", self::$admin_theme_name = "admin_wind");
+ }
+
+ array_unshift($modules, THEMEPATH . self::$admin_theme_name);
+
+ // If the site theme has an admin subdir, load that as a module so that
+ // themes can provide their own code.
+ if (file_exists(THEMEPATH . self::$site_theme_name . "/admin")) {
+ array_unshift($modules, THEMEPATH . self::$site_theme_name . "/admin");
+ }
+ // Admins can override the site theme, temporarily. This lets us preview themes.
+ if (identity::active_user()->admin && $override = $input->get("theme")) {
+ if (file_exists(THEMEPATH . $override)) {
+ self::$admin_theme_name = $override;
+ array_unshift($modules, THEMEPATH . self::$admin_theme_name);
+ } else {
+ Kohana_Log::add("error", "Missing override admin theme: '$override'");
+ }
+ }
+ } else {
+ // Admins can override the site theme, temporarily. This lets us preview themes.
+ if (identity::active_user()->admin && $override = $input->get("theme")) {
+ if (file_exists(THEMEPATH . $override)) {
+ self::$site_theme_name = $override;
+ } else {
+ Kohana_Log::add("error", "Missing override site theme: '$override'");
+ }
+ }
+ array_unshift($modules, THEMEPATH . self::$site_theme_name);
+ }
+
+ $config->set("core.modules", $modules);
+ }
+
+ static function get_info($theme_name) {
+ $theme_name = preg_replace("/[^a-zA-Z0-9\._-]/", "", $theme_name);
+ $file = THEMEPATH . "$theme_name/theme.info";
+ $theme_info = new ArrayObject(parse_ini_file($file), ArrayObject::ARRAY_AS_PROPS);
+ $theme_info->description = t($theme_info->description);
+ $theme_info->name = t($theme_info->name);
+
+ return $theme_info;
+ }
+}
+
diff --git a/modules/gallery/helpers/tree_rest.php b/modules/gallery/helpers/tree_rest.php
new file mode 100644
index 0000000..5186cf0
--- /dev/null
+++ b/modules/gallery/helpers/tree_rest.php
@@ -0,0 +1,92 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class tree_rest_Core {
+ /**
+ * The tree is rooted in a single item and can have modifiers which adjust what data is shown
+ * for items inside the given tree, up to the depth that you want. The entity for this resource
+ * is a series of items.
+ *
+ * depth=<number>
+ * Only traverse this far down into the tree. If there are more albums
+ * below this depth, provide RESTful urls to other tree resources in
+ * the members section. Default is infinite.
+ *
+ * type=<album|photo|movie>
+ * Restrict the items displayed to the given type. Default is all types.
+ *
+ * fields=<comma separated list of field names>
+ * In the entity section only return these fields for each item.
+ * Default is all fields.
+ */
+ static function get($request) {
+ $item = rest::resolve($request->url);
+ access::required("view", $item);
+
+ $query_params = array();
+ $p = $request->params;
+ $where = array();
+ if (isset($p->type)) {
+ $where[] = array("type", "=", $p->type);
+ $query_params[] = "type={$p->type}";
+ }
+
+ if (isset($p->depth)) {
+ $lowest_depth = $item->level + $p->depth;
+ $where[] = array("level", "<=", $lowest_depth);
+ $query_params[] = "depth={$p->depth}";
+ }
+
+ $fields = array();
+ if (isset($p->fields)) {
+ $fields = explode(",", $p->fields);
+ $query_params[] = "fields={$p->fields}";
+ }
+
+ $entity = array(array("url" => rest::url("item", $item),
+ "entity" => $item->as_restful_array($fields)));
+ $members = array();
+ foreach ($item->viewable()->descendants(null, null, $where) as $child) {
+ $entity[] = array("url" => rest::url("item", $child),
+ "entity" => $child->as_restful_array($fields));
+ if (isset($lowest_depth) && $child->level == $lowest_depth) {
+ $members[] = url::merge_querystring(rest::url("tree", $child), $query_params);
+ }
+ }
+
+ $result = array(
+ "url" => $request->url,
+ "entity" => $entity,
+ "members" => $members,
+ "relationships" => rest::relationships("tree", $item));
+ return $result;
+ }
+
+ static function resolve($id) {
+ $item = ORM::factory("item", $id);
+ if (!access::can("view", $item)) {
+ throw new Kohana_404_Exception();
+ }
+ return $item;
+ }
+
+ static function url($item) {
+ return url::abs_site("rest/tree/{$item->id}");
+ }
+}
diff --git a/modules/gallery/helpers/upgrade_checker.php b/modules/gallery/helpers/upgrade_checker.php
new file mode 100644
index 0000000..492f72e
--- /dev/null
+++ b/modules/gallery/helpers/upgrade_checker.php
@@ -0,0 +1,105 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class upgrade_checker_Core {
+ const CHECK_URL = "http://galleryproject.org/versioncheck/gallery3";
+ const AUTO_CHECK_INTERVAL = 604800; // 7 days in seconds
+
+ /**
+ * Return the last version info blob retrieved from the Gallery website or
+ * null if no checks have been performed.
+ */
+ static function version_info() {
+ return unserialize(Cache::instance()->get("upgrade_checker_version_info"));
+ }
+
+ /**
+ * Return true if auto checking is enabled.
+ */
+ static function auto_check_enabled() {
+ return (bool)module::get_var("gallery", "upgrade_checker_auto_enabled");
+ }
+
+ /**
+ * Return true if it's time to auto check.
+ */
+ static function should_auto_check() {
+ if (upgrade_checker::auto_check_enabled() && random::int(1, 100) == 1) {
+ $version_info = upgrade_checker::version_info();
+ return (!$version_info ||
+ (time() - $version_info->timestamp) > upgrade_checker::AUTO_CHECK_INTERVAL);
+ }
+ return false;
+ }
+
+ /**
+ * Fech version info from the Gallery website.
+ */
+ static function fetch_version_info() {
+ $result = new stdClass();
+ try {
+ list ($status, $headers, $body) = remote::do_request(upgrade_checker::CHECK_URL);
+ if ($status == "HTTP/1.1 200 OK") {
+ $result->status = "success";
+ foreach (explode("\n", $body) as $line) {
+ if ($line) {
+ list($key, $val) = explode("=", $line, 2);
+ $result->data[$key] = $val;
+ }
+ }
+ } else {
+ $result->status = "error";
+ }
+ } catch (Exception $e) {
+ Kohana_Log::add("error",
+ sprintf("%s in %s at line %s:\n%s", $e->getMessage(), $e->getFile(),
+ $e->getLine(), $e->getTraceAsString()));
+ }
+ $result->timestamp = time();
+ Cache::instance()->set("upgrade_checker_version_info", serialize($result),
+ array("upgrade"), 86400 * 365);
+ }
+
+ /**
+ * Check the latest version info blob to see if it's time for an upgrade.
+ */
+ static function get_upgrade_message() {
+ $version_info = upgrade_checker::version_info();
+ if ($version_info) {
+ if (gallery::RELEASE_CHANNEL == "release") {
+ if (version_compare($version_info->data["release_version"], gallery::VERSION, ">")) {
+ return t("A newer version of Gallery is available! <a href=\"%upgrade-url\">Upgrade now</a> to version %version",
+ array("version" => $version_info->data["release_version"],
+ "upgrade-url" => $version_info->data["release_upgrade_url"]));
+ }
+ } else {
+ $branch = gallery::RELEASE_BRANCH;
+ if (isset($version_info->data["branch_{$branch}_build_number"]) &&
+ version_compare($version_info->data["branch_{$branch}_build_number"],
+ gallery::build_number(), ">")) {
+ return t("A newer version of Gallery is available! <a href=\"%upgrade-url\">Upgrade now</a> to version %version (build %build on branch %branch)",
+ array("version" => $version_info->data["branch_{$branch}_version"],
+ "upgrade-url" => $version_info->data["branch_{$branch}_upgrade_url"],
+ "build" => $version_info->data["branch_{$branch}_build_number"],
+ "branch" => $branch));
+ }
+ }
+ }
+ }
+}
diff --git a/modules/gallery/helpers/user_profile.php b/modules/gallery/helpers/user_profile.php
new file mode 100644
index 0000000..222d2f5
--- /dev/null
+++ b/modules/gallery/helpers/user_profile.php
@@ -0,0 +1,55 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+class user_profile_Core {
+ /**
+ * Generate the url to display the profile
+ * @return url for the profile display
+ */
+ static function url($user_id) {
+ return url::site("user_profile/show/{$user_id}");
+ }
+
+ static function get_contact_form($user) {
+ $form = new Forge("user_profile/send/{$user->id}", "", "post",
+ array("id" => "g-user-profile-contact-form"));
+ $group = $form->group("message")
+ ->label(t("Compose message to %name", array("name" => $user->display_name())));
+ $group->input("reply_to")
+ ->label(t("From:"))
+ ->rules("required|length[1, 256]|valid_email")
+ ->error_messages("required", t("You must enter a valid email address"))
+ ->error_messages("max_length", t("Your email address is too long"))
+ ->error_messages("valid_email", t("You must enter a valid email address"));
+ $group->input("subject")
+ ->label(t("Subject:"))
+ ->rules("required|length[1, 256]")
+ ->error_messages("required", t("Your message must have a subject"))
+ ->error_messages("max_length", t("Your subject is too long"));
+ $group->textarea("message")
+ ->label(t("Message:"))
+ ->rules("required")
+ ->error_messages("required", t("You must enter a message"));
+ module::event("user_profile_contact_form", $form);
+ module::event("captcha_protect_form", $form);
+ $group->submit("")->value(t("Send"));
+ return $form;
+ }
+}
diff --git a/modules/gallery/helpers/xml.php b/modules/gallery/helpers/xml.php
new file mode 100644
index 0000000..e20beb1
--- /dev/null
+++ b/modules/gallery/helpers/xml.php
@@ -0,0 +1,35 @@
+<?php defined("SYSPATH") or die("No direct script access.");
+/**
+ * Gallery - a web based photo album viewer and editor
+ * Copyright (C) 2000-2013 Bharat Mediratta
+ *
+ * This program 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.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street - Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+class xml_Core {
+ static function to_xml($array, $element_names) {
+ $xml = "<$element_names[0]>\n";
+ foreach ($array as $key => $value) {
+ if (is_array($value)) {
+ $xml .= xml::to_xml($value, array_slice($element_names, 1));
+ } else if (is_object($value)) {
+ $xml .= xml::to_xml($value->as_array(), array_slice($element_names, 1));
+ } else {
+ $xml .= "<$key>$value</$key>\n";
+ }
+ }
+ $xml .= "</$element_names[0]>\n";
+ return $xml;
+ }
+}