diff options
Diffstat (limited to 'modules/gallery/helpers')
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('&', '&', $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&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&from_id={$item->id}&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&from_id={$item->id}&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&from_id={$item->id}&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&from_id={$theme_item->id}&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&from_id={$theme_item->id}&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&" . + "from_id={$theme_item->id}&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: + // Gbor 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; + } +} |
