#!/usr/bin/env php mapAssetURLsByPath = $mapAssetURLsByPath; $this->changedFilePaths = array(); //horrible hack to reverse engineer the 'appid_version' part of //asset URL for use when run() can't find full URL. $this->urlPrefix = null; foreach($this->mapAssetURLsByPath as $path=>$url){ $urlParts = explode('/', $url); $this->urlPrefix = '/om/assets/' . $urlParts[3] . '/'; break; } } function run($filepath) { $contents = file_get_contents($filepath); verify($contents !== FALSE, "Couldn't read " . $filepath); echo 'Fixing up ' . $filepath . ":\n"; //Asset part of URL terminates in whitespace, ', ", ), ?, or & $results = preg_replace_callback('|/om/assets/.+?_\d+?/([^\'\\s">\)&\?]+)|', array($this, 'callback'), $contents); verify($results !== NULL, "Couldn't do fixup of " . $filepath); if($results != $contents){ verify(file_put_contents($filepath, $results) !== FALSE); $this->changedFilePaths[] = $filepath; } else echo "\t(nothing to do)\n"; } function callback($matches) { $fileURL = $matches[0]; $relativePath = $matches[1]; $key = "assets/$relativePath"; /// Until we're willing to restrict partners from using programatically-constructed /// asset URLs (like pingg does for colored dot icons), can't make whole-URL validation /// failures an error, just a warning. For these, we'll stick to just fixing up the URL /// prefix and hope that it's referencing a legal asset. if(array_key_exists($key, $this->mapAssetURLsByPath)){ $replacement = $this->mapAssetURLsByPath[$key]; } else if($this->urlPrefix){ echo "\t*Warning* can't find correct URL for " . $fileURL . "\n" . "\t\tMight not be a legal asset, will work off URL prefix\n"; verify(count($this->mapAssetURLsByPath), "\t\tCan't even do that, app has no assets."); $replacement = preg_replace('|/om/assets/.+?_.+?/|', $this->urlPrefix, $fileURL); // echo "prefix is " . $this->urlPrefix . ",fileURL $fileURL , replacement $replacement\n"; } else{ echo "\t*Warning* can't find correct URL for " . $fileURL . "\n" . "\tThis app has no assets!"; $replacement = $fileURL; } if($replacement !== $fileURL){ echo "\treplacing " . $fileURL . "\n"; echo "\t---> $replacement \n"; return $replacement; } return $fileURL; } } //File-system utility functions. Mostly for dealing with ymdt local app working //directories. class FileSys { const APPID_FNAME = '.appid'; //$paths is an array of (possibly relative) paths / patterns. //Expand directories to their file paths. //Allows globs. //All paths will be absolute. //Ignores root readme.txt static function expandPaths($paths) { $more_paths = array(); foreach($paths as $ndx => $path){ if(!is_dir($path)) continue; unset($paths[$ndx]); $expanded = glob(realpath($path) . '/*', GLOB_MARK); if(!empty($expanded)) $more_paths = array_merge($more_paths, $expanded); } if(empty($more_paths)) return array_map('realpath', $paths); return self::expandPaths(array_merge($paths, $more_paths)); } static function filterMetaFiles($paths, $basepath) { $newpaths = array(); $metas = array($basepath.'/'.self::APPID_FNAME, $basepath.'/readme.txt'); foreach($paths as $p){ if(in_array($p, $metas)) continue; if($p[0] == '.') continue; if($p[strlen($p)-1] == '~') continue; //emacs backup files $newpaths[] = $p; } return $newpaths; } static function filterNonTextPaths($paths) { $txt_extensions = array('htm', 'html', 'js', 'css', 'txt'); $results = array(); foreach($paths as $p){ if(in_array(pathinfo($p, PATHINFO_EXTENSION), $txt_extensions)) $results[] = $p; } return $results; } static function layout($appdir) { global $HELP_README; $subdirs = array($appdir.'/views', $appdir.'/assets'); foreach($subdirs as $s){ verify(file_exists($s) || self::mkdir($s)); } $readme = $appdir.'/readme.txt'; verify(file_put_contents($readme, $HELP_README), "Couldn't write $readme"); } static function mkdir($path) { if(is_dir($path)) return; return mkdir($path, 0755, true); } static function modTime($path) { $stat = stat($path); return $stat['mtime']; } static function nameFromConfFile($appdir) { $confFname = $appdir . '/config.xml'; $xml = simplexml_load_file($confFname); verify(is_a($xml, 'SimpleXMLElement'), "Couldn't read XML conf from $confFname"); return $xml->name; } //returns array(appid, basepath, subpath) //basepath is absolute path to app root, i.e. the dir that contains .appid //subpath is the path (file/dir/pattern) relative to basepath //filename is the filename (or terminating glob / null if dir) function parsePath($path) { $newpath = realpath($path); if(!$newpath){ $newpath = realpath(dirname($path)) . '/' . basename($path); //really only support globs in filenames, no higher up the path verify($newpath, "unsupported glob in dirname $path ?"); } $path = $newpath; //Keep on going up the path until we find the magic .appid file $base = $path; if(!is_dir($base)) $base = dirname($base); while(true){ $appid_path = $base . '/' . self::APPID_FNAME; if(file_exists($appid_path)){ $appid = file_get_contents($appid_path); verify($appid != '', 'Empty .appid file?'); $subpath = substr($path, strlen($base)+1); return array($appid, $base, $subpath); } if($base == '.' || $base == '/' || $base == '') break; $base = dirname($base); } verify(false, "Couldn't find .appid along $path"); } }; /* vim: set expandtab tabstop=4 shiftwidth=4: */ // +----------------------------------------------------------------------+ // | PHP Version 5 | // +----------------------------------------------------------------------+ // | Copyright (c) 1997-2004 The PHP Group | // +----------------------------------------------------------------------+ // | This source file is subject to version 3.0 of the PHP license, | // | that is bundled with this package in the file LICENSE, and is | // | available through the world-wide-web at the following url: | // | http://www.php.net/license/3_0.txt. | // | If you did not receive a copy of the PHP license and are unable to | // | obtain it through the world-wide-web, please send a note to | // | license@php.net so we can mail you a copy immediately. | // +----------------------------------------------------------------------+ // | Author: Andrei Zmievski | // +----------------------------------------------------------------------+ // // $Id: Getopt.php,v 1.4 2007/06/12 14:58:56 cellog Exp $ /** * Command-line options parsing class. * * @author Andrei Zmievski * */ class Console_Getopt { /** * Parses the command-line options. * * The first parameter to this function should be the list of command-line * arguments without the leading reference to the running program. * * The second parameter is a string of allowed short options. Each of the * option letters can be followed by a colon ':' to specify that the option * requires an argument, or a double colon '::' to specify that the option * takes an optional argument. * * The third argument is an optional array of allowed long options. The * leading '--' should not be included in the option name. Options that * require an argument should be followed by '=', and options that take an * option argument should be followed by '=='. * * The return value is an array of two elements: the list of parsed * options and the list of non-option command-line arguments. Each entry in * the list of parsed options is a pair of elements - the first one * specifies the option, and the second one specifies the option argument, * if there was one. * * Long and short options can be mixed. * * Most of the semantics of this function are based on GNU getopt_long(). * * @param array $args an array of command-line arguments * @param string $short_options specifies the list of allowed short options * @param array $long_options specifies the list of allowed long options * * @return array two-element array containing the list of parsed options and * the non-option arguments * * @access public * */ function getopt2($args, $short_options, $long_options = null) { return Console_Getopt::doGetopt(2, $args, $short_options, $long_options); } /** * This function expects $args to start with the script name (POSIX-style). * Preserved for backwards compatibility. * @see getopt2() */ function getopt($args, $short_options, $long_options = null) { return Console_Getopt::doGetopt(1, $args, $short_options, $long_options); } /** * The actual implementation of the argument parsing code. */ function doGetopt($version, $args, $short_options, $long_options = null) { // in case you pass directly readPHPArgv() as the first arg // if (PEAR::isError($args)) { // return $args; // } if (empty($args)) { return array(array(), array()); } $opts = array(); $non_opts = array(); settype($args, 'array'); if ($long_options) { sort($long_options); } /* * Preserve backwards compatibility with callers that relied on * erroneous POSIX fix. */ if ($version < 2) { if (isset($args[0]{0}) && $args[0]{0} != '-') { array_shift($args); } } reset($args); while (list($i, $arg) = each($args)) { /* The special element '--' means explicit end of options. Treat the rest of the arguments as non-options and end the loop. */ if ($arg == '--') { $non_opts = array_merge($non_opts, array_slice($args, $i + 1)); break; } if ($arg{0} != '-' || (strlen($arg) > 1 && $arg{1} == '-' && !$long_options)) { $non_opts[] = $arg; // = array_merge($non_opts, array_slice($args, $i)); // break; } elseif (strlen($arg) > 1 && $arg{1} == '-') { $error = Console_Getopt::_parseLongOption(substr($arg, 2), $long_options, $opts, $args); // if (PEAR::isError($error)) // return $error; } elseif ($arg == '-') { // - is stdin $non_opts = array_merge($non_opts, array_slice($args, $i)); break; } else { $error = Console_Getopt::_parseShortOption(substr($arg, 1), $short_options, $opts, $args); // if (PEAR::isError($error)) // return $error; } } return array($opts, $non_opts); } /** * @access private * */ function _parseShortOption($arg, $short_options, &$opts, &$args) { for ($i = 0; $i < strlen($arg); $i++) { $opt = $arg{$i}; $opt_arg = null; /* Try to find the short option in the specifier string. */ if (($spec = strstr($short_options, $opt)) === false || $arg{$i} == ':') { return verify(false, "Console_Getopt: unrecognized option -- $opt"); } if (strlen($spec) > 1 && $spec{1} == ':') { if (strlen($spec) > 2 && $spec{2} == ':') { if ($i + 1 < strlen($arg)) { /* Option takes an optional argument. Use the remainder of the arg string if there is anything left. */ //$opts[] = array($opt, substr($arg, $i + 1)); $opts[$opt] = substr($arg, $i + 1); break; } } else { /* Option requires an argument. Use the remainder of the arg string if there is anything left. */ if ($i + 1 < strlen($arg)) { // $opts[] = array($opt, substr($arg, $i + 1)); $opts[$opt] = substr($arg, $i + 1); break; } else if (list(, $opt_arg) = each($args)) { /* Else use the next argument. */; if (Console_Getopt::_isShortOpt($opt_arg) || Console_Getopt::_isLongOpt($opt_arg)) { verify(false, "Console_Getopt: option requires an argument -- $opt"); } } else { return verify(false, "Console_Getopt: option requires an argument -- $opt"); } } } // $opts[] = array($opt, $opt_arg); $opts[$opt] = $opt_arg; } } /** * @access private * */ function _isShortOpt($arg) { return strlen($arg) == 2 && $arg[0] == '-' && preg_match('/[a-zA-Z]/', $arg[1]); } /** * @access private * */ function _isLongOpt($arg) { return strlen($arg) > 2 && $arg[0] == '-' && $arg[1] == '-' && preg_match('/[a-zA-Z]+$/', substr($arg, 2)); } /** * @access private * */ function _parseLongOption($arg, $long_options, &$opts, &$args) { @list($opt, $opt_arg) = explode('=', $arg, 2); $opt_len = strlen($opt); for ($i = 0; $i < count($long_options); $i++) { $long_opt = $long_options[$i]; $opt_start = substr($long_opt, 0, $opt_len); $long_opt_name = str_replace('=', '', $long_opt); /* Option doesn't match. Go on to the next one. */ if ($long_opt_name != $opt) { continue; } $opt_rest = substr($long_opt, $opt_len); /* Check that the options uniquely matches one of the allowed options. */ if ($i + 1 < count($long_options)) { $next_option_rest = substr($long_options[$i + 1], $opt_len); } else { $next_option_rest = ''; } if ($opt_rest != '' && $opt{0} != '=' && $i + 1 < count($long_options) && $opt == substr($long_options[$i+1], 0, $opt_len) && $next_option_rest != '' && $next_option_rest{0} != '=') { return verify(false, "Console_Getopt: option --$opt is ambiguous"); } if (substr($long_opt, -1) == '=') { if (substr($long_opt, -2) != '==') { /* Long option requires an argument. Take the next argument if one wasn't specified. */; if (!strlen($opt_arg) && !(list(, $opt_arg) = each($args))) { return verify(false, "Console_Getopt: option --$opt requires an argument"); } if (Console_Getopt::_isShortOpt($opt_arg) || Console_Getopt::_isLongOpt($opt_arg)) { return verify(false, "Console_Getopt: option requires an argument --$opt"); } } } else if ($opt_arg) { return verify(false, "Console_Getopt: option --$opt doesn't allow an argument"); } $opts[] = array('--' . $opt, $opt_arg); return; } return verify(false, "Console_Getopt: unrecognized option --$opt"); } /** * Safely read the $argv PHP array across different PHP configurations. * Will take care on register_globals and register_argc_argv ini directives * * @access public * @return mixed the $argv PHP array or PEAR error if not registered */ function readPHPArgv() { global $argv; if (!is_array($argv)) { if (!@is_array($_SERVER['argv'])) { if (!@is_array($GLOBALS['HTTP_SERVER_VARS']['argv'])) { return verify(false, "Console_Getopt: Could not read cmd args (register_argc_argv=Off?)"); } return $GLOBALS['HTTP_SERVER_VARS']['argv']; } return $_SERVER['argv']; } return $argv; } } $HELP_README = << [flags] [command arguments] Type 'ymdt help ' for help on a specific command. Available commands: apps lists all of the user's apps create creates a new app destroy delete the entire app del deletes one of an app's files dev develop-o-matic mode fixup rewrite app's views and text assets so that asset URLs are correct get get latest of an app's file(s) from dev server help get help on a specific command ls list all of an app's file(s) and their URLs publish publish an app put upload apps file(s) to dev server tester list, invite, or delete testers EOT; $HELP_COMMON_OPTIONS = << Override default Yahoo! Mail developer hostname. (Default is developer.mail.yahoo.com) -u Specify user ID. If this option is omitted, the user ID is either figured from cookies or by prompting the user. After authentication, a token is normally cached in a cookie file in your home directory. It does not contain your password. This time-sensitive token will be reused on subsequent invocations to avoid logging on. Use the -d flag to avoid writing that file and force login on every invocation. -d Don't save auth token to file. -p You're better off not using this option and letting the program offer you a masked prompt for password entry. -k Disables SSL host verification, use if the host you're connecting to is using self-signed SSL certs. EOT; $HELP_APPS = << Create an app on the development server, with a local working copy kept in . If doesn't already contain an app, a minimal app will be created. If already contains an app, a new app will be created based on the app files in , and will be converted to a valid working directory for the newly-created app. In either case, the app name will be set to '', but you can modify it to be different from '' by editing the app's config.xml. For a description of the files comprising an app, see /readme.txt. Options: -A: -A: -A: Usually, the server allocates a globally-unique appid for both the public and private (development) versions of the app. This option allows admins to explicitly specify a public appid, private appid, or both. Fails if any of the specified appids already exist or if the logged-on user doesn't have admin rights. Note the magic ':', it must be there even if you're not specifying both appids. Examples: create a new app in directory ~/apps/llama: ymdt create ~/apps/llama create a new app in directory ~/apps/lindy with pub appid 123 and private appid 456: ymdt -A123:456 create ~/apps/lindy create a new app in directory ~/apps/lindy with autogenerated public appid and private appid 456: ymdt -A:456 create ~/apps/lindy create a new app in directory ~/apps/lindy with public appid 123 and an autogenerated private appid: ymdt -A123: create ~/apps/lindy EOT; $HELP_DEL = << Delete one of an app's files. must be to a single file in an app's local working directory. Examples: delete asset 'lark.jpg' for app in directory ./birds: ymdt del ./birds/assets/lark.jpg EOT; $HELP_DEV = << Enter dev-o-matic mode. must be to the root of an app's local working directory. Will do a get of app, then continuously watch for local changes and update the server. EOT; $HELP_DESTROY = <<] Destroy an application. Must specify the path or the -a flag. -a is the application's private appid. -z Admin option. Really, really delete it from the development server. EOT; $HELP_FIXUP = << When your app is published, the in-development version of your app is copied to the public version of your app, which is visible to the world. The first time after publication that you upload (put) assets to the development server, the asset URLs change. Use this command to fixup up all of the asset URLs in your view HTML and your text assets. Recommended best practice: after your app is published (by an admin), run fixup, but backup your personal source first. (Your app is in source control, right?) Options: -z Suppress autoput. Normally, fixup executes these steps: 1) Uploads all of your app's files to the development server. 2) Fetches the latest asset URLs from the server. 3) Rewrites views and text type assets to use the latest asset URLs. 4) Uploads any files modified in step 3. Using the -z option suppresses steps 1 and 4. This is handy if you want to avoid step 1 when you know the assets have already been uploaded once since the last publish or if you want to manually review the results of 3 before uploading files to the development server. -n Use the application name from the app's conf.xml to lookup the app's private appid on the server and modify the .appid file in your local working copy. See: put -n. -s Sync. Same as for put. EOT; $HELP_GET = << Get latest of an app's file(s) from dev server. Normally, is to the root or some subpath of the app's local working copy. Wildcards (?*) allowed below app root, but only if is quoted. Options: -a Get an app that exists on the server, but for which you don't yet have a working copy. In this case, must be to an empty directory. -s Sync: Do the get, but also delete any local files below that aren't present on the server. -y Autoyes: don't prompt user to confirm local deletes, use with care! Examples: get latest of all files for app 045fa65e3, using directory duck as the local working directory: ymdt -a045fa65e3 get duck get latest of all files for app in directory ./scrooge: ymdt get ./scrooge get latest of all assets for app in directory ./mcduck: ymdt get ./mcduck/assets get latest of config for app in directory ./ham: ymdt get ./ham/config.xml get latest of .js files in ./ham/assets ymdt get './ham/assets/*.js' EOT; $HELP_HELP = << Give help for a particular command. For general usage, run ymdt without any arguments. EOT; $HELP_LS = << List server-side files and their URLs. Handy for figuring out how to reference your assets from views or other assets. must minimally be an app's working directory, in which case all of the files comprising the app will be listed. may also specify a particular subdir or file inside the app's working directory. Wildcards (*?) allowed below app's working directory root. Examples: list all assets for app in directory ~/apps/pigeon: ymdt ls ~/apps/pigeon/assets list all .jpg assets for app in directory ~/apps/zebra: ymdt ls '~/apps/zebra/assets/*.jpg' # note quotes to avoid shell expansion EOT; $HELP_PUBLISH = <<] Update the publically-visible version of your app on the development server to be the same as the latest uploaded private (in-development) version of an app. is the root of the private version's local working copy. It is only used to figure out the right appid. The public app update is peformed only using the private app files already uploaded to the server. Requires admin rights. You must use the -a option if you omit . Options: -a Specify the private appid of the app you'd like to publish. -z Suppress asset URL validation. EOT; $HELP_PUT = << Upload files from local app working copy to the server. is the root or some subpath of the app's local working copy. Wildcards (?*) allowed below app root, but only if is quoted. Options: -a Explicitly specify the appid of the destination app on the server. Normally the .appid in the root directory of the app's local working copy determines what app is modified on the server. Since you may only modify the private (in-development) version of your app, must be a private appid. -n Use the application name from the app's conf.xml to lookup the app's private appid on the server and modify the .appid file in your local working copy. For situations where you created the local working copy by doing a get -a with a public appid or if you've done a get -h from a different server. Revises the .appid so that future puts will apply to the private app on the correct server. -s Sync: Do the put, but also delete any files on the server that aren't present in the local working copy. -y Autoyes: don't prompt user to confirm server deletes, use with care! Examples: upload all files for app in directory ./mcduck: ymdt put ./mcduck upload all assets for app in directory ./phish: ymdt put ./phish/assets upload latest of .js files in ./ham/assets ymdt put './ham/assets/*.js' EOT; $HELP_TESTER = << [tester email address or yid] tester ls will display your tester list. invite will invite someone to be a tester. del will delete an existing tester. EOT; $HELP_UPGRADE = << 'login', 'id' => $bid, 'pass_word' => $password)); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HEADER, TRUE); if($cookie_file_name){ curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file_name); curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file_name); } curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); if(self::isWindows()){ curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); } $result = curl_exec($ch); curl_close($ch); if($cookie_file_name) chmod($cookie_file_name, 0600); if($result === false || preg_match("/YCorp=/", $result) != 1){ return false; } preg_match_all('|Set-Cookie: (.*);|U', $result, $matches); return implode(';', $matches[1]); // return true; } static function authYahoo($yid, $passwd, $cookie_file_name) { $agent = 'Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.5; en-US;'. 'rv:1.9.0.5) Gecko/2008120121 Firefox/3.0.5'; $ch = curl_init(); curl_setopt($ch, CURLOPT_USERAGENT, $agent); curl_setopt($ch, CURLOPT_COOKIEJAR, $cookie_file_name); curl_setopt($ch, CURLOPT_COOKIEFILE, $cookie_file_name); $postFields = "&login=$yid&passwd=$passwd"; curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields); curl_setopt($ch, CURLOPT_URL, 'http://login.yahoo.com'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); $result = curl_exec($ch); curl_close($ch); return self::extractCookieString($cookie_file_name) != null; } static function extractCookieString($cookie_file_name) { if (!file_exists($cookie_file_name)) return null; $file = fopen($cookie_file_name, 'r'); if (!$file) return null; $regex = '/^\.yahoo\.com\tTRUE\t\/\tFALSE\t\d+\t(\w+)\t(.*)$/'; while ($line = fgets($file)) { if (!preg_match($regex, $line, $matches)) continue; $key = $matches[1]; $val = $matches[2]; $cookies[] = "$key=$val"; } fclose($file); if(!count($cookies)) return null; $cstr = join('; ', $cookies); return $cstr; } static function extractBID_FromCookie($cookie_file_name) { if(!file_exists($cookie_file_name)) return null; static $regex = '/YBY\sid%3D[0-9]+%26userid%3D([^%]+)%26/'; $matches = array(); $contents = file_get_contents($cookie_file_name); $count = preg_match($regex, $contents, $matches); if($count != 1){ return null; } return $matches[1]; } } //Wraps calls to the development server. class WebServiceClient { function __construct($hostname, $cookieFname, $lamessl, $uname, $passwd) { $this->hostname = $hostname ? $hostname : 'developer.mail.yahoo.com'; echo "Developer server: " . $this->hostname . "\n"; $this->cookieFname = $cookieFname; $this->lamessl = $lamessl; $this->uname = $uname; $this->passwd = $passwd; $this->wssid = null; } function call($op, &$msg, $fields = array(), $format = 'json', $checkStatus = true, $needLogin = true) { if(!$this->wssid && $needLogin) $this->login(); $url = 'https://' . $this->hostname; if(strpos($op, 'admin.') !== false) $url = 'http://' . $this->hostname . ':9999'; $url .= '/om/api/1.0/openmail.' . $op; if($this->wssid){ $url .= '?bycrumb='.$this->wssid; } $ch = curl_init(); $timeout = 0; curl_setopt($ch, CURLOPT_POST, TRUE); curl_setopt($ch, CURLOPT_POSTFIELDS, $fields); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HEADER, TRUE); curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); if($this->cookieFname) curl_setopt($ch, CURLOPT_COOKIEFILE, $this->cookieFname); else curl_setopt($ch, CURLOPT_COOKIE, $this->cookies); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); if($this->lamessl) curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); $result = curl_exec($ch); if(curl_errno($ch)){ $msg = 'Curl failure: ' . curl_error($ch); } $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); curl_close($ch); if(file_exists($this->cookieFname)) chmod($this->cookieFname, 0600); $content = substr($result, $header_size); $rval = null; if($format){ if($format == 'json') $rval = json_decode($content); elseif($format == 'xml') $rval = simplexml_load_string(trim($content)); if(!get_class($rval) && !is_array($rval)) $rval = null; } if($http_code != '200'){ $msg = "$op invoke failed (HTTP $http_code)."; if($rval && property_exists($rval, 'message')) $msg .= "\nServer says: {$rval->message}"; verify(false, $msg); } if(!$format) return $content; verify($rval !== null, "Unexpected webservice result from $op: \n$format:\n\t[$content]"); //Would be nice if ws results were a little more standardized, a la JSON-RPC. //But, they're not. . . if($checkStatus && $rval->status != 200){ $msg = property_exists($rval, 'message') ? $rval->message : ''; $msg = $msg . " (status: {$rval->status})"; verify(false, $msg); } return $rval; } function getWSSID() { //This call, unlike all the others, returns in XML (since JSON is //security no-no for wssid.) $result = $this->call('dev.file.init', $msg, array(), 'xml', true, false); verify($result && property_exists($result, 'wssid'), 'unable to retrieve wssid'); $this->wssid = $result->wssid; if(property_exists($result, 'version') && YMDT_Version < $result->version){ $latest = $result->version; echo "***You are running an outdated version of ymdt ". "(latest is $latest, you have " . YMDT_Version . ".).\n". "***Please run: ymdt upgrade\n\n"; } } function login() { $uname = $this->uname; $passwd = $this->passwd; $use_guesthouse = strpos($this->hostname, 'corp.yahoo.com') === false; //Username changed? Blow away cookie file. $prevUsername = Login::extractBID_FromCookie($this->cookieFname); if($uname && $prevUsername != $uname){ verify(!file_exists($this->cookieFname) || unlink($this->cookieFname), "login fought {$this->cookieFname} and lost"); } //If we can't get a crumb, try logging into backyard to refresh cookie, //then try getting crumb again. try { $this->getWSSID(); if($this->wssid) return; } catch (Exception $e) {} if(!$uname){ $prompt = 'Backyard ID: '; if($use_guesthouse) $prompt = 'Guesthouse ID: '; list($uname, $passwd) = Login::promptCredentials($prompt, 'Password: '); } elseif(!$passwd){ $passwd = Login::promptUserInput('Password:', false); } $this->cookies = Login::authBouncer($uname, $passwd, $this->cookieFname, $use_guesthouse); verify($this->cookies, 'login failed'); echo "\nlogin successful\n"; if($this->cookieFname){ echo "\nAn authentication token has been stored in " . $this->cookieFname . "\nUntil its expiration,". "it will be used by future ymdt invocations to avoid having to" . " relogin.\nTo disable this behavior, use the -d option.\n\n"; } $this->getWSSID(); } function appList() { $msg = null; $fields = array('ignorePublicationStatus' => 'true'); verify(($result = $this->call('dev.app.list', $msg, $fields, 'json', false)) !== null, "No webservice result."); return $result; } function ls($appid, $subpath = null) { $fields = array('app' => $appid); if($subpath){ $fields['path'] = $subpath; } $result = $this->call('dev.file.ls', $msg, $fields); return get_object_vars($result->data); } //upgrade helper, not really a ws call function fetchLatestScript() { $ch = curl_init(); $timeout = 0; $url = 'https://' . $this->hostname . '/openmail/assets/ymdt'; curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_HEADER, TRUE); curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); if($this->lamessl) curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); $result = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); curl_close($ch); $content = substr($result, $header_size); verify($http_code == '200' && !empty($content), "Upgrade fetch failed (HTTP $http_code)."); return $content; } }; define('YMDT_Version', '0.42'); $HOMEDIR = Login::getHomeDir(); ini_set('open_basedir', ""); ini_set('error_log', './error.log'); ini_set('display_errors', 'On'); ini_set('error_reporting', E_ERROR); $DEBUG_MODE = false; function verify($cond, $msg) { if($cond) return; throw new Exception($msg); } class YMDT { private $ws; const OPTS___CONSTRUCT = 'h:ku:p:d'; public function __construct($cookieFname, $hostname = null, $lamessl = false, $username = null, $password = null, $nocookies = null) { if($nocookies){ verify( !file_exists($cookieFname) || unlink($cookieFname), "Couldn't delete " . $cookieFname . '.'); $cookieFname = null; } $lamessl_hosts = array('om0001.mail.mud.yahoo.com', 'om0002.mail.mud.yahoo.com'); if(in_array($hostname, $lamessl_hosts)) $lamessl = true; $this->ws = new WebServiceClient($hostname, $cookieFname, $lamessl, $username, $password); } private function membersFromCmdLineOptions($optString, $memberNames, $opts) { $opts = str_replace(':', '', $opts); foreach($opts as $ndx=>$char){ $memberName = $memberNames[$ndx]; if(array_key_exists($char, $opts)) $this->args->$memberName = $opts[$char]; else $this->args->$memberName = null; } } public function apps() { $result = $this->ws->appList(); printf("\n%-16s %-16s %.100s", 'private appid', 'public appid', 'name'); printf("\n%-16s %-16s %.100s", '--------------', '--------------', '---------------------------------------------'); foreach ($result as $r) { printf("\n%-16s %-16s %.100s", $r->app, $r->published_to, $r->name); } echo "\n"; } const OPTS_CREATE = 'A:'; public function create($appdir, $appids = null) { verify(file_exists($appdir) || FileSys::mkdir($appdir), "Couldn't create dir $appdir"); $fname = $appdir.'/'.FileSys::APPID_FNAME; //If it's not a virgin app, we have a local working copy already //(i.e. that we've unzipped from someone else), just nothing on the //server yet. $isVirginApp = !file_exists($fname); $fields = array('name' => basename($appdir)); if($appids){ $appids = explode(':', $appids); verify(count($appids) == 2, "Expected -A:"); if($appids[0]) $fields['pub_appid'] = $appids[0]; if($appids[0]) $fields['priv_appid'] = $appids[1]; } verify(($result = $this->ws->call('dev.app.create', $msg, $fields)), "No webservice result."); verify(file_put_contents($fname, $result->id) !== false, "Couldn't write $fname for {$result->id}"); echo($result->message . "\n"); if(!$isVirginApp) return self::fixup($appdir); FileSys::layout(realpath($appdir)); return self::get($appdir); } function del($path) { list($appid, $basepath, $subpath) = FileSys::parsePath($path); verify($subpath, 'del must reference file in working app dir'); if(file_exists($path)) verify(!is_dir($path), 'del requires a path to a single file'); $fields = array('app' => $appid); $fields['path'] = $subpath; $json = $this->ws->call('dev.file.del', $msg, $fields); echo "\tdeleted $path from server\n"; return; } function dev($path) { //todo: offer choice of starting with sync from server to local dir or // vice versa. Make sure this is an app dir, or you have an appid, // etc. self::get($path); echo "\nMonitoring $path for changes. Hit Ctrl-C to exit\n\n"; $prevFiles = FileSys::expandPaths(array($path)); $tmLastMod = max(array_map(array('FileSys', 'modTime'), $prevFiles)); while(true){ $files = FileSys::expandPaths(array($path)); $deletes = array_diff($prevFiles, $files); $updates = array(); $tmMaxMod = $tmLastMod; foreach($files as $f){ $tmMod = stat($f); $tmMod = $tmMod['mtime']; if($tmMod <= $tmLastMod) continue; $updates[] = $f; $tmMaxMod = max($tmMaxMod, $tmMod); } if(!empty($updates) || !empty($deletes)){ echo "\nNoticed some changes in $path, syncing. . .\n"; array_walk($updates, array($this, 'putOne')); array_walk($deletes, array($this, 'del')); echo "Sync done.\n"; } $prevFiles = $files; $tmLastMod = $tmMaxMod; usleep(1000 * 100); } } const OPTS_DESTROY = 'a:z'; function destroy($appdir = null, $appid = null, $reallyReally = false) { verify($appdir || $appid, "Must specify either appdir or -a."); if(!$appid){ list($appid, $basepath, $subpath) = FileSys::parsePath($appdir); } $fields = array('app' => $appid); $call = 'dev.app.delete'; if($reallyReally){ $call = 'admin.app.destroy'; } verify(($result = $this->ws->call($call, $msg, $fields)), "No webservice result."); echo($result->message . "\n"); } //fixup all asset URLs for view/assets files in $approot const OPTS_FIXUP = 'nzs'; function fixup($approot, $appidFromName = false, $suppressAutoput = false, $sync = false) { list($appid, $basepath, $subpath) = FileSys::parsePath($approot); verify(!strlen($subpath), "fixup requires app's root directory as an argument."); if($appidFromName){ $appid = $this->changePrivateAppid($basepath, FileSys::nameFromConfFile($basepath)); } if(!$suppressAutoput){ echo "Uploading all files:\n"; self::put($approot, null, false, $sync); } //Grab only text assets (by file extension). Assume everything in view //dir is text. $paths = FileSys::expandPaths(array($basepath . '/assets')); $paths = FileSys::filterNonTextPaths($paths); $paths = array_merge($paths, FileSys::expandPaths(array($basepath . '/views'))); $paths = FileSys::filterMetaFiles($paths); $fixer = new AssetURL_Fixer($this->ws->ls($appid, 'assets')); array_walk($paths, array($fixer, 'run')); $numChanged = count($fixer->changedFilePaths); if(!$suppressAutoput && $numChanged){ echo "Uploading changed files:\n"; array_walk($fixer->changedFilePaths, array($this, 'putOne')); } } const OPTS_GET = 'a:sy'; function get($path, $appid = null, $sync = false, $autoYes = false) { if($appid){ list($subpath, $basepath) = array(null, $path); $fname = $basepath.'/'.FileSys::APPID_FNAME; FileSys::mkdir($basepath); verify(file_put_contents($fname, $appid), "Couldn't write $fname."); FileSys::layout($basepath); } else{ list($appid, $basepath, $subpath) = FileSys::parsePath($path); } $fetchedPaths = self::lsAndGet($appid, $subpath, $basepath); if(!$sync) return; $fetchedPaths = array_map('realpath', $fetchedPaths); $localPaths = FileSys::expandPaths(array($path)); // echo 'localPaths: ' . print_r($localPaths, true) . "\n"; $localPaths = FileSys::filterMetaFiles($localPaths, realpath($basepath)); // echo 'filtered: ' . print_r($localPaths, true) . "\n"; //echo 'fetched: ' . print_r($fetchedPaths, true) . "\n"; $deletes = array_diff($localPaths, $fetchedPaths); if(!$autoYes && !self::confirmDeletes($deletes, "\nThe following files aren't present on the server:\n", "\nWould you like to delete the local copies? ")) return; { foreach($deletes as $d){ try { verify(unlink($d) , "Couldn't delete $d"); echo "\tdeleted $d\n"; } catch(Exception $e) { echo 'Caught exception: ', $e->getMessage(), "\n"; } } return; } } function help($command) { if(!$command) show_usage(); $txt_var = 'HELP_' . strtoupper($command); if(!array_key_exists($txt_var, $GLOBALS)){ echo 'Command ' . $command . ' unknown.' . "\n\n"; return; } global $$txt_var, $HELP_COMMON_OPTIONS; echo $$txt_var . "\n$HELP_COMMON_OPTIONS\n\n";; } function ls($path) { list($appid, $basepath, $subpath) = FileSys::parsePath($path); $results = $this->ws->ls($appid, $subpath); foreach($results as $fname => $url) echo "\t" . $fname . "\t" . $url . "\n"; } const OPTS_PUBLISH = 'a:z'; function publish($appdir = null, $appidOverride = null, $suppressValidation = false) { $appid = null; if(strlen($appdir)){ list($appid, $basepath, $subpath) = FileSys::parsePath($appdir); verify(!strlen($subpath), "publish requires path to be app's root directory."); } if($appidOverride) $appid = $appidOverride; verify($appid, 'publish requires -a option or a '); $fields = array('app' => $appid); if(!$suppressValidation) $fields['validate_assets'] = 'true'; $this->ws->call('dev.app.publish', $msg, $fields); echo "\tdone publication\n"; } const OPTS_PUT = 'a:nsy'; function put($pattern, $appidOverride = null, $appidFromName = false, $sync = false, $autoYes = false) { list($appid, $basepath, $subpath) = FileSys::parsePath($pattern); if($appidOverride) $appid = $appidOverride; if($appidFromName){ verify(!$appidOverride, "put can't accept both -n and -a"); $appid = $this->changePrivateAppid($basepath, FileSys::nameFromConfFile($basepath)); } $paths = FileSys::expandPaths(glob($pattern, GLOB_MARK)); $paths = FileSys::filterMetaFiles($paths, $basepath); array_walk($paths, array($this, 'putOne')); if(count($paths) > 1) echo "Done all puts for $pattern.\n"; if(!$sync) return; $remote_paths = $this->ws->ls($appid, $subpath); $remote_paths = array_keys($remote_paths); //Need relative (to app root) paths $root = realpath($basepath); $newpaths = array(); foreach($paths as $p){ $newpaths[] = substr($p, strlen($root)+1); } $paths = $newpaths; $deletes = array(); //echo 'remote paths: ' . print_r($remote_paths, true) . "\n\n"; //echo 'paths: ' . print_r($paths, true) . "\n\n"; foreach($remote_paths as $p){ if(in_array($p, $paths) || is_dir($p)) continue; $deletes[] = $p; } if(!$autoYes && !self::confirmDeletes($deletes, "\nThe following files aren't present locally:\n", "\nWould you like to delete them from the server? ")) return; { foreach($deletes as $path){ try { $fields = array('app' => $appid); $fields['path'] = $path; $json = $this->ws->call('dev.file.del', $msg, $fields); echo "\tdeleted $path from server\n"; } catch(Exception $e) { echo 'Caught exception: ', $e->getMessage(), "\n"; } } return; } } function tester($subcmd = null, $emailOrYid = null) { $legalSubs = array('ls', 'del', 'invite'); verify($subcmd && in_array($subcmd, $legalSubs), "tester command requires a subcommand, one of: " . implode(', ', $legalSubs) . "."); if($subcmd == 'ls') return $this->testerLs(); $msg = ''; if($subcmd == 'del'){ verify($emailOrYid, "tester del requires the tester's Yahoo! ID."); $fields = array('yid' => $emailOrYid); $result = $this->ws->call('dev.yid.remove', $msg, $fields); echo "Tester $emailOrYid deleted.\n\n"; return; } verify($emailOrYid, "tester del requires the tester's Yahoo email address."); $fields = array('email' => $emailOrYid); $result = $this->ws->call('dev.yid.add', $msg, $fields); echo "Tester $emailOrYid invited.\n\n"; } function testerLs() { $fields = array(); $arr = $this->ws->call('dev.yid.list', $fields, $msg, 'json', false); verify(is_array($arr), "Unexpected ws result for dev.yid.list: " . print_r($arr, true)); printf("%-32s %-16s\n", 'Yahoo! ID', "Pending?"); printf("-----------------------------------------\n"); foreach($arr as $tester){ printf("%-32s %-16s\n", $tester->yid, $tester->pending ? 'yes' : ''); } } function confirmDeletes($deletes, $list_header, $question) { if(!empty($deletes)){ shell_exec("./script/growl"); echo $list_header; foreach($deletes as $d){ echo "\t$d\n"; } $response = Login::promptUserInput($question); if(strtolower($response[0]) === 'n') return false; } return true; } function lsAndGet($appid, $src_subpath, $dest_basepath) { //Do an ls first in case they're using a dir or glob for path. $results = $this->ws->ls($appid, $src_subpath); $count = 0; //Get each file returned by ls $local_file_paths = array(); foreach($results as $fname => $url){ $fields = array('app' => $appid, 'name' => $fname); $file_path = $dest_basepath . '/' . $fname; echo "\tdownloading $file_path. . .\n"; $result = $this->ws->call('dev.file.get', $msg, $fields, null); FileSys::mkdir(dirname($file_path)); verify(file_put_contents($file_path, $result) !== false, "Couldn't write $file_path"); $local_file_paths[] = $file_path; $count ++; } if($count > 1) echo "Done all gets for $dest_basepath.\n"; return $local_file_paths; } private function changePrivateAppid($appdir, $name) { $list = $this->ws->appList(); $privAppid = null; foreach($list as $app){ if($name != $app->name) continue; verify(!$privAppid, "More than one app named '$name' for logged on developer."); $privAppid = $app->app; } verify($privAppid, "No app named '$name' for logged-on developer."); verify(file_put_contents($appdir . '/' . FileSys::APPID_FNAME, $privAppid) !== FALSE); echo "\tchanged appid to $privAppid. \n"; return $privAppid; } function putOne($path) { echo "\tuploading $path. . .\n"; list($appid, $basepath, $subpath) = FileSys::parsePath($path); $contents = file_get_contents($path); verify($contents !== false, "Couldn't read contents of $path."); $fields = array('app' => $appid, 'name' => $subpath, 'file' => '@' . $path); $result = $this->ws->call('dev.file.put', $msg, $fields); return; } function upgrade() { $newYmdt = $this->ws->fetchLatestScript(); verify(!strpos(__FILE__, '.php'), "Can't upgrade source file, only built version."); $archive = __FILE__ . '.old'; verify(copy(__FILE__, $archive), "Upgrade failed, couldn't archive current version to $archive."); verify(file_put_contents(__FILE__, $newYmdt) !== false, "Upgrade failed, couldn't write " . __FILE__); echo "Done upgrade, previous version archived to $archive.\n"; return 0; } } function show_usage() { global $HELP_USAGE; echo $HELP_USAGE . "\n\n"; exit(-1); } //Given an options string as taken by getopt(), return options array and reindex //global argv to omit the options. function getopts_and_reindex($opts) { list($options, $rest) = Console_Getopt::getopt($GLOBALS['argv'], $opts); $GLOBALS['argv'] = array_merge(array('ymdt'), $rest); // echo "OPTS: $opts\n"; //echo "OPTIONS:\n" . print_r($options, true); //echo "ARGV:\n" . print_r($GLOBALS['argv'], true); return $options; } function getMethodMetadata($className, $methodName) { $class = new ReflectionClass($className); $optString = $class->getConstant(strtoupper('OPTS_' . $methodName)); $method = new ReflectionMethod($className, $methodName); $params = $method->getParameters(); $optionalParamNames = array(); $requiredParamCount = $method->getNumberOfRequiredParameters(); foreach($params as $ndx=>$p){ if($ndx < $requiredParamCount) continue; $optionalParamNames[] = $p->getName(); } //Optional parameters come either from -xYYY command-line or just plain argv args $optChars = str_replace(':', '', $optString); verify(strlen($optChars) <= count($optionalParamNames), "Internal error: $methodName optstring wrong len."); return array($method, $optString, $requiredParamCount); } function getMethodOptionalParamVals($method, $optString, $options) { $params = $method->getParameters(); $requiredParamCount = $method->getNumberOfRequiredParameters(); $optionalParamCount = $method->getNumberOfParameters() - $requiredParamCount; $optChars = str_replace(':', '', $optString); $paramVals = array(); //Switchless as in specified w/o a command-line switch, i.e. for: //ymdt destroy myappdir #myappdir is a switchless optional param //ymdt destroy -a myappid #myappid is a command-line switch optional param $switchlessOptionalParamCount = $optionalParamCount - strlen($optChars); //Grab the switchless optional params that came from argv, unspecified ones //will get default vals. +2 excludes 'ymdt' and command $paramVals = array_slice($GLOBALS['argv'], $requiredParamCount + 2, $switchlessOptionalParamCount); $defaultSwitchlessOptionalParamCount = $switchlessOptionalParamCount - count($paramVals); $defaultSwitchlessOptionalParamCount = max(0, $defaultSwitchlessOptionalParamCount); // echo "sOPC $switchlessOptionalParamCount, dSOPC // $defaultSwitchlessOptionalParamCount rPC $requiredParamCount\n"; // echo "paramVals " . print_r($paramVals, true); for($i = 0; $i < $defaultSwitchlessOptionalParamCount; $i++){ $paramNdx = $requiredParamCount + $i; $paramVals[] = $params[$paramNdx]->getDefaultValue(); } //Grab the optional params that are specified by a command-line switch $switchOptionalParamCount = strlen($optChars); for($i = 0; $i < $switchOptionalParamCount; $i++){ $paramNdx = $requiredParamCount + $defaultSwitchlessOptionalParamCount + $i; $optChar = $optChars[$i]; if(array_key_exists($optChar, $options)) $paramVals[] = strlen($options[$optChar]) ? $options[$optChar] : true; else $paramVals[] = $params[$paramNdx]->getDefaultValue(); } return $paramVals; } function main() { echo "Yahoo! Mail Development Tool Version " . YMDT_Version . "\n"; static $legal_cmds = array('apps', 'create', 'ls', 'fixup', 'get', 'help', 'put', 'del', 'dev', 'upgrade', 'publish', 'destroy', 'tester'); if(count($GLOBALS['argv']) < 2){ echo "\nFirst argument must be a command\n\n"; show_usage(); } $cmd = $GLOBALS['argv'][1]; if(!in_array($cmd, $legal_cmds)){ echo "\nFirst argument must be a command.\n" ."'$cmd' is not a legal command.\n\n"; show_usage(); } //get constructor optstring, opt params //get command optstring, opt params, required params list($ctor, $ctorOptString, $ctorRequiredParamCount) = getMethodMetadata('YMDT', '__construct'); list($method, $cmdOptString, $cmdRequiredParamCount) = getMethodMetadata('YMDT', $cmd); $ctorOptChars = str_replace(':', '', $ctorOptString); verify(!strpbrk($ctorOptChars, $cmdOptString), "Internal error: $cmd optstring clashes with common optstring."); //array of option char ==> value $options = getopts_and_reindex($cmdOptString . $ctorOptString); //Invoke constructor with cookieFname, //All optional take defaults unless overridden at command line. global $HOMEDIR; $paramVals = array("$HOMEDIR/.ymdtcookie"); $paramVals = array_merge($paramVals, getMethodOptionalParamVals($ctor, $ctorOptString, $options)); $paramVals = array_slice($paramVals, 0, -1); //create it // echo "params: " . print_r($paramVals, true) . "\n\n"; $class = new ReflectionClass('YMDT'); $ymdt = $class->newInstanceArgs($paramVals); $argCount = count($GLOBALS['argv']) - 2; if($argCount < $cmdRequiredParamCount){ echo "\n$cmd expected at least $cmdRequiredParamCount argument(s), " . "got $argCount\n" . "run ymdt help $cmd for more information.\n\n"; exit(-1); } $paramVals = array_slice($GLOBALS['argv'], 2, $cmdRequiredParamCount); $paramVals = array_merge($paramVals, getMethodOptionalParamVals($method, $cmdOptString, $options)); // echo print_r($paramVals, true); //invoke the command echo "$cmd:\n"; // implode(' ', array_slice($paramVals, 0, $cmdRequiredParamCount)). "\n\n"; try { $method->invokeArgs($ymdt, $paramVals); } catch(Exception $e){ global $DEBUG_MODE; echo $e->getMessage() . "\n"; if($DEBUG_MODE) throw $e; } } main(); ?>