diff --git a/conf/default.conf.php b/conf/default.conf.php index 8895942921..f8c9e3e427 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -1,1093 +1,1093 @@ null, // If you have multiple environments, provide the production environment URI // here so that emails, etc., generated in development/sandbox environments // contain the right links. 'phabricator.production-uri' => null, // -- IMPORTANT! Security! -------------------------------------------------- // // IMPORTANT: By default, Phabricator serves files from the same domain the // application lives on. This is convenient but not secure: it creates a large // class of vulnerabilities which can not be generally mitigated. // // To avoid this, you should configure a second domain in the same way you // have the primary domain configured (e.g., point it at the same machine and // set up the same vhost rules) and provide it here. For instance, if your // primary install is on "http://www.phabricator-example.com/", you could // configure "http://www.phabricator-files.com/" and specify the entire // domain (with protocol) here. This will enforce that files are // served only from the alternate domain. Ideally, you should use a // completely separate domain name rather than just a different subdomain. // // It is STRONGLY RECOMMENDED that you configure this. Your install is NOT // SECURE unless you do so. 'security.alternate-file-domain' => null, // Default key for HMAC digests where the key is not important (i.e., the // hash itself is secret). You can change this if you want (to any other // string), but doing so will break existing sessions and CSRF tokens. 'security.hmac-key' => '[D\t~Y7eNmnQGJ;rnH6aF;m2!vJ8@v8C=Cs:aQS\.Qw', // If the web server responds to both HTTP and HTTPS requests but you want // users to connect with only HTTPS, you can set this to true to make // Phabricator redirect HTTP requests to HTTPS. // // Normally, you should just configure your server not to accept HTTP traffic, // but this setting may be useful if you originally used HTTP and have now // switched to HTTPS but don't want to break old links, or if your webserver // sits behind a load balancer which terminates HTTPS connections and you // can not reasonably configure more granular behavior there. // // NOTE: Phabricator determines if a request is HTTPS or not by examining the // PHP $_SERVER['HTTPS'] variable. If you run Apache/mod_php this will // probably be set correctly for you automatically, but if you run Phabricator // as CGI/FCGI (e.g., through nginx or lighttpd), you need to configure your // web server so that it passes the value correctly based on the connection // type. Alternatively, you can add a PHP snippet to the top of this // configuration file to directly set $_SERVER['HTTPS'] to the correct value. 'security.require-https' => false, // Is Phabricator permitted to make outbound HTTP requests? 'security.allow-outbound-http' => true, // -- Internationalization -------------------------------------------------- // // This allows customizing texts used in Phabricator. The class must extend // PhabricatorTranslation. 'translation.provider' => 'PhabricatorEnglishTranslation', // You can use 'translation.override' if you don't want to create a full // translation to give users an option for switching to it and you just want // to override some strings in the default translation. 'translation.override' => array(), // -- Access Policies ------------------------------------------------------- // // Phabricator allows you to set the visibility of objects (like repositories // and source code) to "Public", which means anyone on the internet can see // them, even without being logged in. This is great for open source, but // some installs may never want to make anything public, so this policy is // disabled by default. You can enable it here, which will let you set the // policy for objects to "Public". With this option disabled, the most open // policy is "All Users", which means users must be logged in to view things. 'policy.allow-public' => false, // -- Logging --------------------------------------------------------------- // // To enable the Phabricator access log, specify a path here. The Phabricator // access log can provide more detailed information about Phabricator access // than normal HTTP access logs (for instance, it can show logged-in users, // controllers, and other application data). If not set, no log will be // written. // // Make sure the PHP process can write to the log! 'log.access.path' => null, // Format for the access log. If not set, the default format will be used: // // "[%D]\t%h\t%u\t%M\t%C\t%m\t%U\t%c\t%T" // // Available variables are: // // - %c The HTTP response code. // - %C The controller which handled the request. // - %D The request date. // - %e Epoch timestamp. // - %h The webserver's host name. // - %p The PID of the server process. // - %R The HTTP referrer. // - %r The remote IP. // - %T The request duration, in microseconds. // - %U The request path. // - %u The logged-in username, if one is logged in. // - %P The logged-in user PHID, if one is logged in. // - %M The HTTP method. // - %m For conduit, the Conduit method which was invoked. // // If a variable isn't available (for example, %m appears in the file format // but the request is not a Conduit request), it will be rendered as "-". // // Note that the default format is subject to change in the future, so if you // rely on the log's format, specify it explicitly. 'log.access.format' => null, // -- DarkConsole ----------------------------------------------------------- // // DarkConsole is a administrative debugging/profiling tool built into // Phabricator. You can leave it disabled unless you're developing against // Phabricator. // Determines whether or not DarkConsole is available. DarkConsole exposes // some data like queries and stack traces, so you should be careful about // turning it on in production (although users can not normally see it, even // if the deployment configuration enables it). 'darkconsole.enabled' => false, // Always enable DarkConsole, even for logged out users. This potentially // exposes sensitive information to users, so make sure untrusted users can // not access an install running in this mode. You should definitely leave // this off in production. It is only really useful for using DarkConsole // utilities to debug or profile logged-out pages. You must set // 'darkconsole.enabled' to use this option. 'darkconsole.always-on' => false, // Map of additional configuration values to lock. 'config.lock' => array(), // Map of additional configuration values to hide. 'config.hide' => array(), // Map of additional configuration values to mask. 'config.mask' => array(), // Ignore setup warnings of the following issues. 'config.ignore-issues' => array(), // -- MySQL --------------------------------------------------------------- // // Class providing database configuration. It must implement // DatabaseConfigurationProvider. 'mysql.configuration-provider' => 'DefaultDatabaseConfigurationProvider', // The username to use when connecting to MySQL. 'mysql.user' => 'root', // The password to use when connecting to MySQL. 'mysql.pass' => '', // The MySQL server to connect to. 'mysql.host' => 'localhost', // If you want to connect to a different port than the default (which is 3306) 'mysql.port' => null, // Phabricator supports PHP extensions MySQL and MySQLi. It is possible to // implement also other access mechanism (e.g. PDO_MySQL). The class must // extend AphrontMySQLDatabaseConnectionBase. 'mysql.implementation' => (extension_loaded('mysqli') ? 'AphrontMySQLiDatabaseConnection' : 'AphrontMySQLDatabaseConnection'), // -- Notifications --------------------------------------------------------- // // Set this to true to enable real-time notifications. You must also run a // notification server for this to work. Consult the documentation in // "Notifications User Guide: Setup and Configuration" for instructions. 'notification.enabled' => false, // Client port for the realtime server to listen on, and for realtime clients // to connect to. Use "localhost" if you are running the notification server // on the same host as the web server. 'notification.client-uri' => 'http://localhost:22280/', // URI and port for the notification root server. 'notification.server-uri' => 'http://localhost:22281/', // The server must be started as root so it can bind to privileged ports, but // if you specify a user here it will drop permissions after binding. 'notification.user' => null, // Location where the server should log to. 'notification.log' => '/var/log/aphlict.log', // PID file to use. 'notification.pidfile' => '/var/run/aphlict.pid', // Enable this option to get additional debug output in the browser. 'notification.debug' => false, // -- Email ----------------------------------------------------------------- // // Some Phabricator tools send email notifications, e.g. when Differential // revisions are updated or Maniphest tasks are changed. These options allow // you to configure how email is delivered. // You can test your mail setup by going to "MetaMTA" in the web interface, // clicking "Send New Message", and then composing a message. // Default address to send mail "From". 'metamta.default-address' => 'noreply@example.com', // Domain used to generate Message-IDs. 'metamta.domain' => 'example.com', // When a message is sent to multiple recipients (for example, several // reviewers on a code review), Phabricator can either deliver one email to // everyone (e.g., "To: alincoln, usgrant, htaft") or separate emails to each // user (e.g., "To: alincoln", "To: usgrant", "To: htaft"). The major // advantages and disadvantages of each approach are: // // - One mail to everyone: // - Recipients can see To/Cc at a glance. // - If you use mailing lists, you won't get duplicate mail if you're // a normal recipient and also Cc'd on a mailing list. // - Getting threading to work properly is harder, and probably requires // making mail less useful by turning off options. // - Sometimes people will "Reply All" and everyone will get two mails, // one from the user and one from Phabricator turning their mail into // a comment. // - Not supported with a private reply-to address. // - Mails are sent in the server default translation. // - One mail to each user: // - Recipients need to look in the mail body to see To/Cc. // - If you use mailing lists, recipients may sometimes get duplicate // mail. // - Getting threading to work properly is easier, and threading settings // can be customzied by each user. // - "Reply All" no longer spams all other users. // - Required if private reply-to addresses are configured. // - Mails are sent in the language of user preference. // // In the code, splitting one outbound email into one-per-recipient is // sometimes referred to as "multiplexing". 'metamta.one-mail-per-recipient' => true, // When sending a message that has no To recipient (i.e. all recipients // are CC'd, for example when multiplexing mail), set the To field to the // following value. If no value is set, messages with no To will have // their CCs upgraded to To. 'metamta.placeholder-to-recipient' => null, // When a user takes an action which generates an email notification (like // commenting on a Differential revision), Phabricator can either send that // mail "From" the user's email address (like "alincoln@logcabin.com") or // "From" the 'metamta.default-address' address. The user experience is // generally better if Phabricator uses the user's real address as the "From" // since the messages are easier to organize when they appear in mail clients, // but this will only work if the server is authorized to send email on behalf // of the "From" domain. Practically, this means: // - If you are doing an install for Example Corp and all the users will // have corporate @corp.example.com addresses and any hosts Phabricator // is running on are authorized to send email from corp.example.com, // you can enable this to make the user experience a little better. // - If you are doing an install for an open source project and your // users will be registering via Facebook and using personal email // addresses, you MUST NOT enable this or virtually all of your outgoing // email will vanish into SFP blackholes. // - If your install is anything else, you're much safer leaving this // off since the risk in turning it on is that your outgoing mail will // mostly never arrive. 'metamta.can-send-as-user' => false, // Limit the maximum size of the body of an email generated for a diff // (in bytes). 'metamta.email-body-limit' => 524288, // Adapter class to use to transmit mail to the MTA. The default uses // PHPMailerLite, which will invoke "sendmail". This is appropriate // if sendmail actually works on your host, but if you haven't configured mail // it may not be so great. A number of other mailers are available (e.g., SES, // SendGrid, SMTP, custom mailers), consult "Configuring Outbound Email" in // the documentation for details. 'metamta.mail-adapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', // When email is sent, what format should Phabricator use for user's // email addresses? Valid values are: // - 'short' - 'gwashington ' // - 'real' - 'George Washington ' // - 'full' - 'gwashington (George Washington) ' // The default is 'full'. 'metamta.user-address-format' => 'full', // If you're using PHPMailer to send email, provide the mailer and options // here. PHPMailer is much more enormous than PHPMailerLite, and provides more // mailers and greater enormity. You need it when you want to use SMTP // instead of sendmail as the mailer. 'phpmailer.mailer' => 'smtp', 'phpmailer.smtp-host' => '', 'phpmailer.smtp-port' => 25, // When using PHPMailer with SMTP, you can set this to one of "tls" or "ssl" // to use TLS or SSL. Leave it blank for vanilla SMTP. If you're sending // via Gmail, set it to "ssl". 'phpmailer.smtp-protocol' => '', // Set following if your smtp server requires authentication. 'phpmailer.smtp-user' => null, 'phpmailer.smtp-password' => null, // If you're using Amazon SES to send email, provide your AWS access key // and AWS secret key here. To set up Amazon SES with Phabricator, you need // to: // - Make sure 'metamta.mail-adapter' is set to: // "PhabricatorMailImplementationAmazonSESAdapter" // - Make sure 'metamta.can-send-as-user' is false. // - Make sure 'metamta.default-address' is configured to something sensible. // - Make sure 'metamta.default-address' is a validated SES "From" address. 'amazon-ses.access-key' => null, 'amazon-ses.secret-key' => null, // If you're using Sendgrid to send email, provide your access credentials // here. This will use the REST API. You can also use Sendgrid as a normal // SMTP service. 'sendgrid.api-user' => null, 'sendgrid.api-key' => null, // You can configure a reply handler domain so that email sent from Maniphest // will have a special "Reply To" address like "T123+82+af19f@example.com" // that allows recipients to reply by email and interact with tasks. For // instructions on configurating reply handlers, see the article // "Configuring Inbound Email" in the Phabricator documentation. By default, // this is set to 'null' and Phabricator will use a generic 'noreply@' address // or the address of the acting user instead of a special reply handler // address (see 'metamta.default-address'). If you set a domain here, // Phabricator will begin generating private reply handler addresses. See // also 'metamta.maniphest.reply-handler' to further configure behavior. // This key should be set to the domain part after the @, like "example.com". 'metamta.maniphest.reply-handler-domain' => null, // You can follow the instructions in "Configuring Inbound Email" in the // Phabricator documentation and set 'metamta.maniphest.reply-handler-domain' // to support updating Maniphest tasks by email. If you want more advanced // customization than this provides, you can override the reply handler // class with an implementation of your own. This will allow you to do things // like have a single public reply handler or change how private reply // handlers are generated and validated. // // This key should be set to a loadable subclass of // PhabricatorMailReplyHandler. 'metamta.maniphest.reply-handler' => 'ManiphestReplyHandler', // If you don't want phabricator to take up an entire domain // (or subdomain for that matter), you can use this and set a common // prefix for mail sent by phabricator. It will make use of the fact that // a mail-address such as phabricator+D123+1hjk213h@example.com will be // delivered to the phabricator users mailbox. // Set this to the left part of the email address and it well get // prepended to all outgoing mail. If you want to use e.g. // 'phabricator@example.com' this should be set to 'phabricator'. 'metamta.single-reply-handler-prefix' => null, // Prefix prepended to mail sent by Maniphest. You can change this to // distinguish between testing and development installs, for example. 'metamta.maniphest.subject-prefix' => '[Maniphest]', // See 'metamta.maniphest.reply-handler-domain'. This does the same thing, but // affects Pholio. 'metamta.pholio.reply-handler-domain' => null, // Prefix prepended to mail sent by Pholio. 'metamta.pholio.subject-prefix' => '[Pholio]', // See 'metamta.maniphest.reply-handler-domain'. This does the same thing, but // affects Macro. 'metamta.macro.reply-handler-domain' => null, // Prefix prepended to mail sent by Macro. 'metamta.macro.subject-prefix' => '[Macro]', // See 'metamta.maniphest.reply-handler-domain'. This does the same thing, // but allows email replies via Differential. 'metamta.differential.reply-handler-domain' => null, // See 'metamta.maniphest.reply-handler'. This does the same thing, but // affects Differential. 'metamta.differential.reply-handler' => 'DifferentialReplyHandler', // Prefix prepended to mail sent by Differential. 'metamta.differential.subject-prefix' => '[Differential]', // Set this to true if you want patches to be attached to mail from // Differential. This won't work if you are using SendGrid as your mail // adapter. 'metamta.differential.attach-patches' => false, // To include patches in email bodies, set this to a positive integer. Patches // will be inlined if they are at most that many lines. For instance, a value // of 100 means "inline patches if they are no longer than 100 lines". By // default, patches are not inlined. 'metamta.differential.inline-patches' => 0, // If you enable either of the options above, you can choose what format // patches are sent in. Valid options are 'unified' (like diff -u) or 'git'. 'metamta.differential.patch-format' => 'unified', // Enables a different format for comments in differential emails. // Differential will create unified diffs around the comment, which // will give enough context for people who are only viewing the // reviews in email to understand what is going on. The context will // be created based on the range of the comment. 'metamta.differential.unified-comment-context' => false, // Prefix prepended to mail sent by Diffusion. 'metamta.diffusion.subject-prefix' => '[Diffusion]', // See 'metamta.maniphest.reply-handler-domain'. This does the same thing, // but allows email replies via Diffusion. 'metamta.diffusion.reply-handler-domain' => null, // See 'metamta.maniphest.reply-handler'. This does the same thing, but // affects Diffusion. 'metamta.diffusion.reply-handler' => 'PhabricatorAuditReplyHandler', // Set this to true if you want patches to be attached to commit notifications // from Diffusion. This won't work with SendGrid. 'metamta.diffusion.attach-patches' => false, // To include patches in Diffusion email bodies, set this to a positive // integer. Patches will be inlined if they are at most that many lines. // By default, patches are not inlined. 'metamta.diffusion.inline-patches' => 0, // If you've enabled attached patches or inline patches for commit emails, you // can establish a hard byte limit on their size. You should generally set // reasonable byte and time limits (defaults are 1MB and 60 seconds) to avoid // sending ridiculously enormous email for changes like "importing an external // library" or "accidentally committed this full-length movie as text". 'metamta.diffusion.byte-limit' => 1024 * 1024, // If you've enabled attached patches or inline patches for commit emails, you // can establish a hard time limit on generating them. 'metamta.diffusion.time-limit' => 60, // Prefix prepended to mail sent by Package. 'metamta.package.subject-prefix' => '[Package]', // See 'metamta.maniphest.reply-handler'. This does similar thing for package // except that it only supports sending out mail and doesn't handle incoming // email. 'metamta.package.reply-handler' => 'OwnersPackageReplyHandler', // By default, Phabricator generates unique reply-to addresses and sends a // separate email to each recipient when you enable reply handling. This is // more secure than using "From" to establish user identity, but can mean // users may receive multiple emails when they are on mailing lists. Instead, // you can use a single, non-unique reply to address and authenticate users // based on the "From" address by setting this to 'true'. This trades away // a little bit of security for convenience, but it's reasonable in many // installs. Object interactions are still protected using hashes in the // single public email address, so objects can not be replied to blindly. 'metamta.public-replies' => false, // You can configure an email address like "bugs@phabricator.example.com" // which will automatically create Maniphest tasks when users send email // to it. This relies on the "From" address to authenticate users, so it is // is not completely secure. To set this up, enter a complete email // address like "bugs@phabricator.example.com" and then configure mail to // that address so it routed to Phabricator (if you've already configured // reply handlers, you're probably already done). See "Configuring Inbound // Email" in the documentation for more information. 'metamta.maniphest.public-create-email' => null, // If you enable 'metamta.public-replies', Phabricator uses "From" to // authenticate users. You can additionally enable this setting to try to // authenticate with 'Reply-To'. Note that this is completely spoofable and // insecure (any user can set any 'Reply-To' address) but depending on the // nature of your install or other deliverability conditions this might be // okay. Generally, you can't do much more by spoofing Reply-To than be // annoying (you can write but not read content). But, you know, this is // still **COMPLETELY INSECURE**. 'metamta.insecure-auth-with-reply-to' => false, // If you enable 'metamta.maniphest.public-create-email' and create an // email address like "bugs@phabricator.example.com", it will default to // rejecting mail which doesn't come from a known user. However, you might // want to let anyone send email to this address; to do so, set a default // author here (a Phabricator username). A typical use of this might be to // create a "System Agent" user called "bugs" and use that name here. If you // specify a valid username, mail will always be accepted and used to create // a task, even if the sender is not a system user. The original email // address will be stored in an 'From Email' field on the task. 'metamta.maniphest.default-public-author' => null, 'metamta.herald.show-hints' => true, // You can disable the hints under "REPLY HANDLER ACTIONS" if users prefer // smaller messages. The actions themselves will still work properly. 'metamta.reply.show-hints' => true, // You can disable the "To:" and "Cc:" footers in mail if users prefer // smaller messages. 'metamta.recipients.show-hints' => true, // If this option is enabled, Phabricator will add a "Precedence: bulk" // header to transactional mail (e.g., Differential, Maniphest and Herald // notifications). This may improve the behavior of some auto-responder // software and prevent it from replying. However, it may also cause // deliverability issues -- notably, you currently can not send this header // via Amazon SES, and enabling this option with SES will prevent delivery // of any affected mail. 'metamta.precedence-bulk' => false, // Mail.app on OS X Lion won't respect threading headers unless the subject // is prefixed with "Re:". If you enable this option, Phabricator will add // "Re:" to the subject line of all mail which is expected to thread. If // you've set 'metamta.one-mail-per-recipient', users can override this // setting in their preferences. 'metamta.re-prefix' => false, // If true, allow MetaMTA to change mail subjects to put text like // '[Accepted]' and '[Commented]' in them. This makes subjects more useful, // but might break threading on some clients. If you've set // 'metamta.one-mail-per-recipient', users can override this setting in their // preferences. 'metamta.vary-subjects' => true, // -- Auth ------------------------------------------------------------------ // // If true, email addresses must be verified (by clicking a link in an // email) before a user can login. By default, verification is optional // unless 'auth.email-domains' is nonempty (see below). 'auth.require-email-verification' => false, // You can restrict allowed email addresses to certain domains (like // "yourcompany.com") by setting a list of allowed domains here. Users will // only be allowed to register using email addresses at one of the domains, // and will only be able to add new email addresses for these domains. If // you configure this, it implies 'auth.require-email-verification'. // // To configure email domains, set a list of domains like this: // // array( // 'yourcompany.com', // 'yourcompany.co.uk', // ) // // You should omit the "@" from domains. Note that the domain must match // exactly. If you allow "yourcompany.com", that permits "joe@yourcompany.com" // but rejects "joe@mail.yourcompany.com". 'auth.email-domains' => array(), // You can provide an arbitrary block of HTML here, which will appear on the // login screen. Normally, you'd use this to provide login or registration // instructions to users. 'auth.login-message' => null, // -- Accounts -------------------------------------------------------------- // // Is basic account information (email, real name, profile picture) editable? // If you set up Phabricator to automatically synchronize account information // from some other authoritative system, you can disable this to ensure // information remains consistent across both systems. 'account.editable' => true, // When users set or reset a password, it must have at least this many // characters. 'account.minimum-password-length' => 8, // -- Recaptcha ------------------------------------------------------------- // // Is Recaptcha enabled? If disabled, captchas will not appear. You should // enable Recaptcha if your install is public-facing, as it hinders // brute-force attacks. 'recaptcha.enabled' => false, // Your Recaptcha public key, obtained from Recaptcha. 'recaptcha.public-key' => null, // Your Recaptcha private key, obtained from Recaptcha. 'recaptcha.private-key' => null, // -- Misc ------------------------------------------------------------------ // // This is hashed with other inputs to generate CSRF tokens. If you want, you // can change it to some other string which is unique to your install. This // will make your install more secure in a vague, mostly theoretical way. But // it will take you like 3 seconds of mashing on your keyboard to set it up so // you might as well. 'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3', // This is hashed with other inputs to generate mail tokens. If you want, you // can change it to some other string which is unique to your install. In // particular, you will want to do this if you accidentally send a bunch of // mail somewhere you shouldn't have, to invalidate all old reply-to // addresses. 'phabricator.mail-key' => '5ce3e7e8787f6e40dfae861da315a5cdf1018f12', // PHP requires that you set a timezone in your php.ini before using date // functions, or it will emit a warning. If this isn't possible (for instance, // because you are using HPHP) you can set some valid constant for // date_default_timezone_set() here and Phabricator will set it on your // behalf, silencing the warning. 'phabricator.timezone' => null, // Show stack traces when unhandled exceptions occur, force reloading of // static resources (skipping the cache), show an error callout if a page // generated PHP errors, warnings, or notices, force disk reads when // reloading, and generally make development easier. This option should not // be enabled in production. 'phabricator.developer-mode' => false, // Should Phabricator show beta applications on the homepage 'phabricator.show-beta-applications' => false, // Contains a list of uninstalled applications 'phabricator.uninstalled-applications' => array(), // Allowing non-members to interact with tasks over email. 'phabricator.allow-email-users' => false, // -- Welcome Screen -------------------------------------------------------- // // The custom HTML content for the Phabricator welcome screen. 'welcome.html' => null, // -- Files ----------------------------------------------------------------- // // Lists which uploaded file types may be viewed in the browser. If a file // has a mime type which does not appear in this list, it will always be // downloaded instead of displayed. This is mainly a usability // consideration, since browsers tend to freak out when viewing enormous // binary files. // // The keys in this array are viewable mime types; the values are the mime // types they will be delivered as when they are viewed in the browser. // // IMPORTANT: Configure 'security.alternate-file-domain' above! Your install // is NOT safe if it is left unconfigured. 'files.viewable-mime-types' => array( 'image/jpeg' => 'image/jpeg', 'image/jpg' => 'image/jpg', 'image/png' => 'image/png', 'image/gif' => 'image/gif', 'text/plain' => 'text/plain; charset=utf-8', 'text/x-diff' => 'text/plain; charset=utf-8', // ".ico" favicon files, which have mime type diversity. See: // http://en.wikipedia.org/wiki/ICO_(file_format)#MIME_type 'image/x-ico' => 'image/x-icon', 'image/x-icon' => 'image/x-icon', 'image/vnd.microsoft.icon' => 'image/x-icon', ), // List of mime types which can be used as the source for an tag. // This should be a subset of 'files.viewable-mime-types' and exclude files // like text. 'files.image-mime-types' => array( 'image/jpeg' => true, 'image/jpg' => true, 'image/png' => true, 'image/gif' => true, 'image/x-ico' => true, 'image/x-icon' => true, 'image/vnd.microsoft.icon' => true, ), // Configuration option for enabling imagemagick // to resize animated profile pictures (gif) 'files.enable-imagemagick' => false, // -- Storage --------------------------------------------------------------- // // Phabricator allows users to upload files, and can keep them in various // storage engines. This section allows you to configure which engines // Phabricator will use, and how it will use them. // The largest filesize Phabricator will store in the MySQL BLOB storage // engine, which just uses a database table to store files. While this isn't a // best practice, it's really easy to set up. Set this to 0 to disable use of // the MySQL blob engine. 'storage.mysql-engine.max-size' => 1000000, // Phabricator provides a local disk storage engine, which just writes files // to some directory on local disk. The webserver must have read/write // permissions on this directory. This is straightforward and suitable for // most installs, but will not scale past one web frontend unless the path // is actually an NFS mount, since you'll end up with some of the files // written to each web frontend and no way for them to share. To use the // local disk storage engine, specify the path to a directory here. To // disable it, specify null. 'storage.local-disk.path' => null, // If you want to store files in Amazon S3, specify an AWS access and secret // key here and a bucket name below. 'amazon-s3.access-key' => null, 'amazon-s3.secret-key' => null, // To use a custom endpoint, specify it here. Normally, you do not need to // configure this. 'amazon-s3.endpoint' => null, // Set this to a valid Amazon S3 bucket to store files there. You must also // configure S3 access keys above. 'storage.s3.bucket' => null, // Phabricator uses a storage engine selector to choose which storage engine // to use when writing file data. If you add new storage engines or want to // provide very custom rules (e.g., write images to one storage engine and // other files to a different one), you can provide an alternate // implementation here. The default engine will use choose MySQL, Local Disk, // and S3, in that order, if they have valid configurations above and a file // fits within configured limits. 'storage.engine-selector' => 'PhabricatorDefaultFileStorageEngineSelector', // Set the size of the largest file a user may upload. This is used to render // text like "Maximum file size: 10MB" on interfaces where users can upload // files, and files larger than this size will be rejected. // // Specify this limit in bytes, or using a "K", "M", or "G" suffix. // // NOTE: Setting this to a large size is NOT sufficient to allow users to // upload large files. You must also configure a number of other settings. To // configure file upload limits, consult the article "Configuring File Upload // Limits" in the documentation. Once you've configured some limit across all // levels of the server, you can set this limit to an appropriate value and // the UI will then reflect the actual configured limit. 'storage.upload-size-limit' => null, // Phabricator puts databases in a namespace, which defualts to "phabricator" // -- for instance, the Differential database is named // "phabricator_differential" by default. You can change this namespace if you // want. Normally, you should not do this unless you are developing // Phabricator and using namespaces to separate multiple sandbox datasets. 'storage.default-namespace' => 'phabricator', // -- Search ---------------------------------------------------------------- // // Phabricator supports Elastic Search; to use it, specify a host like // 'http://elastic.example.com:9200/' here. 'search.elastic.host' => null, // Phabricator uses a search engine selector to choose which search engine // to use when indexing and reconstructing documents, and when executing // queries. You can override the engine selector to provide a new selector // class which can select some custom engine you implement, if you want to // store your documents in some search engine which does not have default // support. 'search.engine-selector' => 'PhabricatorDefaultSearchEngineSelector', // -- Differential ---------------------------------------------------------- // // List of file regexps where whitespace is meaningful and should not // use 'ignore-all' by default 'differential.whitespace-matters' => array( '/\.py$/', '/\.l?hs$/', ), // Differential has a required "Test Plan" field by default. You can make it // optional by setting this to false. You can also completely remove it above, // if you prefer. 'differential.require-test-plan-field' => true, // If you set this to true, users can "!accept" revisions via email (normally, // they can take other actions but can not "!accept"). This action is disabled // by default because email authentication can be configured to be very weak, // and, socially, email "!accept" is kind of sketchy and implies revisions may // not actually be receiving thorough review. 'differential.enable-email-accept' => false, // List of file regexps that should be treated as if they are generated by // an automatic process, and thus get hidden by default in differential. 'differential.generated-paths' => array( // '/config\.h$/', // '#/autobuilt/#', ), - // If you set this to true, users can accept their own revisions. This action + // If you set this to true, users can accept their own revisions. This action // is disabled by default because it's most likely not a behavior you want, // but it proves useful if you are working alone on a project and want to make // use of all of differential's features. 'differential.allow-self-accept' => false, // If you set this to true, any user can close any revision so long as it has // been accepted. This can be useful depending on your development model. For // example, github-style pull requests where the reviewer is often the // actual committer can benefit from turning this option to true. If false, // only the submitter can close a revision. 'differential.always-allow-close' => false, // If you set this to true, any user can abandon any revision. If false, only // the submitter can abandon a revision. 'differential.always-allow-abandon' => false, // If you set this to true, any user can reopen a revision so long as it has // been closed. This can be useful if a revision is accidentally closed or - // if a developer changes his or her mind after closing a revision. If it is + // if a developer changes his or her mind after closing a revision. If it is // false, reopening is not allowed. 'differential.allow-reopen' => false, // Revisions newer than this number of days are marked as fresh in Action // Required and Revisions Waiting on You views. Only work days (not weekends // and holidays) are included. Set to 0 to disable this feature. 'differential.days-fresh' => 1, // Similar to 'differential.days-fresh' but marks stale revisions. If the // revision is even older than it is marked as old. 'differential.days-stale' => 3, // -- Repositories ---------------------------------------------------------- // // The default location in which to store local copies of repositories. // Anything stored in this directory will be assumed to be under the // control of phabricator, which means that Phabricator will try to do some // maintenance on working copies if there are problems (such as a change // to the remote origin url). This maintenance may include completely // removing (and recloning) anything in this directory. // // When set to null, this option is ignored (i.e. Phabricator will not fully // control any working copies). 'repository.default-local-path' => null, // -- Maniphest ------------------------------------------------------------- // // What should the default task priority be in create flows? // See the constants in @{class:ManiphestTaskPriority} for valid values. // Defaults to "needs triage". 'maniphest.default-priority' => 90, // -- Phame ----------------------------------------------------------------- // // Should Phame users have Disqus comment widget, and if so what's the // website shortname to use? For example, secure.phabricator.org uses // "phabricator", which we registered with Disqus. If you aren't familiar // with Disqus, see: // Disqus quick start guide - http://docs.disqus.com/help/4/ // Information on shortnames - http://docs.disqus.com/help/68/ 'disqus.shortname' => null, // Directories to look for Phame skins inside of. 'phame.skins' => array( 'externals/skins/', ), // -- Remarkup -------------------------------------------------------------- // // If you enable this, linked YouTube videos will be embeded inline. This has // mild security implications (you'll leak referrers to YouTube) and is pretty // silly (but sort of awesome). 'remarkup.enable-embedded-youtube' => false, // -- Cache ----------------------------------------------------------------- // // Set this to false to disable the use of gzdeflate()-based compression in // some caches. This may give you less performant (but more debuggable) // caching. 'cache.enable-deflate' => true, // -- Garbage Collection ---------------------------------------------------- // // Phabricator generates various logs and caches in the database which can // be garbage collected after a while to make the total data size more // manageable. To run garbage collection, launch a // PhabricatorGarbageCollector daemon. // These 'ttl' keys configure how much old data the GC daemon keeps around. // Objects older than the ttl will be collected. Set any value to 0 to store // data indefinitely. 'gcdaemon.ttl.herald-transcripts' => 30 * (24 * 60 * 60), 'gcdaemon.ttl.daemon-logs' => 7 * (24 * 60 * 60), 'gcdaemon.ttl.differential-parse-cache' => 14 * (24 * 60 * 60), 'gcdaemon.ttl.markup-cache' => 30 * (24 * 60 * 60), 'gcdaemon.ttl.task-archive' => 14 * (24 * 60 * 60), 'gcdaemon.ttl.general-cache' => 30 * (24 * 60 * 60), // -- Feed ------------------------------------------------------------------ // // If you set this to true, you can embed Phabricator activity feeds in other // pages using iframes. These feeds are completely public, and a login is not // required to view them! This is intended for things like open source // projects that want to expose an activity feed on the project homepage. // // NOTE: You must also set `policy.allow-public` to true for this setting // to work properly. 'feed.public' => false, // If you set this to a list of http URIs, when a feed story is published a // task will be created for each uri that posts the story data to the uri. // Daemons automagically retry failures 100 times, waiting $fail_count * 60s // between each subsequent failure. Be sure to keep the daemon console // (/daemon/) open while developing and testing your end points. You may need // to restart your daemons to start sending http requests. // // NOTE: URIs are not validated, the URI must return http status 200 within // 30 seconds, and no permission checks are performed. 'feed.http-hooks' => array(), // -- Drydock --------------------------------------------------------------- // // If you want to use Drydock's builtin EC2 Blueprints, configure your AWS // EC2 credentials here. 'amazon-ec2.access-key' => null, 'amazon-ec2.secret-key' => null, // -- Customization --------------------------------------------------------- // // Paths to additional phutil libraries to load. 'load-libraries' => array(), 'aphront.default-application-configuration-class' => 'AphrontDefaultApplicationConfiguration', // Directory that phd (the Phabricator daemon control script) should use to // track running daemons. 'phd.pid-directory' => '/var/tmp/phd/pid', // Directory that the Phabricator daemons should use to store the log file 'phd.log-directory' => '/var/tmp/phd/log', // Number of "TaskMaster" daemons that "phd start" should start. You can // raise this if you have a task backlog, or explicitly launch more with // "phd launch taskmaster". 'phd.start-taskmasters' => 4, // Launch daemons in "verbose" mode by default. This creates a lot of output, // but can help debug issues. Daemons launched in debug mode with "phd debug" // are always launched in verbose mode. See also 'phd.trace'. 'phd.verbose' => false, // Launch daemons in "trace" mode by default. This creates an ENORMOUS amount // of output, but can help debug issues. Daemons launched in debug mode with // "phd debug" are always launched in trace mdoe. See also 'phd.verbose'. 'phd.trace' => false, // This value is an input to the hash function when building resource hashes. // It has no security value, but if you accidentally poison user caches (by // pushing a bad patch or having something go wrong with a CDN, e.g.) you can // change this to something else and rebuild the Celerity map to break user // caches. Unless you are doing Celerity development, it is exceptionally // unlikely that you need to modify this. 'celerity.resource-hash' => 'd9455ea150622ee044f7931dabfa52aa', // Minify static resources by removing whitespace and comments. You should // enable this in production, but disable it in development. 'celerity.minify' => true, // You can respond to various application events by installing listeners, // which will receive callbacks when interesting things occur. Specify a list // of classes which extend PhabricatorEventListener here. 'events.listeners' => array(), // -- Syntax Highlighting --------------------------------------------------- // // Phabricator can highlight PHP by default and use Pygments for other // languages if enabled. You can provide a custom highlighter engine by // extending class PhutilSyntaxHighlighterEngine. 'syntax-highlighter.engine' => 'PhutilDefaultSyntaxHighlighterEngine', // If you want syntax highlighting for other languages than PHP then you can // install the python package 'Pygments', make sure the 'pygmentize' script is // available in the $PATH of the webserver, and then enable this. 'pygments.enabled' => false, // In places that we display a dropdown to syntax-highlight code, // this is where that list is defined. // Syntax is 'lexer-name' => 'Display Name', 'pygments.dropdown-choices' => array( 'apacheconf' => 'Apache Configuration', 'bash' => 'Bash Scripting', 'brainfuck' => 'Brainf*ck', 'c' => 'C', 'coffee-script' => 'CoffeeScript', 'cpp' => 'C++', 'css' => 'CSS', 'd' => 'D', 'diff' => 'Diff', 'django' => 'Django Templating', 'erb' => 'Embedded Ruby/ERB', 'erlang' => 'Erlang', 'go' => 'Golang', 'groovy' => 'Groovy', 'haskell' => 'Haskell', 'html' => 'HTML', 'java' => 'Java', 'js' => 'Javascript', 'json' => 'JSON', 'mysql' => 'MySQL', 'objc' => 'Objective-C', 'perl' => 'Perl', 'php' => 'PHP', 'puppet' => 'Puppet', 'rest' => 'reStructuredText', 'text' => 'Plain Text', 'python' => 'Python', 'rainbow' => 'Rainbow', 'remarkup' => 'Remarkup', 'ruby' => 'Ruby', 'xml' => 'XML', 'yaml' => 'YAML', ), // This is an override list of regular expressions which allows you to choose // what language files are highlighted as. If your projects have certain rules // about filenames or use unusual or ambiguous language extensions, you can // create a mapping here. This is an ordered dictionary of regular expressions // which will be tested against the filename. They should map to either an // explicit language as a string value, or a numeric index into the captured // groups as an integer. 'syntax.filemap' => array( // Example: Treat all '*.xyz' files as PHP. // '@\\.xyz$@' => 'php', // Example: Treat 'httpd.conf' as 'apacheconf'. // '@/httpd\\.conf$@' => 'apacheconf', // Example: Treat all '*.x.bak' file as '.x'. NOTE: we map to capturing // group 1 by specifying the mapping as "1". // '@\\.([^.]+)\\.bak$@' => 1, '@\.arcconfig$@' => 'js', '@\.arclint$@' => 'js', '@\.divinerconfig$@' => 'js', ), // Set the default monospaced font style for users who haven't set a custom // style. 'style.monospace' => '10px "Menlo", "Consolas", "Monaco", monospace', 'style.monospace.windows' => '11px "Menlo", "Consolas", "Monaco", monospace', // -- Debugging ------------------------------------------------------------- // // Enable this to change HTTP redirects into normal pages with a link to the // redirection target. For example, after you submit a form you'll get a page // saying "normally, you'd be redirected...". This is useful to examine // service or profiler information on write pathways, or debug redirects. It // also makes the UX horrible for normal use, so you should enable it only // when debugging. // // NOTE: This does not currently work for forms with Javascript "workflow", // since the redirect happens in Javascript. 'debug.stop-on-redirect' => false, // Set the rate for how often to do sampled profiling. On average, one // request for every number of requests specified here will be sampled. // Set this value to 0 to completely disable profiling. In a production // environment, this value should either be set to 0 (to disable) or to // a large number (to sample only a few requests). 'debug.profile-rate' => 0, // -- Environment ---------------------------------------------------------- // // Phabricator occasionally shells out to other binaries on the server. // An example of this is the "pygmentize" command, used to syntax-highlight // code written in languages other than PHP. By default, it is assumed that // these binaries are in the $PATH of the user running Phabricator (normally // 'apache', 'httpd', or 'nobody'). Here you can add extra directories to // the $PATH environment variable, for when these binaries are in non-standard // locations. 'environment.append-paths' => array(), // -- Audit ---------------------------------------------------------- // // Controls whether or not task creator can Close Audits 'audit.can-author-close-audit' => false, ); diff --git a/externals/balanced-php/tests/Balanced/SuiteTest.php b/externals/balanced-php/tests/Balanced/SuiteTest.php index 83cdbb162d..9c324b912d 100644 --- a/externals/balanced-php/tests/Balanced/SuiteTest.php +++ b/externals/balanced-php/tests/Balanced/SuiteTest.php @@ -1,800 +1,798 @@ *
  • $BALANCED_URL_ROOT If set applies to \Balanced\Settings::$url_root. *
  • $BALANCED_API_KEY If set applies to \Balanced\Settings::$api_key. * - * - * @group suite */ class SuiteTest extends \PHPUnit_Framework_TestCase { static $key, $marketplace, $email_counter = 0; static function _createBuyer($email_address = null, $card = null) { if ($email_address == null) $email_address = sprintf('m+%d@poundpay.com', self::$email_counter++); if ($card == null) $card = self::_createCard(); return self::$marketplace->createBuyer( $email_address, $card->uri, array('test#' => 'test_d'), 'Hobo Joe' ); } static function _createCard($account = null) { $card = self::$marketplace->createCard( '123 Fake Street', 'Jollywood', null, '90210', 'khalkhalash', '4112344112344113', null, 12, 2013); if ($account != null) { $account->addCard($card); $card = Card::get($card->uri); } return $card; } static function _createBankAccount($account = null) { $bank_account = self::$marketplace->createBankAccount( 'Homer Jay', '112233a', '121042882', 'checking' ); if ($account != null) { $account->addBankAccount($bank_account); $bank_account = $account->bank_accounts[0]; } return $bank_account; } public static function _createPersonMerchant($email_address = null, $bank_account = null) { if ($email_address == null) $email_address = sprintf('m+%d@poundpay.com', self::$email_counter++); if ($bank_account == null) $bank_account = self::_createBankAccount(); $merchant = array( 'type' => 'person', 'name' => 'William James', 'tax_id' => '393-48-3992', 'street_address' => '167 West 74th Street', 'postal_code' => '10023', 'dob' => '1842-01-01', 'phone_number' => '+16505551234', 'country_code' => 'USA' ); return self::$marketplace->createMerchant( $email_address, $merchant, $bank_account->uri ); } public static function _createBusinessMerchant($email_address = null, $bank_account = null) { if ($email_address == null) $email_address = sprintf('m+%d@poundpay.com', self::$email_counter++); if ($bank_account == null) $bank_account = self::_createBankAccount(); $merchant = array( 'type' => 'business', 'name' => 'Levain Bakery', 'tax_id' => '253912384', 'street_address' => '167 West 74th Street', 'postal_code' => '10023', 'phone_number' => '+16505551234', 'country_code' => 'USA', 'person' => array( 'name' => 'William James', 'tax_id' => '393483992', 'street_address' => '167 West 74th Street', 'postal_code' => '10023', 'dob' => '1842-01-01', 'phone_number' => '+16505551234', 'country_code' => 'USA', ), ); return self::$marketplace->createMerchant( $email_address, $merchant, $bank_account->uri ); } public static function setUpBeforeClass() { // url root $url_root = getenv('BALANCED_URL_ROOT'); if ($url_root != '') { Settings::$url_root = $url_root; } else Settings::$url_root = 'https://api.balancedpayments.com'; // api key $api_key = getenv('BALANCED_API_KEY'); if ($api_key != '') { Settings::$api_key = $api_key; } else { self::$key = new APIKey(); self::$key->save(); Settings::$api_key = self::$key->secret; } // marketplace try { self::$marketplace = Marketplace::mine(); } catch(\RESTful\Exceptions\NoResultFound $e) { self::$marketplace = new Marketplace(); self::$marketplace->save(); } } function testMarketplaceMine() { $marketplace = Marketplace::mine(); $this->assertEquals($this::$marketplace->id, $marketplace->id); } /** * @expectedException \RESTful\Exceptions\HTTPError */ function testAnotherMarketplace() { $marketplace = new Marketplace(); $marketplace->save(); } /** * @expectedException \RESTful\Exceptions\HTTPError */ function testDuplicateEmailAddress() { self::_createBuyer('dupe@poundpay.com'); self::_createBuyer('dupe@poundpay.com'); } function testIndexMarketplace() { $marketplaces = Marketplace::query()->all(); $this->assertEquals(count($marketplaces), 1); } function testCreateBuyer() { self::_createBuyer(); } function testCreateAccountWithoutEmailAddress() { self::$marketplace->createAccount(); } function testFindOrCreateAccountByEmailAddress() { $account1 = self::$marketplace->createAccount('foc@example.com'); $account2 = self::$marketplace->findOrCreateAccountByEmailAddress('foc@example.com'); $this->assertEquals($account2->id, $account2->id); $account3 = self::$marketplace->findOrCreateAccountByEmailAddress('foc2@example.com'); $this->assertNotEquals($account3->id, $account1->id); } function testGetBuyer() { $buyer1 = self::_createBuyer(); $buyer2 = Account::get($buyer1->uri); $this->assertEquals($buyer1->id, $buyer2->id); } function testMe() { $marketplace = Marketplace::mine(); $merchant = Merchant::me(); $this->assertEquals($marketplace->id, $merchant->marketplace->id); } function testDebitAndRefundBuyer() { $buyer = self::_createBuyer(); $debit = $buyer->debit( 1000, 'Softie', 'something i bought', array('hi' => 'bye') ); $refund = $debit->refund(100); } /** * @expectedException \RESTful\Exceptions\HTTPError */ function testDebitZero() { $buyer = self::_createBuyer(); $debit = $buyer->debit( 0, 'Softie', 'something i bought' ); } function testMultipleRefunds() { $buyer = self::_createBuyer(); $debit = $buyer->debit( 1500, 'Softie', 'something tart', array('hi' => 'bye')); $refunds = array( $debit->refund(100), $debit->refund(100), $debit->refund(100), $debit->refund(100)); $expected_refund_ids = array_map( function($x) { return $x->id; }, $refunds); sort($expected_refund_ids); $this->assertEquals($debit->refunds->total(), 4); // itemization $total = 0; $refund_ids = array(); foreach ($debit->refunds as $refund) { $total += $refund->amount; array_push($refund_ids, $refund->id); } sort($refund_ids); $this->assertEquals($total, 400); $this->assertEquals($expected_refund_ids, $refund_ids); // pagination $total = 0; $refund_ids = array(); foreach ($debit->refunds->paginate() as $page) { foreach ($page->items as $refund) { $total += $refund->amount; array_push($refund_ids, $refund->id); } } sort($refund_ids); $this->assertEquals($total, 400); $this->assertEquals($expected_refund_ids, $refund_ids); } function testDebitSource() { $buyer = self::_createBuyer(); $card1 = self::_createCard($buyer); $card2 = self::_createCard($buyer); $credit = $buyer->debit( 1000, 'Softie', 'something i bought' ); $this->assertEquals($credit->source->id, $card2->id); $credit = $buyer->debit( 1000, 'Softie', 'something i bought', null, $card1 ); $this->assertEquals($credit->source->id, $card1->id); } function testDebitOnBehalfOf() { $buyer = self::_createBuyer(); $merchant = self::$marketplace->createAccount(null); $card1 = self::_createCard($buyer); $debit = $buyer->debit(1000, null, null, null, null, $merchant); $this->assertEquals($debit->amount, 1000); // for now just test the debit succeeds. // TODO: once the on_behalf_of actually shows up on the response, test it. } /** * @expectedException \InvalidArgumentException */ function testDebitOnBehalfOfFailsForBuyer() { $buyer = self::_createBuyer(); $card1 = self::_createCard($buyer); $debit = $buyer->debit(1000, null, null, null, null, $buyer); } function testCreateAndVoidHold() { $buyer = self::_createBuyer(); $hold = $buyer->hold(1000); $this->assertEquals($hold->is_void, false); $hold->void(); $this->assertEquals($hold->is_void, true); } function testCreateAndCaptureHold() { $buyer = self::_createBuyer(); $hold = $buyer->hold(1000); $debit = $hold->capture(909); $this->assertEquals($debit->account->id, $buyer->id); $this->assertEquals($debit->hold->id, $hold->id); $this->assertEquals($hold->debit->id, $debit->id); } function testCreatePersonMerchant() { $merchant = self::_createPersonMerchant(); } function testCreateBusinessMerchant() { $merchant = self::_createBusinessMerchant(); } /** * @expectedException \RESTful\Exceptions\HTTPError */ function testCreditRequiresNonZeroAmount() { $buyer = self::_createBuyer(); $buyer->debit( 1000, 'Softie', 'something i bought' ); $merchant = self::_createBusinessMerchant(); $merchant->credit(0); } /** * @expectedException \RESTful\Exceptions\HTTPError */ function testCreditMoreThanEscrowBalanceFails() { $buyer = self::_createBuyer(); $buyer->credit( 1000, 'something i bought', null, null, 'Softie' ); $merchant = self::_createBusinessMerchant(); $merchant->credit(self::$marketplace->in_escrow + 1); } function testCreditDestiation() { $buyer = self::_createBuyer(); $buyer->debit(3000); # NOTE: build up escrow balance to credit $merchant = self::_createPersonMerchant(); $bank_account1 = self::_createBankAccount($merchant); $bank_account2 = self::_createBankAccount($merchant); $credit = $merchant->credit( 1000, 'something i sold', null, null, 'Softie' ); $this->assertEquals($credit->destination->id, $bank_account2->id); $credit = $merchant->credit( 1000, 'something i sold', null, $bank_account1, 'Softie' ); $this->assertEquals($credit->destination->id, $bank_account1->id); } function testAssociateCard() { $merchant = self::_createPersonMerchant(); $card = self::_createCard(); $merchant->addCard($card->uri); } function testAssociateBankAccount() { $merchant = self::_createPersonMerchant(); $bank_account = self::_createBankAccount(); $merchant->addBankAccount($bank_account->uri); } function testCardMasking() { $card = self::$marketplace->createCard( '123 Fake Street', 'Jollywood', null, '90210', 'khalkhalash', '4112344112344113', '123', 12, 2013); $this->assertEquals($card->last_four, '4113'); $this->assertFalse(property_exists($card, 'number')); } function testBankAccountMasking() { $bank_account = self::$marketplace->createBankAccount( 'Homer Jay', '112233a', '121042882', 'checking' ); $this->assertEquals($bank_account->last_four, '233a'); $this->assertEquals($bank_account->account_number, 'xxx233a'); } function testFilteringAndSorting() { $buyer = self::_createBuyer(); $debit1 = $buyer->debit(1122, null, null, array('tag' => '1')); $debit2 = $buyer->debit(3322, null, null, array('tag' => '1')); $debit3 = $buyer->debit(2211, null, null, array('tag' => '2')); $getId = function($o) { return $o->id; }; $debits = ( self::$marketplace->debits->query() ->filter(Debit::$f->meta->tag->eq('1')) ->sort(Debit::$f->created_at->asc()) ->all()); $debit_ids = array_map($getId, $debits); $this->assertEquals($debit_ids, array($debit1->id, $debit2->id)); $debits = ( self::$marketplace->debits->query() ->filter(Debit::$f->meta->tag->eq('2')) ->all()); $debit_ids = array_map($getId, $debits); $this->assertEquals($debit_ids, array($debit3->id)); $debits = ( self::$marketplace->debits->query() ->filter(Debit::$f->meta->contains('tag')) ->sort(Debit::$f->created_at->asc()) ->all()); $debit_ids = array_map($getId, $debits); $this->assertEquals($debit_ids, array($debit1->id, $debit2->id, $debit3->id)); $debits = ( self::$marketplace->debits->query() ->filter(Debit::$f->meta->contains('tag')) ->sort(Debit::$f->amount->desc()) ->all()); $debit_ids = array_map($getId, $debits); $this->assertEquals($debit_ids, array($debit2->id, $debit3->id, $debit1->id)); } function testMerchantIdentityFailure() { // NOTE: postal_code == '99999' && region == 'EX' triggers identity failure $identity = array( 'type' => 'business', 'name' => 'Levain Bakery', 'tax_id' => '253912384', 'street_address' => '167 West 74th Street', 'postal_code' => '99999', 'region' => 'EX', 'phone_number' => '+16505551234', 'country_code' => 'USA', 'person' => array( 'name' => 'William James', 'tax_id' => '393483992', 'street_address' => '167 West 74th Street', 'postal_code' => '99999', 'region' => 'EX', 'dob' => '1842-01-01', 'phone_number' => '+16505551234', 'country_code' => 'USA', ), ); try { self::$marketplace->createMerchant( sprintf('m+%d@poundpay.com', self::$email_counter++), $identity); } catch(\RESTful\Exceptions\HTTPError $e) { $this->assertEquals($e->response->code, 300); $expected = sprintf('https://www.balancedpayments.com/marketplaces/%s/kyc', self::$marketplace->id); $this->assertEquals($e->redirect_uri, $expected); $this->assertEquals($e->response->headers['Location'], $expected); return; } $this->fail('Expected exception HTTPError not raised.'); } function testInternationalCard() { $payload = array( 'card_number' => '4111111111111111', 'city' => '\xe9\x83\xbd\xe7\x95\x99\xe5\xb8\x82', 'country_code' => 'JPN', 'expiration_month' => 12, 'expiration_year' => 2014, 'name' => 'Johnny Fresh', 'postal_code' => '4020054', 'street_address' => '\xe7\x94\xb0\xe5\x8e\x9f\xef\xbc\x93\xe3\x83\xbc\xef\xbc\x98\xe3\x83\xbc\xef\xbc\x91' ); $card = self::$marketplace->cards->create($payload); $this->assertEquals($card->street_address, $payload['street_address']); } /** * @expectedException \RESTful\Exceptions\NoResultFound */ function testAccountWithEmailAddressNotFound() { self::$marketplace->accounts->query() ->filter(Account::$f->email_address->eq('unlikely@address.com')) ->one(); } function testDebitACard() { $buyer = self::_createBuyer(); $card = self::_createCard($buyer); $debit = $card->debit( 1000, 'Softie', 'something i bought', array('hi' => 'bye')); $this->assertEquals($debit->source->uri, $card->uri); } /** * @expectedException \UnexpectedValueException */ function testDebitAnUnassociatedCard() { $card = self::_createCard(); $card->debit(1000, 'Softie'); } function testCreditABankAccount() { $buyer = self::_createBuyer(); $buyer->debit(101); # NOTE: build up escrow balance to credit $merchant = self::_createPersonMerchant(); $bank_account = self::_createBankAccount($merchant); $credit = $bank_account->credit(55, 'something sour'); $this->assertEquals($credit->destination->uri, $bank_account->uri); } function testQuery() { $buyer = self::_createBuyer(); $tag = '123123123123'; $debit1 = $buyer->debit(1122, null, null, array('tag' => $tag)); $debit2 = $buyer->debit(3322, null, null, array('tag' => $tag)); $debit3 = $buyer->debit(2211, null, null, array('tag' => $tag)); $expected_debit_ids = array($debit1->id, $debit2->id, $debit3->id); $query = ( self::$marketplace->debits->query() ->filter(Debit::$f->meta->tag->eq($tag)) ->sort(Debit::$f->created_at->asc()) ->limit(1)); $this->assertEquals($query->total(), 3); $debit_ids = array(); foreach ($query as $debits) { array_push($debit_ids, $debits->id); } $this->assertEquals($debit_ids, $expected_debit_ids); $debit_ids = array($query[0]->id, $query[1]->id, $query[2]->id); $this->assertEquals($debit_ids, $expected_debit_ids); } function testBuyerPromoteToMerchant() { $merchant = array( 'type' => 'person', 'name' => 'William James', 'tax_id' => '393-48-3992', 'street_address' => '167 West 74th Street', 'postal_code' => '10023', 'dob' => '1842-01-01', 'phone_number' => '+16505551234', 'country_code' => 'USA' ); $buyer = self::_createBuyer(); $buyer->promoteToMerchant($merchant); } function testCreditAccountlessBankAccount() { $buyer = self::_createBuyer(); $buyer->debit(101); # NOTE: build up escrow balance to credit $bank_account = self::_createBankAccount(); $credit = $bank_account->credit(55, 'something sour'); $this->assertEquals($credit->bank_account->id, $bank_account->id); $bank_account = $bank_account->get($bank_account->id); $this->assertEquals($bank_account->credits->total(), 1); } function testCreditUnstoredBankAccount() { $buyer = self::_createBuyer(); $buyer->debit(101); # NOTE: build up escrow balance to credit $credit = Credit::bankAccount( 55, array( 'name' => 'Homer Jay', 'account_number' => '112233a', 'routing_number' => '121042882', 'type' => 'checking', ), 'something sour'); $this->assertFalse(property_exists($credit->bank_account, 'uri')); $this->assertFalse(property_exists($credit->bank_account, 'id')); $this->assertEquals($credit->bank_account->name, 'Homer Jay'); $this->assertEquals($credit->bank_account->account_number, 'xxx233a'); $this->assertEquals($credit->bank_account->type, 'checking'); } function testDeleteBankAccount() { $buyer = self::_createBuyer(); $buyer->debit(101); # NOTE: build up escrow balance to credit $bank_account = self::_createBankAccount(); $credit = $bank_account->credit(55, 'something sour'); $this->assertTrue(property_exists($credit->bank_account, 'uri')); $this->assertTrue(property_exists($credit->bank_account, 'id')); $bank_account = BankAccount::get($bank_account->id); $bank_account->delete(); $credit = Credit::get($credit->uri); $this->assertFalse(property_exists($credit->bank_account, 'uri')); $this->assertFalse(property_exists($credit->bank_account, 'id')); } function testGetBankAccounById() { $bank_account = self::_createBankAccount(); $bank_account_2 = BankAccount::get($bank_account->id); $this->assertEquals($bank_account_2->id, $bank_account->id); } /** * @expectedException \Balanced\Errors\InsufficientFunds */ function testInsufficientFunds() { $marketplace = Marketplace::get(self::$marketplace->uri); $amount = $marketplace->in_escrow + 100; $credit = Credit::bankAccount( $amount, array( 'name' => 'Homer Jay', 'account_number' => '112233a', 'routing_number' => '121042882', 'type' => 'checking', ), 'something sour'); } function testCreateCallback() { $callback = self::$marketplace->createCallback( 'http://example.com/php' ); $this->assertEquals($callback->url, 'http://example.com/php'); } /** * @expectedException \Balanced\Errors\BankAccountVerificationFailure */ function testBankAccountVerificationFailure() { $bank_account = self::_createBankAccount(); $buyer = self::_createBuyer(); $buyer->addBankAccount($bank_account); $verification = $bank_account->verify(); $verification->confirm(1, 2); } /** * @expectedException \Balanced\Errors\BankAccountVerificationFailure */ function testBankAccountVerificationDuplicate() { $bank_account = self::_createBankAccount(); $buyer = self::_createBuyer(); $buyer->addBankAccount($bank_account); $bank_account->verify(); $bank_account->verify(); } function testBankAccountVerificationSuccess() { $bank_account = self::_createBankAccount(); $buyer = self::_createBuyer(); $buyer->addBankAccount($bank_account); $verification = $bank_account->verify(); $verification->confirm(1, 1); // this will fail if the bank account is not verified $debit = $buyer->debit( 1000, 'Softie', 'something i bought', array('hi' => 'bye'), $bank_account ); $this->assertTrue(strpos($debit->source->uri, 'bank_account') > 0); } function testEvents() { - $prev_num_events = Marketplace::mine()->events->total(); - $account = self::_createBuyer(); - $account->debit(123); - $cur_num_events = Marketplace::mine()->events->total(); - $count = 0; + $prev_num_events = Marketplace::mine()->events->total(); + $account = self::_createBuyer(); + $account->debit(123); + $cur_num_events = Marketplace::mine()->events->total(); + $count = 0; while ($cur_num_events == $prev_num_events && $count < 10) { printf("waiting for events - %d, %d == %d\n", $count + 1, $cur_num_events, $prev_num_events); sleep(2); // 2 seconds - $cur_num_events = Marketplace::mine()->events->total(); - $count += 1; - } + $cur_num_events = Marketplace::mine()->events->total(); + $count += 1; + } $this->assertTrue($cur_num_events > $prev_num_events); } } diff --git a/src/aphront/AphrontController.php b/src/aphront/AphrontController.php index a591271270..352e56ee11 100644 --- a/src/aphront/AphrontController.php +++ b/src/aphront/AphrontController.php @@ -1,86 +1,83 @@ delegatingController = $delegating_controller; return $this; } public function getDelegatingController() { return $this->delegatingController; } public function willBeginExecution() { return; } public function willProcessRequest(array $uri_data) { return; } public function didProcessRequest($response) { return $response; } abstract public function processRequest(); final public function __construct(AphrontRequest $request) { $this->request = $request; } final public function getRequest() { return $this->request; } final public function delegateToController(AphrontController $controller) { $controller->setDelegatingController($this); $application = $this->getCurrentApplication(); if ($application) { $controller->setCurrentApplication($application); } return $controller->processRequest(); } final public function setCurrentApplication( PhabricatorApplication $current_application) { $this->currentApplication = $current_application; return $this; } final public function getCurrentApplication() { return $this->currentApplication; } public function getDefaultResourceSource() { throw new Exception( pht( 'A Controller must implement getDefaultResourceSource() before you '. 'can invoke requireResource() or initBehavior().')); } public function requireResource($symbol) { $response = CelerityAPI::getStaticResourceResponse(); $response->requireResource($symbol, $this->getDefaultResourceSource()); return $this; } public function initBehavior($name, $config = array()) { Javelin::initBehavior( $name, $config, $this->getDefaultResourceSource()); } } diff --git a/src/aphront/AphrontURIMapper.php b/src/aphront/AphrontURIMapper.php index 20ae87db97..9c68f2485f 100644 --- a/src/aphront/AphrontURIMapper.php +++ b/src/aphront/AphrontURIMapper.php @@ -1,52 +1,50 @@ map = $map; } final public function mapPath($path) { $map = $this->map; foreach ($map as $rule => $value) { list($controller, $data) = $this->tryRule($rule, $value, $path); if ($controller) { foreach ($data as $k => $v) { if (is_numeric($k)) { unset($data[$k]); } } return array($controller, $data); } } return array(null, null); } final private function tryRule($rule, $value, $path) { $match = null; $pattern = '#^'.$rule.(is_array($value) ? '' : '$').'#'; if (!preg_match($pattern, $path, $match)) { return array(null, null); } if (!is_array($value)) { return array($value, $match); } $path = substr($path, strlen($match[0])); foreach ($value as $srule => $sval) { list($controller, $data) = $this->tryRule($srule, $sval, $path); if ($controller) { return array($controller, $data + $match); } } return array(null, null); } + } diff --git a/src/aphront/configuration/AphrontApplicationConfiguration.php b/src/aphront/configuration/AphrontApplicationConfiguration.php index b15908c2f1..4141bb005d 100644 --- a/src/aphront/configuration/AphrontApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontApplicationConfiguration.php @@ -1,231 +1,231 @@ request = $request; return $this; } final public function getRequest() { return $this->request; } final public function getConsole() { return $this->console; } final public function setConsole($console) { $this->console = $console; return $this; } final public function setHost($host) { $this->host = $host; return $this; } final public function getHost() { return $this->host; } final public function setPath($path) { $this->path = $path; return $this; } final public function getPath() { return $this->path; } public function willBuildRequest() { } /* -( URI Routing )-------------------------------------------------------- */ /** * Using builtin and application routes, build the appropriate * @{class:AphrontController} class for the request. To route a request, we * first test if the HTTP_HOST is configured as a valid Phabricator URI. If * it isn't, we do a special check to see if it's a custom domain for a blog * in the Phame application and if that fails we error. Otherwise, we test * the URI against all builtin routes from @{method:getURIMap}, then against * all application routes from installed @{class:PhabricatorApplication}s. * * If we match a route, we construct the controller it points at, build it, * and return it. * * If we fail to match a route, but the current path is missing a trailing * "/", we try routing the same path with a trailing "/" and do a redirect * if that has a valid route. The idea is to canoncalize URIs for consistency, * but avoid breaking noncanonical URIs that we can easily salvage. * * NOTE: We only redirect on GET. On POST, we'd drop parameters and most * likely mutate the request implicitly, and a bad POST usually indicates a * programming error rather than a sloppy typist. * * If the failing path already has a trailing "/", or we can't route the * version with a "/", we call @{method:build404Controller}, which build a * fallback @{class:AphrontController}. * * @return pair Controller and dictionary of request * parameters. * @task routing */ final public function buildController() { $request = $this->getRequest(); if (PhabricatorEnv::getEnvConfig('security.require-https')) { if (!$request->isHTTPS()) { $https_uri = $request->getRequestURI(); $https_uri->setDomain($request->getHost()); $https_uri->setProtocol('https'); return $this->buildRedirectController($https_uri); } } $path = $request->getPath(); $host = $request->getHost(); $base_uri = PhabricatorEnv::getEnvConfig('phabricator.base-uri'); $prod_uri = PhabricatorEnv::getEnvConfig('phabricator.production-uri'); $file_uri = PhabricatorEnv::getEnvConfig( 'security.alternate-file-domain'); $conduit_uris = PhabricatorEnv::getEnvConfig('conduit.servers'); $allowed_uris = PhabricatorEnv::getEnvConfig('phabricator.allowed-uris'); $uris = array_merge( array( $base_uri, $prod_uri, $file_uri, ), $conduit_uris, $allowed_uris); $host_match = false; foreach ($uris as $uri) { if ($host === id(new PhutilURI($uri))->getDomain()) { $host_match = true; break; } } // NOTE: If the base URI isn't defined yet, don't activate alternate // domains. if ($base_uri && !$host_match) { try { $blog = id(new PhameBlogQuery()) ->setViewer(new PhabricatorUser()) ->withDomain($host) ->executeOne(); } catch (PhabricatorPolicyException $ex) { throw new Exception( 'This blog is not visible to logged out users, so it can not be '. 'visited from a custom domain.'); } if (!$blog) { if ($prod_uri && $prod_uri != $base_uri) { $prod_str = ' or '.$prod_uri; } else { $prod_str = ''; } throw new Exception( 'Specified domain '.$host.' is not configured for Phabricator '. 'requests. Please use '.$base_uri.$prod_str.' to visit this instance.' ); } // TODO: Make this more flexible and modular so any application can // do crazy stuff here if it wants. $path = '/phame/live/'.$blog->getID().'/'.$path; } list($controller, $uri_data) = $this->buildControllerForPath($path); if (!$controller) { if (!preg_match('@/$@', $path)) { // If we failed to match anything but don't have a trailing slash, try // to add a trailing slash and issue a redirect if that resolves. list($controller, $uri_data) = $this->buildControllerForPath($path.'/'); // NOTE: For POST, just 404 instead of redirecting, since the redirect // will be a GET without parameters. if ($controller && !$request->isHTTPPost()) { $slash_uri = $request->getRequestURI()->setPath($path.'/'); return $this->buildRedirectController($slash_uri); } } return $this->build404Controller(); } return array($controller, $uri_data); } /** * Map a specific path to the corresponding controller. For a description * of routing, see @{method:buildController}. * * @return pair Controller and dictionary of request * parameters. * @task routing */ final public function buildControllerForPath($path) { $maps = array(); $maps[] = array(null, $this->getURIMap()); $applications = PhabricatorApplication::getAllInstalledApplications(); foreach ($applications as $application) { $maps[] = array($application, $application->getRoutes()); } $current_application = null; $controller_class = null; foreach ($maps as $map_info) { list($application, $map) = $map_info; $mapper = new AphrontURIMapper($map); list($controller_class, $uri_data) = $mapper->mapPath($path); if ($controller_class) { if ($application) { $current_application = $application; } break; } } if (!$controller_class) { return array(null, null); } $request = $this->getRequest(); $controller = newv($controller_class, array($request)); if ($current_application) { $controller->setCurrentApplication($current_application); } return array($controller, $uri_data); } + } diff --git a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php index 05850cb468..895db41e5c 100644 --- a/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php +++ b/src/aphront/configuration/AphrontDefaultApplicationConfiguration.php @@ -1,316 +1,315 @@ getResourceURIMapRules() + array( '/~/' => array( '' => 'DarkConsoleController', 'data/(?P[^/]+)/' => 'DarkConsoleDataController', ), ); } protected function getResourceURIMapRules() { $extensions = CelerityResourceController::getSupportedResourceTypes(); $extensions = array_keys($extensions); $extensions = implode('|', $extensions); return array( '/res/' => array( '(?:(?P[0-9]+)T/)?'. '(?P[^/]+)/'. '(?P[a-f0-9]{8})/'. '(?P.+\.(?:'.$extensions.'))' => 'CelerityPhabricatorResourceController', ), ); } /** * @phutil-external-symbol class PhabricatorStartup */ public function buildRequest() { $parser = new PhutilQueryStringParser(); $data = array(); // If the request has "multipart/form-data" content, we can't use // PhutilQueryStringParser to parse it, and the raw data supposedly is not // available anyway (according to the PHP documentation, "php://input" is // not available for "multipart/form-data" requests). However, it is // available at least some of the time (see T3673), so double check that // we aren't trying to parse data we won't be able to parse correctly by // examining the Content-Type header. $content_type = idx($_SERVER, 'CONTENT_TYPE'); $is_form_data = preg_match('@^multipart/form-data@i', $content_type); $raw_input = PhabricatorStartup::getRawInput(); if (strlen($raw_input) && !$is_form_data) { $data += $parser->parseQueryString($raw_input); } else if ($_POST) { $data += $_POST; } $data += $parser->parseQueryString(idx($_SERVER, 'QUERY_STRING', '')); $cookie_prefix = PhabricatorEnv::getEnvConfig('phabricator.cookie-prefix'); $request = new AphrontRequest($this->getHost(), $this->getPath()); $request->setRequestData($data); $request->setApplicationConfiguration($this); $request->setCookiePrefix($cookie_prefix); return $request; } public function handleException(Exception $ex) { $request = $this->getRequest(); // For Conduit requests, return a Conduit response. if ($request->isConduit()) { $response = new ConduitAPIResponse(); $response->setErrorCode(get_class($ex)); $response->setErrorInfo($ex->getMessage()); return id(new AphrontJSONResponse()) ->setAddJSONShield(false) ->setContent($response->toDictionary()); } // For non-workflow requests, return a Ajax response. if ($request->isAjax() && !$request->isJavelinWorkflow()) { // Log these; they don't get shown on the client and can be difficult // to debug. phlog($ex); $response = new AphrontAjaxResponse(); $response->setError( array( 'code' => get_class($ex), 'info' => $ex->getMessage(), )); return $response; } $user = $request->getUser(); if (!$user) { // If we hit an exception very early, we won't have a user. $user = new PhabricatorUser(); } if ($ex instanceof PhabricatorSystemActionRateLimitException) { $dialog = id(new AphrontDialogView()) ->setTitle(pht('Slow Down!')) ->setUser($user) ->setErrors(array(pht('You are being rate limited.'))) ->appendParagraph($ex->getMessage()) ->appendParagraph($ex->getRateExplanation()) ->addCancelButton('/', pht('Okaaaaaaaaaaaaaay...')); $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof PhabricatorAuthHighSecurityRequiredException) { $form = id(new PhabricatorAuthSessionEngine())->renderHighSecurityForm( $ex->getFactors(), $ex->getFactorValidationResults(), $user, $request); $dialog = id(new AphrontDialogView()) ->setUser($user) ->setTitle(pht('Entering High Security')) ->setShortTitle(pht('Security Checkpoint')) ->setWidth(AphrontDialogView::WIDTH_FORM) ->addHiddenInput(AphrontRequest::TYPE_HISEC, true) ->setErrors( array( pht( 'You are taking an action which requires you to enter '. 'high security.'), )) ->appendParagraph( pht( 'High security mode helps protect your account from security '. 'threats, like session theft or someone messing with your stuff '. 'while you\'re grabbing a coffee. To enter high security mode, '. 'confirm your credentials.')) ->appendChild($form->buildLayoutView()) ->appendParagraph( pht( 'Your account will remain in high security mode for a short '. 'period of time. When you are finished taking sensitive '. 'actions, you should leave high security.')) ->setSubmitURI($request->getPath()) ->addCancelButton($ex->getCancelURI()) ->addSubmitButton(pht('Enter High Security')); foreach ($request->getPassthroughRequestParameters() as $key => $value) { $dialog->addHiddenInput($key, $value); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof PhabricatorPolicyException) { if (!$user->isLoggedIn()) { // If the user isn't logged in, just give them a login form. This is // probably a generally more useful response than a policy dialog that // they have to click through to get a login form. // // Possibly we should add a header here like "you need to login to see // the thing you are trying to look at". $login_controller = new PhabricatorAuthStartController($request); $auth_app_class = 'PhabricatorApplicationAuth'; $auth_app = PhabricatorApplication::getByClass($auth_app_class); $login_controller->setCurrentApplication($auth_app); return $login_controller->processRequest(); } $list = $ex->getMoreInfo(); foreach ($list as $key => $item) { $list[$key] = phutil_tag('li', array(), $item); } if ($list) { $list = phutil_tag('ul', array(), $list); } $content = array( phutil_tag( 'div', array( 'class' => 'aphront-policy-rejection', ), $ex->getRejection()), phutil_tag( 'div', array( 'class' => 'aphront-capability-details', ), pht('Users with the "%s" capability:', $ex->getCapabilityName())), $list, ); $dialog = new AphrontDialogView(); $dialog ->setTitle($ex->getTitle()) ->setClass('aphront-access-dialog') ->setUser($user) ->appendChild($content); if ($this->getRequest()->isAjax()) { $dialog->addCancelButton('/', pht('Close')); } else { $dialog->addCancelButton('/', pht('OK')); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); return $response; } if ($ex instanceof AphrontUsageException) { $error = new AphrontErrorView(); $error->setTitle($ex->getTitle()); $error->appendChild($ex->getMessage()); $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->appendChild($error); $response = new AphrontWebpageResponse(); $response->setContent($view->render()); $response->setHTTPResponseCode(500); return $response; } // Always log the unhandled exception. phlog($ex); $class = get_class($ex); $message = $ex->getMessage(); if ($ex instanceof AphrontQuerySchemaException) { $message .= "\n\n". "NOTE: This usually indicates that the MySQL schema has not been ". "properly upgraded. Run 'bin/storage upgrade' to ensure your ". "schema is up to date."; } if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { $trace = id(new AphrontStackTraceView()) ->setUser($user) ->setTrace($ex->getTrace()); } else { $trace = null; } $content = phutil_tag( 'div', array('class' => 'aphront-unhandled-exception'), array( phutil_tag('div', array('class' => 'exception-message'), $message), $trace, )); $dialog = new AphrontDialogView(); $dialog ->setTitle('Unhandled Exception ("'.$class.'")') ->setClass('aphront-exception-dialog') ->setUser($user) ->appendChild($content); if ($this->getRequest()->isAjax()) { $dialog->addCancelButton('/', 'Close'); } $response = new AphrontDialogResponse(); $response->setDialog($dialog); $response->setHTTPResponseCode(500); return $response; } public function willSendResponse(AphrontResponse $response) { return $response; } public function build404Controller() { return array(new Phabricator404Controller($this->getRequest()), array()); } public function buildRedirectController($uri) { return array( new PhabricatorRedirectController($this->getRequest()), array( 'uri' => $uri, )); } } diff --git a/src/aphront/console/DarkConsoleController.php b/src/aphront/console/DarkConsoleController.php index eef85de76a..3849ee0cb5 100644 --- a/src/aphront/console/DarkConsoleController.php +++ b/src/aphront/console/DarkConsoleController.php @@ -1,49 +1,46 @@ getRequest(); $user = $request->getUser(); $response = id(new AphrontAjaxResponse())->setDisableConsole(true); if (!$user->isLoggedIn()) { return $response; } $visible = $request->getStr('visible'); if (strlen($visible)) { $user->setConsoleVisible((int)$visible); $user->save(); return $response; } $tab = $request->getStr('tab'); if (strlen($tab)) { $user->setConsoleTab($tab); $user->save(); return $response; } return new Aphront404Response(); } } diff --git a/src/aphront/console/DarkConsoleCore.php b/src/aphront/console/DarkConsoleCore.php index 3389109ebf..321b681d50 100644 --- a/src/aphront/console/DarkConsoleCore.php +++ b/src/aphront/console/DarkConsoleCore.php @@ -1,137 +1,134 @@ setType('class') ->setAncestorClass('DarkConsolePlugin') ->selectAndLoadSymbols(); foreach ($symbols as $symbol) { $plugin = newv($symbol['name'], array()); if (!$plugin->shouldStartup()) { continue; } $plugin->setConsoleCore($this); $plugin->didStartup(); $this->plugins[$symbol['name']] = $plugin; } } public function getPlugins() { return $this->plugins; } public function getKey(AphrontRequest $request) { $plugins = $this->getPlugins(); foreach ($plugins as $plugin) { $plugin->setRequest($request); $plugin->willShutdown(); } foreach ($plugins as $plugin) { $plugin->didShutdown(); } foreach ($plugins as $plugin) { $plugin->setData($plugin->generateData()); } $plugins = msort($plugins, 'getOrderKey'); $key = Filesystem::readRandomCharacters(24); $tabs = array(); $data = array(); foreach ($plugins as $plugin) { $class = get_class($plugin); $tabs[] = array( 'class' => $class, 'name' => $plugin->getName(), 'color' => $plugin->getColor(), ); $data[$class] = $this->sanitizeForJSON($plugin->getData()); } $storage = array( 'vers' => self::STORAGE_VERSION, 'tabs' => $tabs, 'data' => $data, 'user' => $request->getUser() ? $request->getUser()->getPHID() : null, ); $cache = new PhabricatorKeyValueDatabaseCache(); $cache = new PhutilKeyValueCacheProfiler($cache); $cache->setProfiler(PhutilServiceProfiler::getInstance()); // This encoding may fail if there are, e.g., database queries which // include binary data. It would be a little cleaner to try to strip these, // but just do something non-broken here if we end up with unrepresentable // data. $json = @json_encode($storage); if (!$json) { $json = '{}'; } $cache->setKeys( array( 'darkconsole:'.$key => $json, ), $ttl = (60 * 60 * 6)); return $key; } public function getColor() { foreach ($this->getPlugins() as $plugin) { if ($plugin->getColor()) { return $plugin->getColor(); } } } public function render(AphrontRequest $request) { $user = $request->getUser(); $visible = $user ? $user->getConsoleVisible() : true; return javelin_tag( 'div', array( 'id' => 'darkconsole', 'class' => 'dark-console', 'style' => $visible ? '' : 'display: none;', 'data-console-key' => $this->getKey($request), 'data-console-color' => $this->getColor(), ), ''); } /** * Sometimes, tab data includes binary information (like INSERT queries which * write file data into the database). To successfully JSON encode it, we * need to convert it to UTF-8. */ private function sanitizeForJSON($data) { if (is_object($data)) { return ''; } else if (is_array($data)) { foreach ($data as $key => $value) { $data[$key] = $this->sanitizeForJSON($value); } return $data; } else { return phutil_utf8ize($data); } } } diff --git a/src/aphront/console/DarkConsoleDataController.php b/src/aphront/console/DarkConsoleDataController.php index b1df699407..fd5350679d 100644 --- a/src/aphront/console/DarkConsoleDataController.php +++ b/src/aphront/console/DarkConsoleDataController.php @@ -1,87 +1,84 @@ key = $data['key']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $cache = new PhabricatorKeyValueDatabaseCache(); $cache = new PhutilKeyValueCacheProfiler($cache); $cache->setProfiler(PhutilServiceProfiler::getInstance()); $result = $cache->getKey('darkconsole:'.$this->key); if (!$result) { return new Aphront400Response(); } $result = json_decode($result, true); if (!is_array($result)) { return new Aphront400Response(); } if ($result['vers'] != DarkConsoleCore::STORAGE_VERSION) { return new Aphront400Response(); } if ($result['user'] != $user->getPHID()) { return new Aphront400Response(); } $output = array(); $output['tabs'] = $result['tabs']; $output['panel'] = array(); foreach ($result['data'] as $class => $data) { try { $obj = newv($class, array()); $obj->setData($data); $obj->setRequest($request); $panel = $obj->renderPanel(); // Because cookie names can now be prefixed, wipe out any cookie value // with the session cookie name anywhere in its name. $pattern = '('.preg_quote(PhabricatorCookies::COOKIE_SESSION).')'; foreach ($_COOKIE as $cookie_name => $cookie_value) { if (preg_match($pattern, $cookie_name)) { $panel = PhutilSafeHTML::applyFunction( 'str_replace', $cookie_value, '(session-key)', $panel); } } $output['panel'][$class] = $panel; } catch (Exception $ex) { $output['panel'][$class] = 'error'; } } return id(new AphrontAjaxResponse())->setContent($output); } } diff --git a/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php b/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php index cf2d2841f2..513f4b9905 100644 --- a/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleErrorLogPlugin.php @@ -1,101 +1,99 @@ getData()); if ($count) { return pht('Error Log (%d)', $count); } return pht('Error Log'); } public function getOrder() { return 0; } public function getColor() { if (count($this->getData())) { return '#ff0000'; } return null; } public function getDescription() { return pht('Shows errors and warnings.'); } public function generateData() { return DarkConsoleErrorLogPluginAPI::getErrors(); } public function renderPanel() { $data = $this->getData(); $rows = array(); $details = array(); foreach ($data as $index => $row) { $file = $row['file']; $line = $row['line']; $tag = phutil_tag( 'a', array( 'onclick' => jsprintf('show_details(%d)', $index), ), $row['str'].' at ['.basename($file).':'.$line.']'); $rows[] = array($tag); $details[] = hsprintf( '
    '. "%s\nStack trace:\n", $index, $row['details']); foreach ($row['trace'] as $key => $entry) { $line = ''; if (isset($entry['class'])) { $line .= $entry['class'].'::'; } $line .= idx($entry, 'function', ''); $href = null; if (isset($entry['file'])) { $line .= ' called at ['.$entry['file'].':'.$entry['line'].']'; try { $user = $this->getRequest()->getUser(); $href = $user->loadEditorLink($entry['file'], $entry['line'], ''); } catch (Exception $ex) { // The database can be inaccessible. } } $details[] = phutil_tag( 'a', array( 'href' => $href, ), $line); $details[] = "\n"; } $details[] = hsprintf('
    '); } $table = new AphrontTableView($rows); $table->setClassName('error-log'); $table->setHeaders(array('Error')); $table->setNoDataString('No errors.'); return phutil_tag( 'div', array(), array( phutil_tag('div', array(), $table->render()), phutil_tag('pre', array('class' => 'PhabricatorMonospaced'), $details), )); } + } diff --git a/src/aphront/console/plugin/DarkConsoleEventPlugin.php b/src/aphront/console/plugin/DarkConsoleEventPlugin.php index bc191e00ea..02dc4e8b78 100644 --- a/src/aphront/console/plugin/DarkConsoleEventPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleEventPlugin.php @@ -1,98 +1,95 @@ getAllListeners(); foreach ($listeners as $key => $listener) { $listeners[$key] = array( 'id' => $listener->getListenerID(), 'class' => get_class($listener), ); } $events = DarkConsoleEventPluginAPI::getEvents(); foreach ($events as $key => $event) { $events[$key] = array( 'type' => $event->getType(), 'stopped' => $event->isStopped(), ); } return array( 'listeners' => $listeners, 'events' => $events, ); } public function renderPanel() { $data = $this->getData(); $out = array(); $out[] = phutil_tag( 'div', array('class' => 'dark-console-panel-header'), phutil_tag('h1', array(), pht('Registered Event Listeners'))); $rows = array(); foreach ($data['listeners'] as $listener) { $rows[] = array($listener['id'], $listener['class']); } $table = new AphrontTableView($rows); $table->setHeaders( array( 'Internal ID', 'Listener Class', )); $table->setColumnClasses( array( '', 'wide', )); $out[] = $table->render(); $out[] = phutil_tag( 'div', array('class' => 'dark-console-panel-header'), phutil_tag('h1', array(), pht('Event Log'))); $rows = array(); foreach ($data['events'] as $event) { $rows[] = array( $event['type'], $event['stopped'] ? 'STOPPED' : null, ); } $table = new AphrontTableView($rows); $table->setColumnClasses( array( 'wide', )); $table->setHeaders( array( 'Event Type', 'Stopped', )); $out[] = $table->render(); return phutil_implode_html("\n", $out); } + } diff --git a/src/aphront/console/plugin/DarkConsolePlugin.php b/src/aphront/console/plugin/DarkConsolePlugin.php index 20a93199c0..be1eeb846c 100644 --- a/src/aphront/console/plugin/DarkConsolePlugin.php +++ b/src/aphront/console/plugin/DarkConsolePlugin.php @@ -1,90 +1,85 @@ getOrder()), $this->getName()); } public function getOrder() { return 1.0; } public function setConsoleCore(DarkConsoleCore $core) { $this->core = $core; return $this; } public function getConsoleCore() { return $this->core; } public function generateData() { return null; } public function setData($data) { $this->data = $data; return $this; } public function getData() { return $this->data; } public function setRequest($request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } public function getRequestURI() { return $this->getRequest()->getRequestURI(); } public function shouldStartup() { return true; } public function didStartup() { return null; } public function willShutdown() { return null; } public function didShutdown() { return null; } public function processRequest() { return null; } } diff --git a/src/aphront/console/plugin/DarkConsoleRequestPlugin.php b/src/aphront/console/plugin/DarkConsoleRequestPlugin.php index 5d43f93745..d6d9687883 100644 --- a/src/aphront/console/plugin/DarkConsoleRequestPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleRequestPlugin.php @@ -1,78 +1,75 @@ $_REQUEST, 'Server' => $_SERVER, ); } public function renderPanel() { $data = $this->getData(); $sections = array( 'Basics' => array( 'Machine' => php_uname('n'), ), ); // NOTE: This may not be present for some SAPIs, like php-fpm. if (!empty($data['Server']['SERVER_ADDR'])) { $addr = $data['Server']['SERVER_ADDR']; $sections['Basics']['Host'] = $addr; $sections['Basics']['Hostname'] = @gethostbyaddr($addr); } $sections = array_merge($sections, $data); $mask = array( 'HTTP_COOKIE' => true, 'HTTP_X_PHABRICATOR_CSRF' => true, ); $out = array(); foreach ($sections as $header => $map) { $rows = array(); foreach ($map as $key => $value) { if (isset($mask[$key])) { $rows[] = array( $key, phutil_tag('em', array(), '(Masked)')); } else { $rows[] = array( $key, (is_array($value) ? json_encode($value) : $value), ); } } $table = new AphrontTableView($rows); $table->setHeaders( array( $header, null, )); $table->setColumnClasses( array( 'header', 'wide wrap', )); $out[] = $table->render(); } return phutil_implode_html("\n", $out); } } diff --git a/src/aphront/console/plugin/DarkConsoleServicesPlugin.php b/src/aphront/console/plugin/DarkConsoleServicesPlugin.php index e0f760d271..c085815648 100644 --- a/src/aphront/console/plugin/DarkConsoleServicesPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleServicesPlugin.php @@ -1,295 +1,291 @@ getServiceCallLog(); foreach ($log as $key => $entry) { $config = idx($entry, 'config', array()); unset($log[$key]['config']); if (!$should_analyze) { $log[$key]['explain'] = array( 'sev' => 7, 'size' => null, 'reason' => 'Disabled', ); // Query analysis is disabled for this request, so don't do any of it. continue; } if ($entry['type'] != 'query') { continue; } // For each SELECT query, go issue an EXPLAIN on it so we can flag stuff // causing table scans, etc. if (preg_match('/^\s*SELECT\b/i', $entry['query'])) { $conn = PhabricatorEnv::newObjectFromConfig( 'mysql.implementation', array($entry['config'])); try { $explain = queryfx_all( $conn, 'EXPLAIN %Q', $entry['query']); $badness = 0; $size = 1; $reason = null; foreach ($explain as $table) { $size *= (int)$table['rows']; switch ($table['type']) { case 'index': $cur_badness = 1; $cur_reason = 'Index'; break; case 'const': $cur_badness = 1; $cur_reason = 'Const'; break; case 'eq_ref'; $cur_badness = 2; $cur_reason = 'EqRef'; break; case 'range': $cur_badness = 3; $cur_reason = 'Range'; break; case 'ref': $cur_badness = 3; $cur_reason = 'Ref'; break; case 'fulltext': $cur_badness = 3; $cur_reason = 'Fulltext'; break; case 'ALL': if (preg_match('/Using where/', $table['Extra'])) { if ($table['rows'] < 256 && !empty($table['possible_keys'])) { $cur_badness = 2; $cur_reason = 'Small Table Scan'; } else { $cur_badness = 6; $cur_reason = 'TABLE SCAN!'; } } else { $cur_badness = 3; $cur_reason = 'Whole Table'; } break; default: if (preg_match('/No tables used/i', $table['Extra'])) { $cur_badness = 1; $cur_reason = 'No Tables'; } else if (preg_match('/Impossible/i', $table['Extra'])) { $cur_badness = 1; $cur_reason = 'Empty'; } else { $cur_badness = 4; $cur_reason = "Can't Analyze"; } break; } if ($cur_badness > $badness) { $badness = $cur_badness; $reason = $cur_reason; } } $log[$key]['explain'] = array( 'sev' => $badness, 'size' => $size, 'reason' => $reason, ); } catch (Exception $ex) { $log[$key]['explain'] = array( 'sev' => 5, 'size' => null, 'reason' => $ex->getMessage(), ); } } } return array( 'start' => PhabricatorStartup::getStartTime(), 'end' => microtime(true), 'log' => $log, 'analyzeURI' => (string)$this ->getRequestURI() ->alter('__analyze__', true), 'didAnalyze' => $should_analyze, ); } public function renderPanel() { $data = $this->getData(); $log = $data['log']; $results = array(); $results[] = phutil_tag( 'div', array('class' => 'dark-console-panel-header'), array( phutil_tag( 'a', array( 'href' => $data['analyzeURI'], 'class' => $data['didAnalyze'] ? 'disabled button' : 'green button', ), pht('Analyze Query Plans')), phutil_tag('h1', array(), pht('Calls to External Services')), phutil_tag('div', array('style' => 'clear: both;')), )); $page_total = $data['end'] - $data['start']; $totals = array(); $counts = array(); foreach ($log as $row) { $totals[$row['type']] = idx($totals, $row['type'], 0) + $row['duration']; $counts[$row['type']] = idx($counts, $row['type'], 0) + 1; } $totals['All Services'] = array_sum($totals); $counts['All Services'] = array_sum($counts); $totals['Entire Page'] = $page_total; $counts['Entire Page'] = 0; $summary = array(); foreach ($totals as $type => $total) { $summary[] = array( $type, number_format($counts[$type]), number_format((int)(1000000 * $totals[$type])).' us', sprintf('%.1f%%', 100 * $totals[$type] / $page_total), ); } $summary_table = new AphrontTableView($summary); $summary_table->setColumnClasses( array( '', 'n', 'n', 'wide', )); $summary_table->setHeaders( array( 'Type', 'Count', 'Total Cost', 'Page Weight', )); $results[] = $summary_table->render(); $rows = array(); foreach ($log as $row) { $analysis = null; switch ($row['type']) { case 'query': $info = $row['query']; $info = wordwrap($info, 128, "\n", true); if (!empty($row['explain'])) { $analysis = phutil_tag( 'span', array( 'class' => 'explain-sev-'.$row['explain']['sev'], ), $row['explain']['reason']); } break; case 'connect': $info = $row['host'].':'.$row['database']; break; case 'exec': $info = $row['command']; break; case 'conduit': $info = $row['method']; break; case 'http': $info = $row['uri']; break; default: $info = '-'; break; } $rows[] = array( $row['type'], '+'.number_format(1000 * ($row['begin'] - $data['start'])).' ms', number_format(1000000 * $row['duration']).' us', $info, $analysis, ); } $table = new AphrontTableView($rows); $table->setColumnClasses( array( null, 'n', 'n', 'wide', '', )); $table->setHeaders( array( 'Event', 'Start', 'Duration', 'Details', 'Analysis', )); $results[] = $table->render(); return phutil_implode_html("\n", $results); } } diff --git a/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php b/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php index f56ad00b9a..2a0d22f775 100644 --- a/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php +++ b/src/aphront/console/plugin/DarkConsoleXHProfPlugin.php @@ -1,110 +1,107 @@ getData(); if ($data['profileFilePHID']) { return '#ff00ff'; } return null; } public function getDescription() { return 'Provides detailed PHP profiling information through XHProf.'; } public function generateData() { return array( 'profileFilePHID' => $this->profileFilePHID, 'profileURI' => (string)$this ->getRequestURI() ->alter('__profile__', 'page'), ); } public function getXHProfRunID() { return $this->profileFilePHID; } public function renderPanel() { $data = $this->getData(); $run = $data['profileFilePHID']; $profile_uri = $data['profileURI']; if (!DarkConsoleXHProfPluginAPI::isProfilerAvailable()) { $href = PhabricatorEnv::getDoclink('Installation Guide'); $install_guide = phutil_tag( 'a', array( 'href' => $href, 'class' => 'bright-link', ), 'Installation Guide'); return hsprintf( '
    '. 'The "xhprof" PHP extension is not available. Install xhprof '. 'to enable the XHProf console plugin. You can find instructions in '. 'the %s.'. '
    ', $install_guide); } $result = array(); $header = phutil_tag( 'div', array('class' => 'dark-console-panel-header'), array( phutil_tag( 'a', array( 'href' => $profile_uri, 'class' => $run ? 'disabled button' : 'green button', ), pht('Profile Page')), phutil_tag('h1', array(), pht('XHProf Profiler')), )); $result[] = $header; if ($run) { $result[] = phutil_tag( 'a', array( 'href' => "/xhprof/profile/$run/", 'class' => 'bright-link', 'style' => 'float: right; margin: 1em 2em 0 0; font-weight: bold;', 'target' => '_blank', ), pht('Profile Permalink')); $result[] = phutil_tag( 'iframe', array('src' => "/xhprof/profile/$run/?frame=true")); } else { $result[] = phutil_tag( 'div', array('class' => 'dark-console-no-content'), pht( 'Profiling was not enabled for this page. Use the button above '. 'to enable it.')); } return phutil_implode_html("\n", $result); } public function willShutdown() { $this->profileFilePHID = DarkConsoleXHProfPluginAPI::getProfileFilePHID(); } } diff --git a/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php b/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php index c65a4a27b8..0708c98eec 100644 --- a/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php +++ b/src/aphront/console/plugin/errorlog/DarkConsoleErrorLogPluginAPI.php @@ -1,78 +1,75 @@ $value->getMessage(), 'event' => $event, 'file' => $value->getFile(), 'line' => $value->getLine(), 'str' => $value->getMessage(), 'trace' => $metadata['trace'], ); break; case PhutilErrorHandler::ERROR: // $value is a simple string self::$errors[] = array( 'details' => $value, 'event' => $event, 'file' => $metadata['file'], 'line' => $metadata['line'], 'str' => $value, 'trace' => $metadata['trace'], ); break; case PhutilErrorHandler::PHLOG: // $value can be anything self::$errors[] = array( 'details' => PhutilReadableSerializer::printShallow($value, 3), 'event' => $event, 'file' => $metadata['file'], 'line' => $metadata['line'], 'str' => PhutilReadableSerializer::printShort($value), 'trace' => $metadata['trace'], ); break; default: error_log('Unknown event : '.$event); break; } } } diff --git a/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php b/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php index afac58ceb4..08db894e4e 100644 --- a/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php +++ b/src/aphront/console/plugin/event/DarkConsoleEventPluginAPI.php @@ -1,30 +1,27 @@ listen(PhabricatorEventType::TYPE_ALL); } public function handleEvent(PhutilEvent $event) { if (self::$discardMode) { return; } self::$events[] = $event; } } diff --git a/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php b/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php index d14f6b535e..a89c5a37b8 100644 --- a/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php +++ b/src/aphront/console/plugin/xhprof/DarkConsoleXHProfPluginAPI.php @@ -1,187 +1,186 @@ setFilePHID($file_phid) ->setSampleRate($sample_rate) ->setUsTotal($access_log->getData('T')) ->setHostname($access_log->getData('h')) ->setRequestPath($access_log->getData('U')) ->setController($access_log->getData('C')) ->setUserPHID($access_log->getData('P')); AphrontWriteGuard::allowDangerousUnguardedWrites(true); $caught = null; try { $profile_sample->save(); } catch (Exception $ex) { $caught = $ex; } AphrontWriteGuard::allowDangerousUnguardedWrites(false); if ($caught) { throw $caught; } } public static function hookProfiler() { if (!self::shouldStartProfiler()) { return; } if (!self::isProfilerAvailable()) { return; } if (self::$profilerStarted) { return; } self::startProfiler(); } private static function startProfiler() { self::includeXHProfLib(); xhprof_enable(); self::$profilerStarted = true; self::$profilerRunning = true; } public static function getProfileFilePHID() { self::stopProfiler(); return self::$profileFilePHID; } private static function stopProfiler() { if (!self::isProfilerRunning()) { return; } $data = xhprof_disable(); $data = @json_encode($data); self::$profilerRunning = false; // Since these happen on GET we can't do guarded writes. These also // sometimes happen after we've disposed of the write guard; in this // case we need to disable the whole mechanism. $use_scope = AphrontWriteGuard::isGuardActive(); if ($use_scope) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); } else { AphrontWriteGuard::allowDangerousUnguardedWrites(true); } $caught = null; try { $file = call_user_func( array('PhabricatorFile', 'newFromFileData'), $data, array( 'mime-type' => 'application/xhprof', 'name' => 'profile.xhprof', )); } catch (Exception $ex) { $caught = $ex; } if ($use_scope) { unset($unguarded); } else { AphrontWriteGuard::allowDangerousUnguardedWrites(false); } if ($caught) { throw $caught; } self::$profileFilePHID = $file->getPHID(); } } diff --git a/src/aphront/response/Aphront304Response.php b/src/aphront/response/Aphront304Response.php index 4a1d93c773..bdf1e4f980 100644 --- a/src/aphront/response/Aphront304Response.php +++ b/src/aphront/response/Aphront304Response.php @@ -1,19 +1,16 @@ forbiddenText = $text; return $this; } private function getForbiddenText() { return $this->forbiddenText; } public function getHTTPResponseCode() { return 403; } public function buildResponseString() { $forbidden_text = $this->getForbiddenText(); if (!$forbidden_text) { $forbidden_text = 'You do not have privileges to access the requested page.'; } $failure = new AphrontRequestFailureView(); $failure->setHeader('403 Forbidden'); $failure->appendChild(phutil_tag('p', array(), $forbidden_text)); $view = new PhabricatorStandardPageView(); $view->setTitle('403 Forbidden'); $view->setRequest($this->getRequest()); $view->appendChild($failure); return $view->render(); } } diff --git a/src/aphront/response/Aphront404Response.php b/src/aphront/response/Aphront404Response.php index fbfa41a0da..f24ed524a5 100644 --- a/src/aphront/response/Aphront404Response.php +++ b/src/aphront/response/Aphront404Response.php @@ -1,26 +1,23 @@ setHeader('404 Not Found'); $failure->appendChild(phutil_tag('p', array(), pht( 'The page you requested was not found.'))); $view = new PhabricatorStandardPageView(); $view->setTitle('404 Not Found'); $view->setRequest($this->getRequest()); $view->appendChild($failure); return $view->render(); } } diff --git a/src/aphront/response/AphrontAjaxResponse.php b/src/aphront/response/AphrontAjaxResponse.php index 032f11e120..c37fcb3de9 100644 --- a/src/aphront/response/AphrontAjaxResponse.php +++ b/src/aphront/response/AphrontAjaxResponse.php @@ -1,80 +1,77 @@ content = $content; return $this; } public function setError($error) { $this->error = $error; return $this; } public function setDisableConsole($disable) { $this->disableConsole = $disable; return $this; } private function getConsole() { if ($this->disableConsole) { $console = null; } else { $request = $this->getRequest(); $console = $request->getApplicationConfiguration()->getConsole(); } return $console; } public function buildResponseString() { $console = $this->getConsole(); if ($console) { // NOTE: We're stripping query parameters here both for readability and // to mitigate BREACH and similar attacks. The parameters are available // in the "Request" tab, so this should not impact usability. See T3684. $uri = $this->getRequest()->getRequestURI(); $uri = new PhutilURI($uri); $uri->setQueryParams(array()); Javelin::initBehavior( 'dark-console', array( 'uri' => (string)$uri, 'key' => $console->getKey($this->getRequest()), 'color' => $console->getColor(), )); } // Flatten the response first, so we initialize any behaviors and metadata // we need to. $content = array( 'payload' => $this->content, ); $this->encodeJSONForHTTPResponse($content); $response = CelerityAPI::getStaticResourceResponse(); $object = $response->buildAjaxResponse( $content['payload'], $this->error); $response_json = $this->encodeJSONForHTTPResponse($object); return $this->addJSONShield($response_json); } public function getHeaders() { $headers = array( array('Content-Type', 'text/plain; charset=UTF-8'), ); $headers = array_merge(parent::getHeaders(), $headers); return $headers; } } diff --git a/src/aphront/response/AphrontDialogResponse.php b/src/aphront/response/AphrontDialogResponse.php index 7aa34db8ae..e2db051077 100644 --- a/src/aphront/response/AphrontDialogResponse.php +++ b/src/aphront/response/AphrontDialogResponse.php @@ -1,23 +1,20 @@ dialog = $dialog; return $this; } public function getDialog() { return $this->dialog; } public function buildResponseString() { return $this->dialog->render(); } } diff --git a/src/aphront/response/AphrontFileResponse.php b/src/aphront/response/AphrontFileResponse.php index 13d980d06e..fae9f8af17 100644 --- a/src/aphront/response/AphrontFileResponse.php +++ b/src/aphront/response/AphrontFileResponse.php @@ -1,95 +1,92 @@ allowOrigins[] = $origin; return $this; } public function setDownload($download) { $download = preg_replace('/[^A-Za-z0-9_.-]/', '_', $download); if (!strlen($download)) { $download = 'untitled_document.txt'; } $this->download = $download; return $this; } public function getDownload() { return $this->download; } public function setMimeType($mime_type) { $this->mimeType = $mime_type; return $this; } public function getMimeType() { return $this->mimeType; } public function setContent($content) { $this->content = $content; return $this; } public function buildResponseString() { if ($this->rangeMin || $this->rangeMax) { $length = ($this->rangeMax - $this->rangeMin) + 1; return substr($this->content, $this->rangeMin, $length); } else { return $this->content; } } public function setRange($min, $max) { $this->rangeMin = $min; $this->rangeMax = $max; return $this; } public function getHeaders() { $headers = array( array('Content-Type', $this->getMimeType()), array('Content-Length', strlen($this->buildResponseString())), ); if ($this->rangeMin || $this->rangeMax) { $len = strlen($this->content); $min = $this->rangeMin; $max = $this->rangeMax; $headers[] = array('Content-Range', "bytes {$min}-{$max}/{$len}"); } if (strlen($this->getDownload())) { $headers[] = array('X-Download-Options', 'noopen'); $filename = $this->getDownload(); $headers[] = array( 'Content-Disposition', 'attachment; filename='.$filename, ); } if ($this->allowOrigins) { $headers[] = array( 'Access-Control-Allow-Origin', implode(',', $this->allowOrigins)); } $headers = array_merge(parent::getHeaders(), $headers); return $headers; } } diff --git a/src/aphront/response/AphrontHTMLResponse.php b/src/aphront/response/AphrontHTMLResponse.php index 4448fcdf12..ae3264d287 100644 --- a/src/aphront/response/AphrontHTMLResponse.php +++ b/src/aphront/response/AphrontHTMLResponse.php @@ -1,16 +1,13 @@ content = $content; return $this; } public function setAddJSONShield($should_add) { $this->addJSONShield = $should_add; return $this; } public function shouldAddJSONShield() { if ($this->addJSONShield === null) { return true; } return (bool) $this->addJSONShield; } public function buildResponseString() { $response = $this->encodeJSONForHTTPResponse($this->content); if ($this->shouldAddJSONShield()) { $response = $this->addJSONShield($response); } return $response; } public function getHeaders() { $headers = array( array('Content-Type', 'application/json'), ); $headers = array_merge(parent::getHeaders(), $headers); return $headers; } + } diff --git a/src/aphront/response/AphrontPlainTextResponse.php b/src/aphront/response/AphrontPlainTextResponse.php index 20ae3b750e..a14b7367e9 100644 --- a/src/aphront/response/AphrontPlainTextResponse.php +++ b/src/aphront/response/AphrontPlainTextResponse.php @@ -1,25 +1,22 @@ content = $content; return $this; } public function buildResponseString() { return $this->content; } public function getHeaders() { $headers = array( array('Content-Type', 'text/plain; charset=utf-8'), ); return array_merge(parent::getHeaders(), $headers); } } diff --git a/src/aphront/response/AphrontProxyResponse.php b/src/aphront/response/AphrontProxyResponse.php index 36f2830e29..7e27ea2b1b 100644 --- a/src/aphront/response/AphrontProxyResponse.php +++ b/src/aphront/response/AphrontProxyResponse.php @@ -1,74 +1,71 @@ proxy) { $this->proxy = $this->buildProxy(); } return $this->proxy; } public function setRequest($request) { $this->getProxy()->setRequest($request); return $this; } public function getRequest() { return $this->getProxy()->getRequest(); } public function getHeaders() { return $this->getProxy()->getHeaders(); } public function setCacheDurationInSeconds($duration) { $this->getProxy()->setCacheDurationInSeconds($duration); return $this; } public function setLastModified($epoch_timestamp) { $this->getProxy()->setLastModified($epoch_timestamp); return $this; } public function setHTTPResponseCode($code) { $this->getProxy()->setHTTPResponseCode($code); return $this; } public function getHTTPResponseCode() { return $this->getProxy()->getHTTPResponseCode(); } public function setFrameable($frameable) { $this->getProxy()->setFrameable($frameable); return $this; } public function getCacheHeaders() { return $this->getProxy()->getCacheHeaders(); } abstract protected function buildProxy(); abstract public function reduceProxyResponse(); final public function buildResponseString() { throw new Exception( 'AphrontProxyResponse must implement reduceProxyResponse().'); } - } diff --git a/src/aphront/response/AphrontRedirectResponse.php b/src/aphront/response/AphrontRedirectResponse.php index d94bce6068..3ab88cace9 100644 --- a/src/aphront/response/AphrontRedirectResponse.php +++ b/src/aphront/response/AphrontRedirectResponse.php @@ -1,90 +1,88 @@ shouldStopForDebugging()) { // If we're going to stop, capture the stack so we can print it out. $this->stackWhenCreated = id(new Exception())->getTrace(); } } public function setURI($uri) { $this->uri = $uri; return $this; } public function getURI() { return (string)$this->uri; } public function shouldStopForDebugging() { return PhabricatorEnv::getEnvConfig('debug.stop-on-redirect'); } public function getHeaders() { $headers = array(); if (!$this->shouldStopForDebugging()) { $headers[] = array('Location', $this->uri); } $headers = array_merge(parent::getHeaders(), $headers); return $headers; } public function buildResponseString() { if ($this->shouldStopForDebugging()) { $request = $this->getRequest(); $viewer = $request->getUser(); $view = new PhabricatorStandardPageView(); $view->setRequest($this->getRequest()); $view->setApplicationName(pht('Debug')); $view->setTitle(pht('Stopped on Redirect')); $dialog = new AphrontDialogView(); $dialog->setUser($viewer); $dialog->setTitle(pht('Stopped on Redirect')); $dialog->appendParagraph( pht( 'You were stopped here because %s is set in your configuration.', phutil_tag('tt', array(), 'debug.stop-on-redirect'))); $dialog->appendParagraph( pht( 'You are being redirected to: %s', phutil_tag('tt', array(), $this->getURI()))); $dialog->addCancelButton($this->getURI(), pht('Continue')); $dialog->appendChild(phutil_tag('br')); $dialog->appendChild( id(new AphrontStackTraceView()) ->setUser($viewer) ->setTrace($this->stackWhenCreated)); $dialog->setIsStandalone(true); $dialog->setWidth(AphrontDialogView::WIDTH_FULL); $box = id(new PHUIBoxView()) ->addMargin(PHUI::MARGIN_LARGE) ->appendChild($dialog); $view->appendChild($box); return $view->render(); } return ''; } } diff --git a/src/aphront/response/AphrontReloadResponse.php b/src/aphront/response/AphrontReloadResponse.php index 2b16512620..a95f02963e 100644 --- a/src/aphront/response/AphrontReloadResponse.php +++ b/src/aphront/response/AphrontReloadResponse.php @@ -1,21 +1,19 @@ getRequest()->isAjax()) { return null; } else { return parent::getURI(); } } } diff --git a/src/aphront/response/AphrontResponse.php b/src/aphront/response/AphrontResponse.php index 67da4aee93..759b03b091 100644 --- a/src/aphront/response/AphrontResponse.php +++ b/src/aphront/response/AphrontResponse.php @@ -1,151 +1,147 @@ request = $request; return $this; } public function getRequest() { return $this->request; } public function getHeaders() { $headers = array(); if (!$this->frameable) { $headers[] = array('X-Frame-Options', 'Deny'); } return $headers; } public function setCacheDurationInSeconds($duration) { $this->cacheable = $duration; return $this; } public function setLastModified($epoch_timestamp) { $this->lastModified = $epoch_timestamp; return $this; } public function setHTTPResponseCode($code) { $this->responseCode = $code; return $this; } public function getHTTPResponseCode() { return $this->responseCode; } public function getHTTPResponseMessage() { return ''; } public function setFrameable($frameable) { $this->frameable = $frameable; return $this; } public static function processValueForJSONEncoding(&$value, $key) { if ($value instanceof PhutilSafeHTMLProducerInterface) { // This renders the producer down to PhutilSafeHTML, which will then // be simplified into a string below. $value = hsprintf('%s', $value); } if ($value instanceof PhutilSafeHTML) { // TODO: Javelin supports implicity conversion of '__html' objects to // JX.HTML, but only for Ajax responses, not behaviors. Just leave things // as they are for now (where behaviors treat responses as HTML or plain // text at their discretion). $value = $value->getHTMLContent(); } } public static function encodeJSONForHTTPResponse(array $object) { array_walk_recursive( $object, array('AphrontResponse', 'processValueForJSONEncoding')); $response = json_encode($object); // Prevent content sniffing attacks by encoding "<" and ">", so browsers // won't try to execute the document as HTML even if they ignore // Content-Type and X-Content-Type-Options. See T865. $response = str_replace( array('<', '>'), array('\u003c', '\u003e'), $response); return $response; } protected function addJSONShield($json_response) { - // Add a shield to prevent "JSON Hijacking" attacks where an attacker // requests a JSON response using a normal ') !== false) { throw new Exception( 'Literal is not allowed inside inline script.'); } if (strpos($data, ' because it is ignored by HTML parsers. We // would need to send the document with XHTML content type. return phutil_tag( 'script', array('type' => 'text/javascript'), phutil_safe_html($data)); } public function buildAjaxResponse($payload, $error = null) { $response = array( 'error' => $error, 'payload' => $payload, ); if ($this->metadata) { $response['javelin_metadata'] = $this->metadata; $this->metadata = array(); } if ($this->behaviors) { $response['javelin_behaviors'] = $this->behaviors; $this->behaviors = array(); } $this->resolveResources(); $resources = array(); foreach ($this->packaged as $source_name => $resource_names) { $map = CelerityResourceMap::getNamedInstance($source_name); foreach ($resource_names as $resource_name) { $resources[] = $this->getURI($map, $resource_name); } } if ($resources) { $response['javelin_resources'] = $resources; } return $response; } public function getURI( CelerityResourceMap $map, $name, $use_primary_domain = false) { $uri = $map->getURIForName($name); // In developer mode, we dump file modification times into the URI. When a // page is reloaded in the browser, any resources brought in by Ajax calls // do not trigger revalidation, so without this it's very difficult to get // changes to Ajaxed-in CSS to work (you must clear your cache or rerun // the map script). In production, we can assume the map script gets run // after changes, and safely skip this. if (PhabricatorEnv::getEnvConfig('phabricator.developer-mode')) { $mtime = $map->getModifiedTimeForName($name); $uri = preg_replace('@^/res/@', '/res/'.$mtime.'T/', $uri); } if ($use_primary_domain) { return PhabricatorEnv::getURI($uri); } else { return PhabricatorEnv::getCDNURI($uri); } } } diff --git a/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php b/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php index f23862b8d0..f6f43a1de0 100644 --- a/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php +++ b/src/infrastructure/celerity/__tests__/CelerityResourceTransformerTestCase.php @@ -1,37 +1,34 @@ parse($options) + array( 'minify' => false, 'name' => $name, ); $xformer = new CelerityResourceTransformer(); $xformer->setRawURIMap( array( '/rsrc/example.png' => '/res/hash/example.png', )); $xformer->setMinify($options['minify']); $result = $xformer->transformResource($options['name'], $in); $this->assertEqual(rtrim($expect), rtrim($result), $file); } } } diff --git a/src/infrastructure/celerity/api.php b/src/infrastructure/celerity/api.php index 6c51f6beec..4340b57df0 100644 --- a/src/infrastructure/celerity/api.php +++ b/src/infrastructure/celerity/api.php @@ -1,58 +1,52 @@ requireResource($symbol, $source_name); } /** * Generate a node ID which is guaranteed to be unique for the current page, * even across Ajax requests. You should use this method to generate IDs for * nodes which require a uniqueness guarantee. * * @return string A string appropriate for use as an 'id' attribute on a DOM * node. It is guaranteed to be unique for the current page, even * if the current request is a subsequent Ajax request. - * - * @group celerity */ function celerity_generate_unique_node_id() { static $uniq = 0; $response = CelerityAPI::getStaticResourceResponse(); $block = $response->getMetadataBlock(); return 'UQ'.$block.'_'.($uniq++); } /** * Get the versioned URI for a raw resource, like an image. * * @param string Path to the raw image. * @return string Versioned path to the image, if one is available. - * - * @group celerity */ function celerity_get_resource_uri($resource, $source = 'phabricator') { $resource = ltrim($resource, '/'); $map = CelerityResourceMap::getNamedInstance($source); $response = CelerityAPI::getStaticResourceResponse(); return $response->getURI($map, $resource); } diff --git a/src/infrastructure/daemon/bot/PhabricatorBot.php b/src/infrastructure/daemon/bot/PhabricatorBot.php index 6738b4592e..0d6675849d 100644 --- a/src/infrastructure/daemon/bot/PhabricatorBot.php +++ b/src/infrastructure/daemon/bot/PhabricatorBot.php @@ -1,150 +1,148 @@ getArgv(); if (count($argv) !== 1) { throw new Exception('usage: PhabricatorBot '); } $json_raw = Filesystem::readFile($argv[0]); $config = json_decode($json_raw, true); if (!is_array($config)) { throw new Exception("File '{$argv[0]}' is not valid JSON!"); } $nick = idx($config, 'nick', 'phabot'); $handlers = idx($config, 'handlers', array()); $protocol_adapter_class = idx( $config, 'protocol-adapter', 'PhabricatorIRCProtocolAdapter'); $this->pollFrequency = idx($config, 'poll-frequency', 1); $this->config = $config; foreach ($handlers as $handler) { $obj = newv($handler, array($this)); $this->handlers[] = $obj; } $ca_bundle = idx($config, 'https.cabundle'); if ($ca_bundle) { HTTPSFuture::setGlobalCABundleFromPath($ca_bundle); } $conduit_uri = idx($config, 'conduit.uri'); if ($conduit_uri) { $conduit_user = idx($config, 'conduit.user'); $conduit_cert = idx($config, 'conduit.cert'); // Normalize the path component of the URI so users can enter the // domain without the "/api/" part. $conduit_uri = new PhutilURI($conduit_uri); $conduit_host = (string)$conduit_uri->setPath('/'); $conduit_uri = (string)$conduit_uri->setPath('/api/'); $conduit = new ConduitClient($conduit_uri); $response = $conduit->callMethodSynchronous( 'conduit.connect', array( 'client' => 'PhabricatorBot', 'clientVersion' => '1.0', 'clientDescription' => php_uname('n').':'.$nick, 'host' => $conduit_host, 'user' => $conduit_user, 'certificate' => $conduit_cert, )); $this->conduit = $conduit; } // Instantiate Protocol Adapter, for now follow same technique as // handler instantiation $this->protocolAdapter = newv($protocol_adapter_class, array()); $this->protocolAdapter ->setConfig($this->config) ->connect(); $this->runLoop(); } public function getConfig($key, $default = null) { return idx($this->config, $key, $default); } private function runLoop() { do { $this->stillWorking(); $messages = $this->protocolAdapter->getNextMessages($this->pollFrequency); if (count($messages) > 0) { foreach ($messages as $message) { $this->routeMessage($message); } } foreach ($this->handlers as $handler) { $handler->runBackgroundTasks(); } } while (true); } public function writeMessage(PhabricatorBotMessage $message) { return $this->protocolAdapter->writeMessage($message); } private function routeMessage(PhabricatorBotMessage $message) { $ignore = $this->getConfig('ignore'); if ($ignore) { $sender = $message->getSender(); if ($sender && in_array($sender->getName(), $ignore)) { return; } } if ($message->getCommand() == 'LOG') { $this->log('[LOG] '.$message->getBody()); } foreach ($this->handlers as $handler) { try { $handler->receiveMessage($message); } catch (Exception $ex) { phlog($ex); } } } public function getAdapter() { return $this->protocolAdapter; } public function getConduit() { if (empty($this->conduit)) { throw new Exception( "This bot is not configured with a Conduit uplink. Set 'conduit.uri', ". "'conduit.user' and 'conduit.cert' in the configuration to connect."); } return $this->conduit; } } diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php index 81520518d9..21eadf1569 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotFeedNotificationHandler.php @@ -1,183 +1,180 @@ getConfig('notification.types'); if ($show) { $obj_type = phid_get_type($story_objectphid); if (!in_array(strtolower($obj_type), $show)) { return false; } } $verbosity = $this->getConfig('notification.verbosity', 3); $verbs = array(); switch ($verbosity) { case 2: $verbs[] = array( - 'commented', - 'added', - 'changed', - 'resigned', - 'explained', - 'modified', - 'attached', - 'edited', - 'joined', - 'left', - 'removed' - ); + 'commented', + 'added', + 'changed', + 'resigned', + 'explained', + 'modified', + 'attached', + 'edited', + 'joined', + 'left', + 'removed', + ); // fallthrough case 1: $verbs[] = array( - 'updated', - 'accepted', - 'requested', - 'planned', - 'claimed', - 'summarized', - 'commandeered', - 'assigned' - ); + 'updated', + 'accepted', + 'requested', + 'planned', + 'claimed', + 'summarized', + 'commandeered', + 'assigned', + ); // fallthrough case 0: $verbs[] = array( - 'created', - 'closed', - 'raised', - 'committed', - 'abandoned', - 'reclaimed', - 'reopened', - 'deleted' - ); - break; + 'created', + 'closed', + 'raised', + 'committed', + 'abandoned', + 'reclaimed', + 'reopened', + 'deleted', + ); + break; case 3: default: return true; - break; } $verbs = '/('.implode('|', array_mergev($verbs)).')/'; if (preg_match($verbs, $story_text)) { return true; } return false; } public function receiveMessage(PhabricatorBotMessage $message) { return; } public function runBackgroundTasks() { if ($this->startupDelay > 0) { - // the event loop runs every 1s so delay enough to fully conenct - $this->startupDelay--; + // the event loop runs every 1s so delay enough to fully conenct + $this->startupDelay--; - return; + return; } if ($this->lastSeenChronoKey == 0) { // Since we only want to post notifications about new stories, skip // everything that's happened in the past when we start up so we'll // only process real-time stories. $latest = $this->getConduit()->callMethodSynchronous( 'feed.query', array( 'limit' => 1, )); foreach ($latest as $story) { if ($story['chronologicalKey'] > $this->lastSeenChronoKey) { $this->lastSeenChronoKey = $story['chronologicalKey']; } } return; } $config_max_pages = $this->getConfig('notification.max_pages', 5); $config_page_size = $this->getConfig('notification.page_size', 10); $last_seen_chrono_key = $this->lastSeenChronoKey; $chrono_key_cursor = 0; // Not efficient but works due to feed.query API for ($max_pages = $config_max_pages; $max_pages > 0; $max_pages--) { $stories = $this->getConduit()->callMethodSynchronous( 'feed.query', array( 'limit' => $config_page_size, 'after' => $chrono_key_cursor, 'view' => 'text', )); foreach ($stories as $story) { if ($story['chronologicalKey'] == $last_seen_chrono_key) { // Caught up on feed return; } if ($story['chronologicalKey'] > $this->lastSeenChronoKey) { // Keep track of newest seen story $this->lastSeenChronoKey = $story['chronologicalKey']; } if (!$chrono_key_cursor || $story['chronologicalKey'] < $chrono_key_cursor) { // Keep track of oldest story on this page $chrono_key_cursor = $story['chronologicalKey']; } if (!$story['text'] || !$this->shouldShowStory($story)) { continue; } $message = $story['text']; $story_object_type = phid_get_type($story['objectPHID']); if (in_array($story_object_type, $this->typesNeedURI)) { $objects = $this->getConduit()->callMethodSynchronous( 'phid.lookup', array( 'names' => array($story['objectPHID']), )); $message .= ' '.$objects[$story['objectPHID']]['uri']; } $channels = $this->getConfig('join'); foreach ($channels as $channel_name) { $channel = id(new PhabricatorBotChannel()) ->setName($channel_name); $this->writeMessage( id(new PhabricatorBotMessage()) ->setCommand('MESSAGE') ->setTarget($channel) ->setBody($message)); } } } } } diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php index 9dd449b155..b649d33619 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotHandler.php @@ -1,73 +1,72 @@ bot = $irc_bot; } final protected function writeMessage(PhabricatorBotMessage $message) { $this->bot->writeMessage($message); return $this; } final protected function getConduit() { return $this->bot->getConduit(); } final protected function getConfig($key, $default = null) { return $this->bot->getConfig($key, $default); } final protected function getURI($path) { $base_uri = new PhutilURI($this->bot->getConfig('conduit.uri')); $base_uri->setPath($path); return (string)$base_uri; } final protected function getServiceName() { return $this->bot->getAdapter()->getServiceName(); } final protected function getServiceType() { return $this->bot->getAdapter()->getServiceType(); } abstract public function receiveMessage(PhabricatorBotMessage $message); public function runBackgroundTasks() { return; } public function replyTo(PhabricatorBotMessage $original_message, $body) { if ($original_message->getCommand() != 'MESSAGE') { throw new Exception( 'Handler is trying to reply to something which is not a message!'); } $reply = id(new PhabricatorBotMessage()) ->setCommand('MESSAGE'); if ($original_message->getTarget()->isPublic()) { // This is a public target, like a chatroom. Send the response to the // chatroom. $reply->setTarget($original_message->getTarget()); } else { // This is a private target, like a private message. Send the response // back to the sender (presumably, we are the target). $reply->setTarget($original_message->getSender()); } $reply->setBody($body); return $this->writeMessage($reply); } + } diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php index 7f110c9487..4f0a9dee35 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotLogHandler.php @@ -1,80 +1,77 @@ getCommand()) { case 'MESSAGE': $target = $message->getTarget(); if (!$target->isPublic()) { // Don't log private messages, although maybe we should for debugging? break; } $target_name = $target->getName(); $logs = array( array( 'channel' => $target_name, 'type' => 'mesg', 'epoch' => time(), 'author' => $message->getSender()->getName(), 'message' => $message->getBody(), 'serviceName' => $this->getServiceName(), 'serviceType' => $this->getServiceType(), ), ); $this->futures[] = $this->getConduit()->callMethod( 'chatlog.record', array( 'logs' => $logs, )); $prompts = array( '/where is the (chat)?log\?/i', '/where am i\?/i', '/what year is (this|it)\?/i', ); $tell = false; foreach ($prompts as $prompt) { if (preg_match($prompt, $message->getBody())) { $tell = true; break; } } if ($tell) { $response = $this->getURI( '/chatlog/channel/'.phutil_escape_uri($target_name).'/'); $this->replyTo($message, $response); } break; } } public function runBackgroundTasks() { foreach ($this->futures as $key => $future) { try { if ($future->isReady()) { unset($this->futures[$key]); } } catch (Exception $ex) { unset($this->futures[$key]); phlog($ex); } } } } diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php index 009982fb5b..10bbbd48b9 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotMacroHandler.php @@ -1,164 +1,161 @@ macros === false) { return false; } if ($this->macros !== null) { return true; } $macros = $this->getConduit()->callMethodSynchronous( 'macro.query', array()); // If we have no macros, cache `false` (meaning "no macros") and return // immediately. if (!$macros) { $this->macros = false; return false; } $regexp = array(); foreach ($macros as $macro_name => $macro) { $regexp[] = preg_quote($macro_name, '/'); } $regexp = '/('.implode('|', $regexp).')/'; $this->macros = $macros; $this->regexp = $regexp; return true; } public function receiveMessage(PhabricatorBotMessage $message) { if (!$this->init()) { return; } switch ($message->getCommand()) { case 'MESSAGE': $message_body = $message->getBody(); $matches = null; if (!preg_match($this->regexp, $message_body, $matches)) { return; } $macro = $matches[1]; $ascii = idx($this->macros[$macro], 'ascii'); if ($ascii === false) { return; } if (!$ascii) { $this->macros[$macro]['ascii'] = $this->rasterize( $this->macros[$macro], $this->getConfig('macro.size', 48), $this->getConfig('macro.aspect', 0.66)); $ascii = $this->macros[$macro]['ascii']; } $target_name = $message->getTarget()->getName(); foreach ($ascii as $line) { $this->replyTo($message, $line); } break; } } public function rasterize($macro, $size, $aspect) { $image = HTTPSFuture::loadContent($macro['uri']); if (!$image) { return false; } $img = @imagecreatefromstring($image); if (!$img) { return false; } $sx = imagesx($img); $sy = imagesy($img); if ($sx > $size || $sy > $size) { $scale = max($sx, $sy) / $size; $dx = floor($sx / $scale); $dy = floor($sy / $scale); } else { $dx = $sx; $dy = $sy; } $dy = floor($dy * $aspect); $dst = imagecreatetruecolor($dx, $dy); if (!$dst) { return false; } imagealphablending($dst, false); $ok = imagecopyresampled( $dst, $img, 0, 0, 0, 0, $dx, $dy, $sx, $sy); if (!$ok) { return false; } $map = array( ' ', '.', ',', ':', ';', '!', '|', '*', '=', '@', '$', '#', ); $lines = array(); for ($ii = 0; $ii < $dy; $ii++) { $buf = ''; for ($jj = 0; $jj < $dx; $jj++) { $c = imagecolorat($dst, $jj, $ii); $a = ($c >> 24) & 0xFF; $r = ($c >> 16) & 0xFF; $g = ($c >> 8) & 0xFF; $b = ($c) & 0xFF; $luma = (255 - ((0.30 * $r) + (0.59 * $g) + (0.11 * $b))) / 256; $luma *= ((127 - $a) / 127); $char = $map[max(0, floor($luma * count($map)))]; $buf .= $char; } $lines[] = $buf; } return $lines; } } diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php index 800f5aa767..9bdc0d0253 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotObjectNameHandler.php @@ -1,197 +1,194 @@ getCommand()) { - case 'MESSAGE': - $message = $original_message->getBody(); - $matches = null; - - $paste_ids = array(); - $commit_names = array(); - $vote_ids = array(); - $file_ids = array(); - $object_names = array(); - $output = array(); - - $pattern = - '@'. - '(?getBody(); + $matches = null; + + $paste_ids = array(); + $commit_names = array(); + $vote_ids = array(); + $file_ids = array(); + $object_names = array(); + $output = array(); + + $pattern = + '@'. + '(?getConduit()->callMethodSynchronous( - 'phid.lookup', - array( - 'names' => $object_names, - )); - foreach ($objects as $object) { - $output[$object['phid']] = $object['fullName'].' - '.$object['uri']; - } - } - if ($vote_ids) { - foreach ($vote_ids as $vote_id) { - $vote = $this->getConduit()->callMethodSynchronous( - 'slowvote.info', + if ($object_names) { + $objects = $this->getConduit()->callMethodSynchronous( + 'phid.lookup', array( - 'poll_id' => $vote_id, + 'names' => $object_names, )); - $output[$vote['phid']] = 'V'.$vote['id'].': '.$vote['question']. - ' Come Vote '.$vote['uri']; + foreach ($objects as $object) { + $output[$object['phid']] = $object['fullName'].' - '.$object['uri']; + } } - } - if ($file_ids) { - foreach ($file_ids as $file_id) { - $file = $this->getConduit()->callMethodSynchronous( - 'file.info', - array( - 'id' => $file_id, - )); - $output[$file['phid']] = $file['objectName'].': '.$file['uri'].' - '. - $file['name']; + if ($vote_ids) { + foreach ($vote_ids as $vote_id) { + $vote = $this->getConduit()->callMethodSynchronous( + 'slowvote.info', + array( + 'poll_id' => $vote_id, + )); + $output[$vote['phid']] = 'V'.$vote['id'].': '.$vote['question']. + ' Come Vote '.$vote['uri']; + } } - } - if ($paste_ids) { - foreach ($paste_ids as $paste_id) { - $paste = $this->getConduit()->callMethodSynchronous( - 'paste.info', - array( - 'paste_id' => $paste_id, - )); - // Eventually I'd like to show the username of the paster as well, - // however that will need something like a user.username_from_phid - // since we (ideally) want to keep the bot to Conduit calls...and - // not call to Phabricator-specific stuff (like actually loading - // the User object and fetching his/her username.) - $output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '. - $paste['title']; - - if ($paste['language']) { - $output[$paste['phid']] .= ' ('.$paste['language'].')'; + if ($file_ids) { + foreach ($file_ids as $file_id) { + $file = $this->getConduit()->callMethodSynchronous( + 'file.info', + array( + 'id' => $file_id, + )); + $output[$file['phid']] = $file['objectName'].': '.$file['uri'].' - '. + $file['name']; } } - } - - if ($commit_names) { - $commits = $this->getConduit()->callMethodSynchronous( - 'diffusion.getcommits', - array( - 'commits' => $commit_names, - )); - foreach ($commits as $commit) { - if (isset($commit['error'])) { - continue; + + if ($paste_ids) { + foreach ($paste_ids as $paste_id) { + $paste = $this->getConduit()->callMethodSynchronous( + 'paste.info', + array( + 'paste_id' => $paste_id, + )); + // Eventually I'd like to show the username of the paster as well, + // however that will need something like a user.username_from_phid + // since we (ideally) want to keep the bot to Conduit calls...and + // not call to Phabricator-specific stuff (like actually loading + // the User object and fetching his/her username.) + $output[$paste['phid']] = 'P'.$paste['id'].': '.$paste['uri'].' - '. + $paste['title']; + + if ($paste['language']) { + $output[$paste['phid']] .= ' ('.$paste['language'].')'; + } } - $output[$commit['commitPHID']] = $commit['uri']; } - } - foreach ($output as $phid => $description) { + if ($commit_names) { + $commits = $this->getConduit()->callMethodSynchronous( + 'diffusion.getcommits', + array( + 'commits' => $commit_names, + )); + foreach ($commits as $commit) { + if (isset($commit['error'])) { + continue; + } + $output[$commit['commitPHID']] = $commit['uri']; + } + } - // Don't mention the same object more than once every 10 minutes - // in public channels, so we avoid spamming the chat over and over - // again for discsussions of a specific revision, for example. + foreach ($output as $phid => $description) { - $target_name = $original_message->getTarget()->getName(); - if (empty($this->recentlyMentioned[$target_name])) { - $this->recentlyMentioned[$target_name] = array(); - } + // Don't mention the same object more than once every 10 minutes + // in public channels, so we avoid spamming the chat over and over + // again for discsussions of a specific revision, for example. - $quiet_until = idx( - $this->recentlyMentioned[$target_name], - $phid, - 0) + (60 * 10); + $target_name = $original_message->getTarget()->getName(); + if (empty($this->recentlyMentioned[$target_name])) { + $this->recentlyMentioned[$target_name] = array(); + } - if (time() < $quiet_until) { - // Remain quiet on this channel. - continue; - } + $quiet_until = idx( + $this->recentlyMentioned[$target_name], + $phid, + 0) + (60 * 10); - $this->recentlyMentioned[$target_name][$phid] = time(); - $this->replyTo($original_message, $description); - } - break; + if (time() < $quiet_until) { + // Remain quiet on this channel. + continue; + } + + $this->recentlyMentioned[$target_name][$phid] = time(); + $this->replyTo($original_message, $description); + } + break; } } } diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php index 536f8713ad..48734af94a 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotSymbolHandler.php @@ -1,50 +1,47 @@ ?" - * - * @group irc */ final class PhabricatorBotSymbolHandler extends PhabricatorBotHandler { public function receiveMessage(PhabricatorBotMessage $message) { - switch ($message->getCommand()) { - case 'MESSAGE': - $text = $message->getBody(); - - $matches = null; - if (!preg_match('/where(?: in the world)? is (\S+?)\?/i', - $text, $matches)) { - break; + case 'MESSAGE': + $text = $message->getBody(); + + $matches = null; + if (!preg_match('/where(?: in the world)? is (\S+?)\?/i', + $text, $matches)) { + break; + } + + $symbol = $matches[1]; + $results = $this->getConduit()->callMethodSynchronous( + 'diffusion.findsymbols', + array( + 'name' => $symbol, + )); + + $default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/'); + + if (count($results) > 1) { + $response = "Multiple symbols named '{$symbol}': {$default_uri}"; + } else if (count($results) == 1) { + $result = head($results); + $response = + $result['type'].' '. + $result['name'].' '. + '('.$result['language'].'): '. + nonempty($result['uri'], $default_uri); + } else { + $response = "No symbol '{$symbol}' found anywhere."; } - $symbol = $matches[1]; - $results = $this->getConduit()->callMethodSynchronous( - 'diffusion.findsymbols', - array( - 'name' => $symbol, - )); - - $default_uri = $this->getURI('/diffusion/symbol/'.$symbol.'/'); - - if (count($results) > 1) { - $response = "Multiple symbols named '{$symbol}': {$default_uri}"; - } else if (count($results) == 1) { - $result = head($results); - $response = - $result['type'].' '. - $result['name'].' '. - '('.$result['language'].'): '. - nonempty($result['uri'], $default_uri); - } else { - $response = "No symbol '{$symbol}' found anywhere."; - } - - $this->replyTo($message, $response); - - break; + $this->replyTo($message, $response); + + break; } } } diff --git a/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php b/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php index e052230410..59a921555c 100644 --- a/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php +++ b/src/infrastructure/daemon/bot/handler/PhabricatorBotWhatsNewHandler.php @@ -1,45 +1,43 @@ getCommand()) { case 'MESSAGE': $message_body = $message->getBody(); $now = time(); $prompt = '~what( i|\')?s new\?~i'; if (preg_match($prompt, $message_body)) { if ($now < $this->floodblock) { return; } $this->floodblock = $now + 60; $this->reportNew($message); } break; } } public function reportNew(PhabricatorBotMessage $message) { $latest = $this->getConduit()->callMethodSynchronous( 'feed.query', array( 'limit' => 5, 'view' => 'text' )); foreach ($latest as $feed_item) { if (isset($feed_item['text'])) { $this->replyTo($message, html_entity_decode($feed_item['text'])); } } } } diff --git a/src/infrastructure/daemon/workers/PhabricatorWorker.php b/src/infrastructure/daemon/workers/PhabricatorWorker.php index b162f4344a..074d580a52 100644 --- a/src/infrastructure/daemon/workers/PhabricatorWorker.php +++ b/src/infrastructure/daemon/workers/PhabricatorWorker.php @@ -1,241 +1,239 @@ data = $data; } final protected function getTaskData() { return $this->data; } final public function executeTask() { $this->doWork(); } final public static function scheduleTask($task_class, $data) { $task = id(new PhabricatorWorkerActiveTask()) ->setTaskClass($task_class) ->setData($data); if (self::$runAllTasksInProcess) { // Do the work in-process. $worker = newv($task_class, array($data)); while (true) { try { $worker->doWork(); foreach ($worker->getQueuedTasks() as $queued_task) { list($queued_class, $queued_data) = $queued_task; self::scheduleTask($queued_class, $queued_data); } break; } catch (PhabricatorWorkerYieldException $ex) { phlog( pht( 'In-process task "%s" yielded for %s seconds, sleeping...', $task_class, $ex->getDuration())); sleep($ex->getDuration()); } } // Now, save a task row and immediately archive it so we can return an // object with a valid ID. $task->openTransaction(); $task->save(); $archived = $task->archiveTask( PhabricatorWorkerArchiveTask::RESULT_SUCCESS, 0); $task->saveTransaction(); return $archived; } else { $task->save(); return $task; } } /** * Wait for tasks to complete. If tasks are not leased by other workers, they * will be executed in this process while waiting. * * @param list List of queued task IDs to wait for. * @return void */ final public static function waitForTasks(array $task_ids) { if (!$task_ids) { return; } $task_table = new PhabricatorWorkerActiveTask(); $waiting = array_fuse($task_ids); while ($waiting) { $conn_w = $task_table->establishConnection('w'); // Check if any of the tasks we're waiting on are still queued. If they // are not, we're done waiting. $row = queryfx_one( $conn_w, 'SELECT COUNT(*) N FROM %T WHERE id IN (%Ld)', $task_table->getTableName(), $waiting); if (!$row['N']) { // Nothing is queued anymore. Stop waiting. break; } $tasks = id(new PhabricatorWorkerLeaseQuery()) ->withIDs($waiting) ->setLimit(1) ->execute(); if (!$tasks) { // We were not successful in leasing anything. Sleep for a bit and // see if we have better luck later. sleep(1); continue; } $task = head($tasks)->executeTask(); $ex = $task->getExecutionException(); if ($ex) { throw $ex; } } $tasks = id(new PhabricatorWorkerArchiveTask())->loadAllWhere( 'id IN (%Ld)', $task_ids); foreach ($tasks as $task) { if ($task->getResult() != PhabricatorWorkerArchiveTask::RESULT_SUCCESS) { throw new Exception( pht('Task %d failed!', $task->getID())); } } } public function renderForDisplay(PhabricatorUser $viewer) { $data = PhutilReadableSerializer::printableValue($this->data); return phutil_tag('pre', array(), $data); } /** * Set this flag to execute scheduled tasks synchronously, in the same * process. This is useful for debugging, and otherwise dramatically worse * in every way imaginable. */ public static function setRunAllTasksInProcess($all) { self::$runAllTasksInProcess = $all; } protected function log($pattern /* $args */) { $console = PhutilConsole::getConsole(); $argv = func_get_args(); call_user_func_array(array($console, 'writeLog'), $argv); return $this; } /** - * Queue a task to be executed after this one suceeds. + * Queue a task to be executed after this one succeeds. * * The followup task will be queued only if this task completes cleanly. * * @param string Task class to queue. * @param array Data for the followup task. * @return this */ protected function queueTask($class, array $data) { $this->queuedTasks[] = array($class, $data); return $this; } /** * Get tasks queued as followups by @{method:queueTask}. * * @return list> Queued task specifications. */ public function getQueuedTasks() { return $this->queuedTasks; } } diff --git a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php index 3447206173..ca0d5db5c6 100644 --- a/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php +++ b/src/infrastructure/daemon/workers/query/PhabricatorWorkerLeaseQuery.php @@ -1,236 +1,234 @@ ids = $ids; return $this; } public function setLimit($limit) { $this->limit = $limit; return $this; } public function execute() { if (!$this->limit) { throw new Exception('You must setLimit() when leasing tasks.'); } $task_table = new PhabricatorWorkerActiveTask(); $taskdata_table = new PhabricatorWorkerTaskData(); $lease_ownership_name = $this->getLeaseOwnershipName(); $conn_w = $task_table->establishConnection('w'); // Try to satisfy the request from new, unleased tasks first. If we don't // find enough tasks, try tasks with expired leases (i.e., tasks which have // previously failed). $phases = array( self::PHASE_UNLEASED, self::PHASE_EXPIRED, ); $limit = $this->limit; $leased = 0; foreach ($phases as $phase) { // NOTE: If we issue `UPDATE ... WHERE ... ORDER BY id ASC`, the query // goes very, very slowly. The `ORDER BY` triggers this, although we get // the same apparent results without it. Without the ORDER BY, binary // read slaves complain that the query isn't repeatable. To avoid both // problems, do a SELECT and then an UPDATE. $rows = queryfx_all( $conn_w, 'SELECT id, leaseOwner FROM %T %Q %Q %Q', $task_table->getTableName(), $this->buildWhereClause($conn_w, $phase), $this->buildOrderClause($conn_w, $phase), $this->buildLimitClause($conn_w, $limit - $leased)); // NOTE: Sometimes, we'll race with another worker and they'll grab // this task before we do. We could reduce how often this happens by // selecting more tasks than we need, then shuffling them and trying // to lock only the number we're actually after. However, the amount // of time workers spend here should be very small relative to their // total runtime, so keep it simple for the moment. if ($rows) { queryfx( $conn_w, 'UPDATE %T task SET leaseOwner = %s, leaseExpires = UNIX_TIMESTAMP() + %d %Q', $task_table->getTableName(), $lease_ownership_name, self::getDefaultLeaseDuration(), $this->buildUpdateWhereClause($conn_w, $phase, $rows)); $leased += $conn_w->getAffectedRows(); if ($leased == $limit) { break; } } } if (!$leased) { return array(); } $data = queryfx_all( $conn_w, 'SELECT task.*, taskdata.data _taskData, UNIX_TIMESTAMP() _serverTime FROM %T task LEFT JOIN %T taskdata ON taskdata.id = task.dataID WHERE leaseOwner = %s AND leaseExpires > UNIX_TIMESTAMP() %Q %Q', $task_table->getTableName(), $taskdata_table->getTableName(), $lease_ownership_name, $this->buildOrderClause($conn_w, $phase), $this->buildLimitClause($conn_w, $limit)); $tasks = $task_table->loadAllFromArray($data); $tasks = mpull($tasks, null, 'getID'); foreach ($data as $row) { $tasks[$row['id']]->setServerTime($row['_serverTime']); if ($row['_taskData']) { $task_data = json_decode($row['_taskData'], true); } else { $task_data = null; } $tasks[$row['id']]->setData($task_data); } return $tasks; } private function buildWhereClause(AphrontDatabaseConnection $conn_w, $phase) { $where = array(); switch ($phase) { case self::PHASE_UNLEASED: $where[] = 'leaseOwner IS NULL'; break; case self::PHASE_EXPIRED: $where[] = 'leaseExpires < UNIX_TIMESTAMP()'; break; default: throw new Exception("Unknown phase '{$phase}'!"); } if ($this->ids) { $where[] = qsprintf( $conn_w, 'id IN (%Ld)', $this->ids); } return $this->formatWhereClause($where); } private function buildUpdateWhereClause( AphrontDatabaseConnection $conn_w, $phase, array $rows) { $where = array(); // NOTE: This is basically working around the MySQL behavior that // `IN (NULL)` doesn't match NULL. switch ($phase) { case self::PHASE_UNLEASED: $where[] = qsprintf( $conn_w, 'leaseOwner IS NULL'); $where[] = qsprintf( $conn_w, 'id IN (%Ld)', ipull($rows, 'id')); break; case self::PHASE_EXPIRED: $in = array(); foreach ($rows as $row) { $in[] = qsprintf( $conn_w, '(id = %d AND leaseOwner = %s)', $row['id'], $row['leaseOwner']); } $where[] = qsprintf( $conn_w, '(%Q)', implode(' OR ', $in)); break; default: throw new Exception("Unknown phase '{$phase}'!"); } return $this->formatWhereClause($where); } private function buildOrderClause(AphrontDatabaseConnection $conn_w, $phase) { switch ($phase) { case self::PHASE_UNLEASED: // When selecting new tasks, we want to consume them in roughly // FIFO order, so we order by the task ID. return qsprintf($conn_w, 'ORDER BY id ASC'); case self::PHASE_EXPIRED: // When selecting failed tasks, we want to consume them in roughly // FIFO order of their failures, which is not necessarily their original // queue order. // Particularly, this is important for tasks which use soft failures to // indicate that they are waiting on other tasks to complete: we need to // push them to the end of the queue after they fail, at least on // average, so we don't deadlock retrying the same blocked task over // and over again. return qsprintf($conn_w, 'ORDER BY leaseExpires ASC'); default: throw new Exception(pht('Unknown phase "%s"!', $phase)); } } private function buildLimitClause(AphrontDatabaseConnection $conn_w, $limit) { return qsprintf($conn_w, 'LIMIT %d', $limit); } private function getLeaseOwnershipName() { static $sequence = 0; $parts = array( getmypid(), time(), php_uname('n'), ++$sequence, ); return implode(':', $parts); } } diff --git a/src/infrastructure/events/PhabricatorEvent.php b/src/infrastructure/events/PhabricatorEvent.php index 03116cdd9e..bfb08b203b 100644 --- a/src/infrastructure/events/PhabricatorEvent.php +++ b/src/infrastructure/events/PhabricatorEvent.php @@ -1,44 +1,40 @@ user = $user; return $this; } public function getUser() { return $this->user; } public function setAphrontRequest(AphrontRequest $aphront_request) { $this->aphrontRequest = $aphront_request; return $this; } public function getAphrontRequest() { return $this->aphrontRequest; } public function setConduitRequest(ConduitAPIRequest $conduit_request) { $this->conduitRequest = $conduit_request; return $this; } public function getConduitRequest() { return $this->conduitRequest; } } diff --git a/src/infrastructure/events/PhabricatorExampleEventListener.php b/src/infrastructure/events/PhabricatorExampleEventListener.php index 4cbbb6b0bb..0a7bd699df 100644 --- a/src/infrastructure/events/PhabricatorExampleEventListener.php +++ b/src/infrastructure/events/PhabricatorExampleEventListener.php @@ -1,31 +1,28 @@ listen(PhabricatorEventType::TYPE_TEST_DIDRUNTEST); } public function handleEvent(PhutilEvent $event) { // When an event you have called listen() for in your register() method // occurs, this method will be invoked. You should respond to the event. // In this case, we just echo a message out so the event test script will // do something visible. $console = PhutilConsole::getConsole(); $console->writeOut( "PhabricatorExampleEventListener got test event at %d\n", $event->getValue('time')); } } diff --git a/src/infrastructure/events/constant/PhabricatorEventType.php b/src/infrastructure/events/constant/PhabricatorEventType.php index 780d894a4b..f33cd4a2d4 100644 --- a/src/infrastructure/events/constant/PhabricatorEventType.php +++ b/src/infrastructure/events/constant/PhabricatorEventType.php @@ -1,41 +1,39 @@ PhabricatorEnv::getEnvConfig('pygments.enabled'), 'filename.map' => PhabricatorEnv::getEnvConfig('syntax.filemap'), ); foreach ($config as $key => $value) { $engine->setConfig($key, $value); } return $engine; } public static function highlightWithFilename($filename, $source) { $engine = self::newEngine(); $language = $engine->getLanguageFromFilename($filename); return $engine->highlightSource($language, $source); } public static function highlightWithLanguage($language, $source) { $engine = self::newEngine(); return $engine->highlightSource($language, $source); } - } diff --git a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php index 2627468896..e0057bf3dd 100644 --- a/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php +++ b/src/infrastructure/markup/rule/PhabricatorRemarkupRuleYoutube.php @@ -1,51 +1,47 @@ uri = new PhutilURI($text); if ($this->uri->getDomain() && preg_match('/(^|\.)youtube\.com$/', $this->uri->getDomain()) && idx($this->uri->getQueryParams(), 'v')) { return $this->markupYoutubeLink(); } return $text; } public function markupYoutubeLink() { $v = idx($this->uri->getQueryParams(), 'v'); if ($this->getEngine()->isTextMode()) { return $this->getEngine()->storeText('http://youtu.be/'.$v); } $youtube_src = 'https://www.youtube.com/embed/'.$v; $iframe = $this->newTag( 'div', array( 'class' => 'embedded-youtube-video', ), $this->newTag( 'iframe', array( 'width' => '650', 'height' => '400', 'style' => 'margin: 1em auto; border: 0px;', 'src' => $youtube_src, 'frameborder' => 0, ), '')); return $this->getEngine()->storeText($iframe); } } diff --git a/src/infrastructure/storage/lisk/LiskDAO.php b/src/infrastructure/storage/lisk/LiskDAO.php index 9250c8b7df..3683e35e6f 100644 --- a/src/infrastructure/storage/lisk/LiskDAO.php +++ b/src/infrastructure/storage/lisk/LiskDAO.php @@ -1,1826 +1,1823 @@ setName('Sawyer') * ->setBreed('Pug') * ->save(); * * Note that **Lisk automatically builds getters and setters for all of your * object's protected properties** via @{method:__call}. If you want to add * custom behavior to your getters or setters, you can do so by overriding the * @{method:readField} and @{method:writeField} methods. * * Calling @{method:save} will persist the object to the database. After calling * @{method:save}, you can call @{method:getID} to retrieve the object's ID. * * To load objects by ID, use the @{method:load} method: * * $dog = id(new Dog())->load($id); * - * This will load the Dog record with ID $id into $dog, or ##null## if no such + * This will load the Dog record with ID $id into $dog, or `null` if no such * record exists (@{method:load} is an instance method rather than a static * method because PHP does not support late static binding, at least until PHP * 5.3). * * To update an object, change its properties and save it: * * $dog->setBreed('Lab')->save(); * * To delete an object, call @{method:delete}: * * $dog->delete(); * * That's Lisk CRUD in a nutshell. * * = Queries = * * Often, you want to load a bunch of objects, or execute a more specialized * query. Use @{method:loadAllWhere} or @{method:loadOneWhere} to do this: * * $pugs = $dog->loadAllWhere('breed = %s', 'Pug'); * $sawyer = $dog->loadOneWhere('name = %s', 'Sawyer'); * * These methods work like @{function@libphutil:queryfx}, but only take half of * a query (the part after the WHERE keyword). Lisk will handle the connection, * columns, and object construction; you are responsible for the rest of it. * @{method:loadAllWhere} returns a list of objects, while * @{method:loadOneWhere} returns a single object (or `null`). * * There's also a @{method:loadRelatives} method which helps to prevent the 1+N * queries problem. * * = Managing Transactions = * * Lisk uses a transaction stack, so code does not generally need to be aware * of the transactional state of objects to implement correct transaction * semantics: * * $obj->openTransaction(); * $obj->save(); * $other->save(); * // ... * $other->openTransaction(); * $other->save(); * $another->save(); * if ($some_condition) { * $other->saveTransaction(); * } else { * $other->killTransaction(); * } * // ... * $obj->saveTransaction(); * * Assuming ##$obj##, ##$other## and ##$another## live on the same database, * this code will work correctly by establishing savepoints. * * Selects whose data are used later in the transaction should be included in * @{method:beginReadLocking} or @{method:beginWriteLocking} block. * * @task conn Managing Connections * @task config Configuring Lisk * @task load Loading Objects * @task info Examining Objects * @task save Writing Objects * @task hook Hooks and Callbacks * @task util Utilities * @task xaction Managing Transactions * @task isolate Isolation for Unit Testing - * - * @group storage */ abstract class LiskDAO { const CONFIG_IDS = 'id-mechanism'; const CONFIG_TIMESTAMPS = 'timestamps'; const CONFIG_AUX_PHID = 'auxiliary-phid'; const CONFIG_SERIALIZATION = 'col-serialization'; const CONFIG_PARTIAL_OBJECTS = 'partial-objects'; const CONFIG_BINARY = 'binary'; const SERIALIZATION_NONE = 'id'; const SERIALIZATION_JSON = 'json'; const SERIALIZATION_PHP = 'php'; const IDS_AUTOINCREMENT = 'ids-auto'; const IDS_COUNTER = 'ids-counter'; const IDS_MANUAL = 'ids-manual'; const COUNTER_TABLE_NAME = 'lisk_counter'; private $dirtyFields = array(); private $missingFields = array(); private static $processIsolationLevel = 0; private static $transactionIsolationLevel = 0; private $ephemeral = false; private static $connections = array(); private $inSet = null; protected $id; protected $phid; protected $dateCreated; protected $dateModified; /** * Build an empty object. * * @return obj Empty object. */ public function __construct() { $id_key = $this->getIDKey(); if ($id_key) { $this->$id_key = null; } if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) { $this->resetDirtyFields(); } } /* -( Managing Connections )----------------------------------------------- */ /** * Establish a live connection to a database service. This method should * return a new connection. Lisk handles connection caching and management; * do not perform caching deeper in the stack. * * @param string Mode, either 'r' (reading) or 'w' (reading and writing). * @return AphrontDatabaseConnection New database connection. * @task conn */ abstract protected function establishLiveConnection($mode); /** * Return a namespace for this object's connections in the connection cache. * Generally, the database name is appropriate. Two connections are considered * equivalent if they have the same connection namespace and mode. * * @return string Connection namespace for cache * @task conn */ abstract protected function getConnectionNamespace(); /** * Get an existing, cached connection for this object. * * @param mode Connection mode. * @return AprontDatabaseConnection|null Connection, if it exists in cache. * @task conn */ protected function getEstablishedConnection($mode) { $key = $this->getConnectionNamespace().':'.$mode; if (isset(self::$connections[$key])) { return self::$connections[$key]; } return null; } /** * Store a connection in the connection cache. * * @param mode Connection mode. * @param AphrontDatabaseConnection Connection to cache. * @return this * @task conn */ protected function setEstablishedConnection( $mode, AphrontDatabaseConnection $connection, $force_unique = false) { $key = $this->getConnectionNamespace().':'.$mode; if ($force_unique) { $key .= ':unique'; while (isset(self::$connections[$key])) { $key .= '!'; } } self::$connections[$key] = $connection; return $this; } /* -( Configuring Lisk )--------------------------------------------------- */ /** * Change Lisk behaviors, like ID configuration and timestamps. If you want * to change these behaviors, you should override this method in your child * class and change the options you're interested in. For example: * * public function getConfiguration() { * return array( * Lisk_DataAccessObject::CONFIG_EXAMPLE => true, * ) + parent::getConfiguration(); * } * * The available options are: * * CONFIG_IDS * Lisk objects need to have a unique identifying ID. The three mechanisms * available for generating this ID are IDS_AUTOINCREMENT (default, assumes * the ID column is an autoincrement primary key), IDS_MANUAL (you are taking * full responsibility for ID management), or IDS_COUNTER (see below). * * InnoDB does not persist the value of `auto_increment` across restarts, * and instead initializes it to `MAX(id) + 1` during startup. This means it * may reissue the same autoincrement ID more than once, if the row is deleted * and then the database is restarted. To avoid this, you can set an object to * use a counter table with IDS_COUNTER. This will generally behave like * IDS_AUTOINCREMENT, except that the counter value will persist across * restarts and inserts will be slightly slower. If a database stores any * DAOs which use this mechanism, you must create a table there with this * schema: * * CREATE TABLE lisk_counter ( * counterName VARCHAR(64) COLLATE utf8_bin PRIMARY KEY, * counterValue BIGINT UNSIGNED NOT NULL * ) ENGINE=InnoDB DEFAULT CHARSET=utf8; * * CONFIG_TIMESTAMPS * Lisk can automatically handle keeping track of a `dateCreated' and * `dateModified' column, which it will update when it creates or modifies * an object. If you don't want to do this, you may disable this option. * By default, this option is ON. * * CONFIG_AUX_PHID * This option can be enabled by being set to some truthy value. The meaning * of this value is defined by your PHID generation mechanism. If this option * is enabled, a `phid' property will be populated with a unique PHID when an * object is created (or if it is saved and does not currently have one). You * need to override generatePHID() and hook it into your PHID generation * mechanism for this to work. By default, this option is OFF. * * CONFIG_SERIALIZATION * You can optionally provide a column serialization map that will be applied * to values when they are written to the database. For example: * * self::CONFIG_SERIALIZATION => array( * 'complex' => self::SERIALIZATION_JSON, * ) * * This will cause Lisk to JSON-serialize the 'complex' field before it is * written, and unserialize it when it is read. * * CONFIG_PARTIAL_OBJECTS * Sometimes, it is useful to load only some fields of an object (such as * when you are loading all objects of a class, but only care about a few * fields). Turning on this option (by setting it to a truthy value) allows * users of the class to create/use partial objects, but it comes with some * side effects: your class cannot override the setters and getters provided * by Lisk (use readField and writeField instead), and you should not * directly access or assign protected members of your class (use the getters * and setters). * * CONFIG_BINARY * You can optionally provide a map of columns to a flag indicating that * they store binary data. These columns will not raise an error when * handling binary writes. * * @return dictionary Map of configuration options to values. * * @task config */ protected function getConfiguration() { return array( self::CONFIG_IDS => self::IDS_AUTOINCREMENT, self::CONFIG_TIMESTAMPS => true, self::CONFIG_PARTIAL_OBJECTS => false, ); } /** * Determine the setting of a configuration option for this class of objects. * * @param const Option name, one of the CONFIG_* constants. * @return mixed Option value, if configured (null if unavailable). * * @task config */ public function getConfigOption($option_name) { static $options = null; if (!isset($options)) { $options = $this->getConfiguration(); } return idx($options, $option_name); } /* -( Loading Objects )---------------------------------------------------- */ /** * Load an object by ID. You need to invoke this as an instance method, not * a class method, because PHP doesn't have late static binding (until * PHP 5.3.0). For example: * * $dog = id(new Dog())->load($dog_id); * * @param int Numeric ID identifying the object to load. * @return obj|null Identified object, or null if it does not exist. * * @task load */ public function load($id) { if (is_object($id)) { $id = (string)$id; } if (!$id || (!is_int($id) && !ctype_digit($id))) { return null; } return $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $id); } /** * Loads all of the objects, unconditionally. * * @return dict Dictionary of all persisted objects of this type, keyed * on object ID. * * @task load */ public function loadAll() { return $this->loadAllWhere('1 = 1'); } /** * Loads all objects, but only fetches the specified columns. * * @param array Array of canonical column names as strings * @return dict Dictionary of all objects, keyed by ID. * * @task load */ public function loadColumns(array $columns) { return $this->loadColumnsWhere($columns, '1 = 1'); } /** * Load all objects which match a WHERE clause. You provide everything after * the 'WHERE'; Lisk handles everything up to it. For example: * * $old_dogs = id(new Dog())->loadAllWhere('age > %d', 7); * * The pattern and arguments are as per queryfx(). * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return dict Dictionary of matching objects, keyed on ID. * * @task load */ - public function loadAllWhere($pattern/* , $arg, $arg, $arg ... */) { + public function loadAllWhere($pattern /* , $arg, $arg, $arg ... */) { $args = func_get_args(); array_unshift($args, null); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); return $this->loadAllFromArray($data); } /** * Loads selected columns from objects that match a WHERE clause. You must * provide everything after the WHERE. See loadAllWhere(). * * @param array List of column names. * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return dict Dictionary of matching objecks, keyed by ID. * * @task load */ - public function loadColumnsWhere(array $columns, $pattern/* , $args... */) { + public function loadColumnsWhere(array $columns, $pattern /* , $args... */) { if (!$this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) { throw new BadMethodCallException( 'This class does not support partial objects.'); } $args = func_get_args(); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); return $this->loadAllFromArray($data); } /** * Load a single object identified by a 'WHERE' clause. You provide - * everything after the 'WHERE', and Lisk builds the first half of the + * everything after the 'WHERE', and Lisk builds the first half of the * query. See loadAllWhere(). This method is similar, but returns a single * result instead of a list. * * @param string queryfx()-style SQL WHERE clause. * @param ... Zero or more conversions. * @return obj|null Matching object, or null if no object matches. * * @task load */ - public function loadOneWhere($pattern/* , $arg, $arg, $arg ... */) { + public function loadOneWhere($pattern /* , $arg, $arg, $arg ... */) { $args = func_get_args(); array_unshift($args, null); $data = call_user_func_array( array($this, 'loadRawDataWhere'), $args); if (count($data) > 1) { throw new AphrontQueryCountException( 'More than 1 result from loadOneWhere()!'); } $data = reset($data); if (!$data) { return null; } return $this->loadFromArray($data); } - protected function loadRawDataWhere($columns, $pattern/* , $args... */) { + protected function loadRawDataWhere($columns, $pattern /* , $args... */) { $connection = $this->establishConnection('r'); $lock_clause = ''; if ($connection->isReadLocking()) { $lock_clause = 'FOR UPDATE'; } else if ($connection->isWriteLocking()) { $lock_clause = 'LOCK IN SHARE MODE'; } $args = func_get_args(); $args = array_slice($args, 2); if (!$columns) { $column = '*'; } else { $column = '%LC'; $columns[] = $this->getIDKey(); $properties = $this->getProperties(); $this->missingFields = array_diff_key( array_flip($properties), array_flip($columns)); } $pattern = 'SELECT '.$column.' FROM %T WHERE '.$pattern.' %Q'; array_unshift($args, $this->getTableName()); if ($columns) { array_unshift($args, $columns); } array_push($args, $lock_clause); array_unshift($args, $pattern); return call_user_func_array( array($connection, 'queryData'), $args); } /** * Reload an object from the database, discarding any changes to persistent * properties. This is primarily useful after entering a transaction but * before applying changes to an object. * * @return this * * @task load */ public function reload() { - if (!$this->getID()) { throw new Exception("Unable to reload object that hasn't been loaded!"); } $result = $this->loadOneWhere( '%C = %d', $this->getIDKeyForUse(), $this->getID()); if (!$result) { throw new AphrontQueryObjectMissingException(); } return $this; } /** * Initialize this object's properties from a dictionary. Generally, you * load single objects with loadOneWhere(), but sometimes it may be more * convenient to pull data from elsewhere directly (e.g., a complicated - * join via queryData()) and then load from an array representation. + * join via @{method:queryData}) and then load from an array representation. * * @param dict Dictionary of properties, which should be equivalent to - * selecting a row from the table or calling getProperties(). + * selecting a row from the table or calling + * @{method:getProperties}. * @return this * * @task load */ public function loadFromArray(array $row) { static $valid_properties = array(); $map = array(); foreach ($row as $k => $v) { // We permit (but ignore) extra properties in the array because a // common approach to building the array is to issue a raw SELECT query // which may include extra explicit columns or joins. // This pathway is very hot on some pages, so we're inlining a cache // and doing some microoptimization to avoid a strtolower() call for each // assignment. The common path (assigning a valid property which we've // already seen) always incurs only one empty(). The second most common // path (assigning an invalid property which we've already seen) costs // an empty() plus an isset(). if (empty($valid_properties[$k])) { if (isset($valid_properties[$k])) { // The value is set but empty, which means it's false, so we've // already determined it's not valid. We don't need to check again. continue; } $valid_properties[$k] = $this->hasProperty($k); if (!$valid_properties[$k]) { continue; } } $map[$k] = $v; } $this->willReadData($map); foreach ($map as $prop => $value) { $this->$prop = $value; } $this->didReadData(); return $this; } /** * Initialize a list of objects from a list of dictionaries. Usually you - * load lists of objects with loadAllWhere(), but sometimes that isn't - * flexible enough. One case is if you need to do joins to select the right - * objects: + * load lists of objects with @{method:loadAllWhere}, but sometimes that + * isn't flexible enough. One case is if you need to do joins to select the + * right objects: * * function loadAllWithOwner($owner) { * $data = $this->queryData( * 'SELECT d.* * FROM owner o * JOIN owner_has_dog od ON o.id = od.ownerID * JOIN dog d ON od.dogID = d.id * WHERE o.id = %d', * $owner); * return $this->loadAllFromArray($data); * } * - * This is a lot messier than loadAllWhere(), but more flexible. + * This is a lot messier than @{method:loadAllWhere}, but more flexible. * * @param list List of property dictionaries. * @return dict List of constructed objects, keyed on ID. * * @task load */ public function loadAllFromArray(array $rows) { $result = array(); $id_key = $this->getIDKey(); foreach ($rows as $row) { $obj = clone $this; if ($id_key && isset($row[$id_key])) { $result[$row[$id_key]] = $obj->loadFromArray($row); } else { $result[] = $obj->loadFromArray($row); } if ($this->inSet) { $this->inSet->addToSet($obj); } } return $result; } /** * This method helps to prevent the 1+N queries problem. It happens when you * execute a query for each row in a result set. Like in this code: * * COUNTEREXAMPLE, name=Easy to write but expensive to execute * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * foreach ($diffs as $diff) { * $changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID = %d', * $diff->getID()); * // Do something with $changesets. * } * * One can solve this problem by reading all the dependent objects at once and * assigning them later: * * COUNTEREXAMPLE, name=Cheaper to execute but harder to write and maintain * $diffs = id(new DifferentialDiff())->loadAllWhere( * 'revisionID = %d', * $revision->getID()); * $all_changesets = id(new DifferentialChangeset())->loadAllWhere( * 'diffID IN (%Ld)', * mpull($diffs, 'getID')); * $all_changesets = mgroup($all_changesets, 'getDiffID'); * foreach ($diffs as $diff) { * $changesets = idx($all_changesets, $diff->getID(), array()); * // Do something with $changesets. * } * * The method @{method:loadRelatives} abstracts this approach which allows * writing a code which is simple and efficient at the same time: * * name=Easy to write and cheap to execute * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * // Do something with $changesets. * } * * This will load dependent objects for all diffs in the first call of * @{method:loadRelatives} and use this result for all following calls. * * The method supports working with set of sets, like in this code: * * $diffs = $revision->loadRelatives(new DifferentialDiff(), 'revisionID'); * foreach ($diffs as $diff) { * $changesets = $diff->loadRelatives( * new DifferentialChangeset(), * 'diffID'); * foreach ($changesets as $changeset) { * $hunks = $changeset->loadRelatives( * new DifferentialHunk(), * 'changesetID'); * // Do something with hunks. * } * } * * This code will execute just three queries - one to load all diffs, one to * load all their related changesets and one to load all their related hunks. * You can try to write an equivalent code without using this method as * a homework. * * The method also supports retrieving referenced objects, for example authors * of all diffs (using shortcut @{method:loadOneRelative}): * * foreach ($diffs as $diff) { * $author = $diff->loadOneRelative( * new PhabricatorUser(), * 'phid', * 'getAuthorPHID'); * // Do something with author. * } * * It is also possible to specify additional conditions for the `WHERE` * clause. Similarly to @{method:loadAllWhere}, you can specify everything * after `WHERE` (except `LIMIT`). Contrary to @{method:loadAllWhere}, it is * allowed to pass only a constant string (`%` doesn't have a special * meaning). This is intentional to avoid mistakes with using data from one * row in retrieving other rows. Example of a correct usage: * * $status = $author->loadOneRelative( * new PhabricatorCalendarEvent(), * 'userPHID', * 'getPHID', * '(UNIX_TIMESTAMP() BETWEEN dateFrom AND dateTo)'); * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return list Objects of type $object. * * @task load */ public function loadRelatives( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { if (!$this->inSet) { id(new LiskDAOSet())->addToSet($this); } $relatives = $this->inSet->loadRelatives( $object, $foreign_column, $key_method, $where); return idx($relatives, $this->$key_method(), array()); } /** * Load referenced row. See @{method:loadRelatives} for details. * * @param LiskDAO Type of objects to load. * @param string Name of the column in target table. * @param string Method name in this table. * @param string Additional constraints on returned rows. It supports no * placeholders and requires putting the WHERE part into * parentheses. It's not possible to use LIMIT. * @return LiskDAO Object of type $object or null if there's no such object. * * @task load */ final public function loadOneRelative( LiskDAO $object, $foreign_column, $key_method = 'getID', $where = '') { $relatives = $this->loadRelatives( $object, $foreign_column, $key_method, $where); if (!$relatives) { return null; } if (count($relatives) > 1) { throw new AphrontQueryCountException( 'More than 1 result from loadOneRelative()!'); } return reset($relatives); } final public function putInSet(LiskDAOSet $set) { $this->inSet = $set; return $this; } final protected function getInSet() { return $this->inSet; } /* -( Examining Objects )-------------------------------------------------- */ /** * Set unique ID identifying this object. You normally don't need to call this * method unless with `IDS_MANUAL`. * * @param mixed Unique ID. * @return this * @task save */ public function setID($id) { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } $this->$id_key = $id; return $this; } /** * Retrieve the unique ID identifying this object. This value will be null if * the object hasn't been persisted and you didn't set it manually. * * @return mixed Unique ID. * * @task info */ public function getID() { static $id_key = null; if ($id_key === null) { $id_key = $this->getIDKeyForUse(); } return $this->$id_key; } public function getPHID() { return $this->phid; } /** * Test if a property exists. * * @param string Property name. * @return bool True if the property exists. * @task info */ public function hasProperty($property) { return (bool)$this->checkProperty($property); } /** * Retrieve a list of all object properties. This list only includes * properties that are declared as protected, and it is expected that * all properties returned by this function should be persisted to the * database. * Properties that should not be persisted must be declared as private. * * @return dict Dictionary of normalized (lowercase) to canonical (original * case) property names. * * @task info */ protected function getProperties() { static $properties = null; if (!isset($properties)) { $class = new ReflectionClass(get_class($this)); $properties = array(); foreach ($class->getProperties(ReflectionProperty::IS_PROTECTED) as $p) { $properties[strtolower($p->getName())] = $p->getName(); } $id_key = $this->getIDKey(); if ($id_key != 'id') { unset($properties['id']); } if (!$this->getConfigOption(self::CONFIG_TIMESTAMPS)) { unset($properties['datecreated']); unset($properties['datemodified']); } if ($id_key != 'phid' && !$this->getConfigOption(self::CONFIG_AUX_PHID)) { unset($properties['phid']); } } return $properties; } /** * Check if a property exists on this object. * * @return string|null Canonical property name, or null if the property * does not exist. * * @task info */ protected function checkProperty($property) { static $properties = null; if ($properties === null) { $properties = $this->getProperties(); } $property = strtolower($property); if (empty($properties[$property])) { return null; } return $properties[$property]; } /** * Get or build the database connection for this object. * * @param string 'r' for read, 'w' for read/write. * @param bool True to force a new connection. The connection will not * be retrieved from or saved into the connection cache. * @return LiskDatabaseConnection Lisk connection object. * * @task info */ public function establishConnection($mode, $force_new = false) { if ($mode != 'r' && $mode != 'w') { throw new Exception("Unknown mode '{$mode}', should be 'r' or 'w'."); } if (self::shouldIsolateAllLiskEffectsToCurrentProcess()) { $mode = 'isolate-'.$mode; $connection = $this->getEstablishedConnection($mode); if (!$connection) { $connection = $this->establishIsolatedConnection($mode); $this->setEstablishedConnection($mode, $connection); } return $connection; } if (self::shouldIsolateAllLiskEffectsToTransactions()) { // If we're doing fixture transaction isolation, force the mode to 'w' // so we always get the same connection for reads and writes, and thus // can see the writes inside the transaction. $mode = 'w'; } // TODO: There is currently no protection on 'r' queries against writing. $connection = null; if (!$force_new) { if ($mode == 'r') { // If we're requesting a read connection but already have a write // connection, reuse the write connection so that reads can take place // inside transactions. $connection = $this->getEstablishedConnection('w'); } if (!$connection) { $connection = $this->getEstablishedConnection($mode); } } if (!$connection) { $connection = $this->establishLiveConnection($mode); if (self::shouldIsolateAllLiskEffectsToTransactions()) { $connection->openTransaction(); } $this->setEstablishedConnection( $mode, $connection, $force_unique = $force_new); } return $connection; } /** * Convert this object into a property dictionary. This dictionary can be - * restored into an object by using loadFromArray() (unless you're using - * legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you should - * just go ahead and die in a fire). + * restored into an object by using @{method:loadFromArray} (unless you're + * using legacy features with CONFIG_CONVERT_CAMELCASE, but in that case you + * should just go ahead and die in a fire). * * @return dict Dictionary of object properties. * * @task info */ protected function getPropertyValues() { $map = array(); foreach ($this->getProperties() as $p) { // We may receive a warning here for properties we've implicitly added // through configuration; squelch it. $map[$p] = @$this->$p; } return $map; } /* -( Writing Objects )---------------------------------------------------- */ /** * Make an object read-only. * * Making an object ephemeral indicates that you will be changing state in * such a way that you would never ever want it to be written back to the * storage. */ public function makeEphemeral() { $this->ephemeral = true; return $this; } private function isEphemeralCheck() { if ($this->ephemeral) { throw new LiskEphemeralObjectException(); } } /** * Persist this object to the database. In most cases, this is the only * method you need to call to do writes. If the object has not yet been * inserted this will do an insert; if it has, it will do an update. * * @return this * * @task save */ public function save() { if ($this->shouldInsertWhenSaved()) { return $this->insert(); } else { return $this->update(); } } /** * Save this object, forcing the query to use REPLACE regardless of object * state. * * @return this * * @task save */ public function replace() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('REPLACE'); } /** * Save this object, forcing the query to use INSERT regardless of object * state. * * @return this * * @task save */ public function insert() { $this->isEphemeralCheck(); return $this->insertRecordIntoDatabase('INSERT'); } /** * Save this object, forcing the query to use UPDATE regardless of object * state. * * @return this * * @task save */ public function update() { $this->isEphemeralCheck(); $this->willSaveObject(); $data = $this->getPropertyValues(); if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) { $data = array_intersect_key($data, $this->dirtyFields); } $this->willWriteData($data); $map = array(); foreach ($data as $k => $v) { $map[$k] = $v; } $conn = $this->establishConnection('w'); $binary = $this->getBinaryColumns(); foreach ($map as $key => $value) { if (!empty($binary[$key])) { $map[$key] = qsprintf($conn, '%C = %nB', $key, $value); } else { $map[$key] = qsprintf($conn, '%C = %ns', $key, $value); } } $map = implode(', ', $map); $id = $this->getID(); $conn->query( 'UPDATE %T SET %Q WHERE %C = '.(is_int($id) ? '%d' : '%s'), $this->getTableName(), $map, $this->getIDKeyForUse(), $id); // We can't detect a missing object because updating an object without // changing any values doesn't affect rows. We could jiggle timestamps // to catch this for objects which track them if we wanted. $this->didWriteData(); if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) { $this->resetDirtyFields(); } return $this; } /** * Delete this object, permanently. * * @return this * * @task save */ public function delete() { $this->isEphemeralCheck(); $this->willDelete(); $conn = $this->establishConnection('w'); $conn->query( 'DELETE FROM %T WHERE %C = %d', $this->getTableName(), $this->getIDKeyForUse(), $this->getID()); $this->didDelete(); return $this; } /** * Internal implementation of INSERT and REPLACE. * * @param const Either "INSERT" or "REPLACE", to force the desired mode. * * @task save */ protected function insertRecordIntoDatabase($mode) { $this->willSaveObject(); $data = $this->getPropertyValues(); $conn = $this->establishConnection('w'); $id_mechanism = $this->getConfigOption(self::CONFIG_IDS); switch ($id_mechanism) { case self::IDS_AUTOINCREMENT: // If we are using autoincrement IDs, let MySQL assign the value for the // ID column, if it is empty. If the caller has explicitly provided a // value, use it. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { unset($data[$id_key]); } break; case self::IDS_COUNTER: // If we are using counter IDs, assign a new ID if we don't already have // one. $id_key = $this->getIDKeyForUse(); if (empty($data[$id_key])) { $counter_name = $this->getTableName(); $id = self::loadNextCounterID($conn, $counter_name); $this->setID($id); $data[$id_key] = $id; } break; case self::IDS_MANUAL: break; default: throw new Exception('Unknown CONFIG_IDs mechanism!'); } $this->willWriteData($data); $columns = array_keys($data); $binary = $this->getBinaryColumns(); foreach ($data as $key => $value) { try { if (!empty($binary[$key])) { $data[$key] = qsprintf($conn, '%nB', $value); } else { $data[$key] = qsprintf($conn, '%ns', $value); } } catch (AphrontQueryParameterException $parameter_exception) { throw new PhutilProxyException( pht( "Unable to insert or update object of class %s, field '%s' ". "has a nonscalar value.", get_class($this), $key), $parameter_exception); } } $data = implode(', ', $data); $conn->query( '%Q INTO %T (%LC) VALUES (%Q)', $mode, $this->getTableName(), $columns, $data); // Only use the insert id if this table is using auto-increment ids if ($id_mechanism === self::IDS_AUTOINCREMENT) { $this->setID($conn->getInsertID()); } $this->didWriteData(); if ($this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS)) { $this->resetDirtyFields(); } return $this; } /** * Method used to determine whether to insert or update when saving. * * @return bool true if the record should be inserted */ protected function shouldInsertWhenSaved() { $key_type = $this->getConfigOption(self::CONFIG_IDS); if ($key_type == self::IDS_MANUAL) { throw new Exception( 'You are using manual IDs. You must override the '. 'shouldInsertWhenSaved() method to properly detect '. 'when to insert a new record.'); } else { return !$this->getID(); } } /* -( Hooks and Callbacks )------------------------------------------------ */ /** * Retrieve the database table name. By default, this is the class name. * * @return string Table name for object storage. * * @task hook */ public function getTableName() { return get_class($this); } /** * Retrieve the primary key column, "id" by default. If you can not * reasonably name your ID column "id", override this method. * * @return string Name of the ID column. * * @task hook */ public function getIDKey() { return 'id'; } protected function getIDKeyForUse() { $id_key = $this->getIDKey(); if (!$id_key) { throw new Exception( 'This DAO does not have a single-part primary key. The method you '. 'called requires a single-part primary key.'); } return $id_key; } /** * Generate a new PHID, used by CONFIG_AUX_PHID. * * @return phid Unique, newly allocated PHID. * * @task hook */ protected function generatePHID() { throw new Exception( 'To use CONFIG_AUX_PHID, you need to overload '. 'generatePHID() to perform PHID generation.'); } /** * Hook to apply serialization or validation to data before it is written to - * the database. See also willReadData(). + * the database. See also @{method:willReadData}. * * @task hook */ protected function willWriteData(array &$data) { $this->applyLiskDataSerialization($data, false); } /** * Hook to perform actions after data has been written to the database. * * @task hook */ protected function didWriteData() {} /** * Hook to make internal object state changes prior to INSERT, REPLACE or * UPDATE. * * @task hook */ protected function willSaveObject() { $use_timestamps = $this->getConfigOption(self::CONFIG_TIMESTAMPS); if ($use_timestamps) { if (!$this->getDateCreated()) { $this->setDateCreated(time()); } $this->setDateModified(time()); } if ($this->getConfigOption(self::CONFIG_AUX_PHID) && !$this->getPHID()) { $this->setPHID($this->generatePHID()); } } /** * Hook to apply serialization or validation to data as it is read from the - * database. See also willWriteData(). + * database. See also @{method:willWriteData}. * * @task hook */ protected function willReadData(array &$data) { $this->applyLiskDataSerialization($data, $deserialize = true); } /** * Hook to perform an action on data after it is read from the database. * * @task hook */ protected function didReadData() {} /** * Hook to perform an action before the deletion of an object. * * @task hook */ protected function willDelete() {} /** * Hook to perform an action after the deletion of an object. * * @task hook */ protected function didDelete() {} /** * Reads the value from a field. Override this method for custom behavior - * of getField() instead of overriding getField directly. + * of @{method:getField} instead of overriding getField directly. * * @param string Canonical field name * @return mixed Value of the field * * @task hook */ protected function readField($field) { if (isset($this->$field)) { return $this->$field; } return null; } /** * Writes a value to a field. Override this method for custom behavior of * setField($value) instead of overriding setField directly. * * @param string Canonical field name * @param mixed Value to write * * @task hook */ protected function writeField($field, $value) { $this->$field = $value; } /* -( Manging Transactions )----------------------------------------------- */ /** * Increase transaction stack depth. * * @return this */ public function openTransaction() { $this->establishConnection('w')->openTransaction(); return $this; } /** * Decrease transaction stack depth, saving work. * * @return this */ public function saveTransaction() { $this->establishConnection('w')->saveTransaction(); return $this; } /** * Decrease transaction stack depth, discarding work. * * @return this */ public function killTransaction() { $this->establishConnection('w')->killTransaction(); return $this; } /** * Begins read-locking selected rows with SELECT ... FOR UPDATE, so that * other connections can not read them (this is an enormous oversimplification * of FOR UPDATE semantics; consult the MySQL documentation for details). To * end read locking, call @{method:endReadLocking}. For example: * * $beach->openTransaction(); * $beach->beginReadLocking(); * * $beach->reload(); * $beach->setGrainsOfSand($beach->getGrainsOfSand() + 1); * $beach->save(); * * $beach->endReadLocking(); * $beach->saveTransaction(); * * @return this * @task xaction */ public function beginReadLocking() { $this->establishConnection('w')->beginReadLocking(); return $this; } /** * Ends read-locking that began at an earlier @{method:beginReadLocking} call. * * @return this * @task xaction */ public function endReadLocking() { $this->establishConnection('w')->endReadLocking(); return $this; } /** * Begins write-locking selected rows with SELECT ... LOCK IN SHARE MODE, so * that other connections can not update or delete them (this is an * oversimplification of LOCK IN SHARE MODE semantics; consult the * MySQL documentation for details). To end write locking, call * @{method:endWriteLocking}. * * @return this * @task xaction */ public function beginWriteLocking() { $this->establishConnection('w')->beginWriteLocking(); return $this; } /** * Ends write-locking that began at an earlier @{method:beginWriteLocking} * call. * * @return this * @task xaction */ public function endWriteLocking() { $this->establishConnection('w')->endWriteLocking(); return $this; } /* -( Isolation )---------------------------------------------------------- */ /** * @task isolate */ public static function beginIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToCurrentProcess() { self::$processIsolationLevel--; if (self::$processIsolationLevel < 0) { throw new Exception( 'Lisk process isolation level was reduced below 0.'); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToCurrentProcess() { return (bool)self::$processIsolationLevel; } /** * @task isolate */ private function establishIsolatedConnection($mode) { $config = array(); return new AphrontIsolatedDatabaseConnection($config); } /** * @task isolate */ public static function beginIsolateAllLiskEffectsToTransactions() { if (self::$transactionIsolationLevel === 0) { self::closeAllConnections(); } self::$transactionIsolationLevel++; } /** * @task isolate */ public static function endIsolateAllLiskEffectsToTransactions() { self::$transactionIsolationLevel--; if (self::$transactionIsolationLevel < 0) { throw new Exception( 'Lisk transaction isolation level was reduced below 0.'); } else if (self::$transactionIsolationLevel == 0) { foreach (self::$connections as $key => $conn) { if ($conn) { $conn->killTransaction(); } } self::closeAllConnections(); } } /** * @task isolate */ public static function shouldIsolateAllLiskEffectsToTransactions() { return (bool)self::$transactionIsolationLevel; } public static function closeAllConnections() { self::$connections = array(); } /* -( Utilities )---------------------------------------------------------- */ /** * Applies configured serialization to a dictionary of values. * * @task util */ protected function applyLiskDataSerialization(array &$data, $deserialize) { $serialization = $this->getConfigOption(self::CONFIG_SERIALIZATION); if ($serialization) { foreach (array_intersect_key($serialization, $data) as $col => $format) { switch ($format) { case self::SERIALIZATION_NONE: break; case self::SERIALIZATION_PHP: if ($deserialize) { $data[$col] = unserialize($data[$col]); } else { $data[$col] = serialize($data[$col]); } break; case self::SERIALIZATION_JSON: if ($deserialize) { $data[$col] = json_decode($data[$col], true); } else { $data[$col] = json_encode($data[$col]); } break; default: throw new Exception("Unknown serialization format '{$format}'."); } } } } /** * Resets the dirty fields (fields which need to be written on next save/ * update/insert/replace). If this DAO has timestamps, the modified time * is always a dirty field. * * @task util */ private function resetDirtyFields() { $this->dirtyFields = array(); if ($this->getConfigOption(self::CONFIG_TIMESTAMPS)) { $this->dirtyFields['dateModified'] = true; } } /** * Black magic. Builds implied get*() and set*() for all properties. * * @param string Method name. * @param list Argument vector. * @return mixed get*() methods return the property value. set*() methods * return $this. * @task util */ public function __call($method, $args) { - // NOTE: PHP has a bug that static variables defined in __call() are shared // across all children classes. Call a different method to work around this // bug. return $this->call($method, $args); } /** * @task util */ final protected function call($method, $args) { // NOTE: This method is very performance-sensitive (many thousands of calls // per page on some pages), and thus has some silliness in the name of // optimizations. static $dispatch_map = array(); static $partial = null; if ($partial === null) { $partial = $this->getConfigOption(self::CONFIG_PARTIAL_OBJECTS); } if ($method[0] === 'g') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'get') { throw new Exception("Unable to resolve method '{$method}'!"); } $property = substr($method, 3); if (!($property = $this->checkProperty($property))) { throw new Exception("Bad getter call: {$method}"); } $dispatch_map[$method] = $property; } if ($partial && isset($this->missingFields[$property])) { throw new Exception("Cannot get field that wasn't loaded: {$property}"); } return $this->readField($property); } if ($method[0] === 's') { if (isset($dispatch_map[$method])) { $property = $dispatch_map[$method]; } else { if (substr($method, 0, 3) !== 'set') { throw new Exception("Unable to resolve method '{$method}'!"); } $property = substr($method, 3); $property = $this->checkProperty($property); if (!$property) { throw new Exception("Bad setter call: {$method}"); } $dispatch_map[$method] = $property; } if ($partial) { // Accept writes to fields that weren't initially loaded unset($this->missingFields[$property]); $this->dirtyFields[$property] = true; } $this->writeField($property, $args[0]); return $this; } throw new Exception("Unable to resolve method '{$method}'."); } /** * Warns against writing to undeclared property. * * @task util */ public function __set($name, $value) { phlog('Wrote to undeclared property '.get_class($this).'::$'.$name.'.'); $this->$name = $value; } /** * Increments a named counter and returns the next value. * * @param AphrontDatabaseConnection Database where the counter resides. * @param string Counter name to create or increment. * @return int Next counter value. * * @task util */ public static function loadNextCounterID( AphrontDatabaseConnection $conn_w, $counter_name) { // NOTE: If an insert does not touch an autoincrement row or call // LAST_INSERT_ID(), MySQL normally does not change the value of // LAST_INSERT_ID(). This can cause a counter's value to leak to a // new counter if the second counter is created after the first one is // updated. To avoid this, we insert LAST_INSERT_ID(1), to ensure the // LAST_INSERT_ID() is always updated and always set correctly after the // query completes. queryfx( $conn_w, 'INSERT INTO %T (counterName, counterValue) VALUES (%s, LAST_INSERT_ID(1)) ON DUPLICATE KEY UPDATE counterValue = LAST_INSERT_ID(counterValue + 1)', self::COUNTER_TABLE_NAME, $counter_name); return $conn_w->getInsertID(); } private function getBinaryColumns() { return $this->getConfigOption(self::CONFIG_BINARY); } } diff --git a/webroot/rsrc/externals/javelin/core/Event.js b/webroot/rsrc/externals/javelin/core/Event.js index b7b5a7b3b2..9ce02c5811 100644 --- a/webroot/rsrc/externals/javelin/core/Event.js +++ b/webroot/rsrc/externals/javelin/core/Event.js @@ -1,347 +1,346 @@ /** * @requires javelin-install * @provides javelin-event * @javelin */ /** * A generic event, routed by @{class:JX.Stratcom}. All events within Javelin * are represented by a {@class:JX.Event}, regardless of whether they originate * from a native DOM event (like a mouse click) or are custom application * events. * * See @{article:Concepts: Event Delegation} for an introduction to Javelin's * event delegation model. * * Events have a propagation model similar to native Javascript events, in that * they can be stopped with stop() (which stops them from continuing to * propagate to other handlers) or prevented with prevent() (which prevents them * from taking their default action, like following a link). You can do both at * once with kill(). * * @task stop Stopping Event Behaviors * @task info Getting Event Information - * @group event */ JX.install('Event', { members : { /** * Stop an event from continuing to propagate. No other handler will * receive this event, but its default behavior will still occur. See * ""Using Events"" for more information on the distinction between * 'stopping' and 'preventing' an event. See also prevent() (which prevents * an event but does not stop it) and kill() (which stops and prevents an * event). * * @return this * @task stop */ stop : function() { var r = this.getRawEvent(); if (r) { r.cancelBubble = true; r.stopPropagation && r.stopPropagation(); } this.setStopped(true); return this; }, /** * Prevent an event's default action. This depends on the event type, but * the common default actions are following links, submitting forms, * and typing text. Event prevention is generally used when you have a link * or form which work properly without Javascript but have a specialized * Javascript behavior. When you intercept the event and make the behavior * occur, you prevent it to keep the browser from following the link. * * Preventing an event does not stop it from propagating, so other handlers * will still receive it. See ""Using Events"" for more information on the * distinction between 'stopping' and 'preventing' an event. See also * stop() (which stops an event but does not prevent it) and kill() * (which stops and prevents an event). * * @return this * @task stop */ prevent : function() { var r = this.getRawEvent(); if (r) { r.returnValue = false; r.preventDefault && r.preventDefault(); } this.setPrevented(true); return this; }, /** * Stop and prevent an event, which stops it from propagating and prevents * its defualt behavior. This is a convenience function, see stop() and * prevent() for information on what it means to stop or prevent an event. * * @return this * @task stop */ kill : function() { this.prevent(); this.stop(); return this; }, /** * Get the special key (like tab or return), if any, associated with this * event. Browsers report special keys differently; this method allows you * to identify a keypress in a browser-agnostic way. Note that this detects * only some special keys: delete, tab, return escape, left, up, right, * down. * * For example, if you want to react to the escape key being pressed, you * could install a listener like this: * * JX.Stratcom.listen('keydown', 'example', function(e) { * if (e.getSpecialKey() == 'esc') { * JX.log("You pressed 'Escape'! Well done! Bravo!"); * } * }); * * @return string|null ##null## if there is no associated special key, * or one of the strings 'delete', 'tab', 'return', * 'esc', 'left', 'up', 'right', or 'down'. * @task info */ getSpecialKey : function() { var r = this.getRawEvent(); if (!r) { return null; } return JX.Event._keymap[r.keyCode] || null; }, /** * Get whether the mouse button associated with the mouse event is the * right-side button in a browser-agnostic way. * * @return bool * @task info */ isRightButton : function() { var r = this.getRawEvent(); return r.which == 3 || r.button == 2; }, /** * Determine if a mouse event is a normal event (left mouse button, no * modifier keys). * * @return bool * @task info */ isNormalMouseEvent : function() { var supportedEvents = {'click': 1, 'mouseup': 1, 'mousedown': 1}; if (!(this.getType() in supportedEvents)) { return false; } var r = this.getRawEvent(); if (r.metaKey || r.altKey || r.ctrlKey || r.shiftKey) { return false; } if (('which' in r) && (r.which != 1)) { return false; } if (('button' in r) && r.button) { if ('which' in r) { return false; // IE won't have which and has left click == 1 here } else if (r.button != 1) { return false; } } return true; }, /** * Determine if a click event is a normal click (left mouse button, no * modifier keys). * * @return bool * @task info */ isNormalClick : function() { if (this.getType() != 'click') { return false; } return this.isNormalMouseEvent(); }, /** * Get the node corresponding to the specified key in this event's node map. * This is a simple helper method that makes the API for accessing nodes * less ugly. * * JX.Stratcom.listen('click', 'tag:a', function(e) { * var a = e.getNode('tag:a'); * // do something with the link that was clicked * }); * * @param string sigil or stratcom node key * @return node|null Node mapped to the specified key, or null if it the * key does not exist. The available keys include: * - 'tag:'+tag - first node of each type * - 'id:'+id - all nodes with an id * - sigil - first node of each sigil * @task info */ getNode : function(key) { return this.getNodes()[key] || null; }, /** * Get the metadata associated with the node that corresponds to the key - * in this event's node map. This is a simple helper method that makes + * in this event's node map. This is a simple helper method that makes * the API for accessing metadata associated with specific nodes less ugly. * * JX.Stratcom.listen('click', 'tag:a', function(event) { * var anchorData = event.getNodeData('tag:a'); * // do something with the metadata of the link that was clicked * }); * * @param string sigil or stratcom node key * @return dict dictionary of the node's metadata * @task info */ getNodeData : function(key) { // Evade static analysis - JX.Stratcom return JX['Stratcom'].getData(this.getNode(key)); } }, statics : { _keymap : { 8 : 'delete', 9 : 'tab', // On Windows and Linux, Chrome sends '10' for return. On Mac OS X, it // sends 13. Other browsers evidence varying degrees of diversity in their // behavior. Treat '10' and '13' identically. 10 : 'return', 13 : 'return', 27 : 'esc', 37 : 'left', 38 : 'up', 39 : 'right', 40 : 'down', 63232 : 'up', 63233 : 'down', 62234 : 'left', 62235 : 'right' } }, properties : { /** * Native Javascript event which generated this @{class:JX.Event}. Not every * event is generated by a native event, so there may be ##null## in * this field. * * @type Event|null * @task info */ rawEvent : null, /** * String describing the event type, like 'click' or 'mousedown'. This * may also be an application or object event. * * @type string * @task info */ type : null, /** * If available, the DOM node where this event occurred. For example, if * this event is a click on a button, the target will be the button which * was clicked. Application events will not have a target, so this property * will return the value ##null##. * * @type DOMNode|null * @task info */ target : null, /** * Metadata attached to nodes associated with this event. * * For native events, the DOM is walked from the event target to the root * element. Each sigil which is encountered while walking up the tree is * added to the map as a key. If the node has associated metainformation, * it is set as the value; otherwise, the value is null. * * @type dict * @task info */ data : null, /** * Sigil path this event was activated from. TODO: explain this * * @type list * @task info */ path : [], /** * True if propagation of the event has been stopped. See stop(). * * @type bool * @task stop */ stopped : false, /** * True if default behavior of the event has been prevented. See prevent(). * * @type bool * @task stop */ prevented : false, /** * @task info */ nodes : {}, /** * @task info */ nodeDistances : {} }, /** * @{class:JX.Event} installs a toString() method in ##__DEV__## which allows * you to log or print events and get a reasonable representation of them: * * Event<'click', ['path', 'stuff'], [object HTMLDivElement]> */ initialize : function() { if (__DEV__) { JX.Event.prototype.toString = function() { var path = '['+this.getPath().join(', ')+']'; return 'Event<'+this.getType()+', '+path+', '+this.getTarget()+'>'; }; } } }); diff --git a/webroot/rsrc/externals/javelin/core/Stratcom.js b/webroot/rsrc/externals/javelin/core/Stratcom.js index dc62c852de..28eff44c12 100644 --- a/webroot/rsrc/externals/javelin/core/Stratcom.js +++ b/webroot/rsrc/externals/javelin/core/Stratcom.js @@ -1,646 +1,645 @@ /** * @requires javelin-install javelin-event javelin-util javelin-magical-init * @provides javelin-stratcom * @javelin */ /** * Javelin strategic command, the master event delegation core. This class is * a sort of hybrid between Arbiter and traditional event delegation, and * serves to route event information to handlers in a general way. * * Each Javelin :JX.Event has a 'type', which may be a normal Javascript type * (for instance, a click or a keypress) or an application-defined type. It * also has a "path", based on the path in the DOM from the root node to the * event target. Note that, while the type is required, the path may be empty * (it often will be for application-defined events which do not originate * from the DOM). * * The path is determined by walking down the tree to the event target and * looking for nodes that have been tagged with metadata. These names are used * to build the event path, and unnamed nodes are ignored. Each named node may * also have data attached to it. * * Listeners specify one or more event types they are interested in handling, * and, optionally, one or more paths. A listener will only receive events * which occurred on paths it is listening to. See listen() for more details. * * @task invoke Invoking Events * @task listen Listening to Events * @task handle Responding to Events * @task sigil Managing Sigils * @task meta Managing Metadata * @task internal Internals - * @group event */ JX.install('Stratcom', { statics : { ready : false, _targets : {}, _handlers : [], _need : {}, _auto : '*', _data : {}, _execContext : [], /** * Node metadata is stored in a series of blocks to prevent collisions * between indexes that are generated on the server side (and potentially * concurrently). Block 0 is for metadata on the initial page load, block 1 * is for metadata added at runtime with JX.Stratcom.siglize(), and blocks * 2 and up are for metadata generated from other sources (e.g. JX.Request). * Use allocateMetadataBlock() to reserve a block, and mergeData() to fill * a block with data. * * When a JX.Request is sent, a block is allocated for it and any metadata * it returns is filled into that block. */ _dataBlock : 2, /** * Within each datablock, data is identified by a unique index. The data * pointer (data-meta attribute) on a node looks like this: * * 1_2 * * ...where 1 is the block, and 2 is the index within that block. Normally, * blocks are filled on the server side, so index allocation takes place * there. However, when data is provided with JX.Stratcom.addData(), we * need to allocate indexes on the client. */ _dataIndex : 0, /** * Dispatch a simple event that does not have a corresponding native event * object. It is unusual to call this directly. Generally, you will instead * dispatch events from an object using the invoke() method present on all * objects. See @{JX.Base.invoke()} for documentation. * * @param string Event type. * @param string|list? Optionally, a sigil path to attach to the event. * This is rarely meaningful for simple events. * @param object? Optionally, arbitrary data to send with the event. * @return @{JX.Event} The event object which was dispatched to listeners. * The main use of this is to test whether any * listeners prevented the event. * @task invoke */ invoke : function(type, path, data) { if (__DEV__) { if (path && typeof path !== 'string' && !JX.isArray(path)) { throw new Error( 'JX.Stratcom.invoke(...): path must be a string or an array.'); } } path = JX.$AX(path); return this._dispatchProxy( new JX.Event() .setType(type) .setData(data || {}) .setPath(path || []) ); }, /** * Listen for events on given paths. Specify one or more event types, and * zero or more paths to filter on. If you don't specify a path, you will * receive all events of the given type: * * // Listen to all clicks. * JX.Stratcom.listen('click', null, handler); * * This will notify you of all clicks anywhere in the document (unless * they are intercepted and killed by a higher priority handler before they * get to you). * * Often, you may be interested in only clicks on certain elements. You * can specify the paths you're interested in to filter out events which * you do not want to be notified of. * * // Listen to all clicks inside elements annotated "news-feed". * JX.Stratcom.listen('click', 'news-feed', handler); * * By adding more elements to the path, you can create a finer-tuned * filter: * * // Listen to only "like" clicks inside "news-feed". * JX.Stratcom.listen('click', ['news-feed', 'like'], handler); * * * TODO: Further explain these shenanigans. * * @param string|list Event type (or list of event names) to * listen for. For example, ##'click'## or * ##['keydown', 'keyup']##. * * @param wild Sigil paths to listen for this event on. See discussion * in method documentation. * * @param function Callback to invoke when this event is triggered. It * should have the signature ##f(:JX.Event e)##. * * @return object A reference to the installed listener. You can later * remove the listener by calling this object's remove() * method. * @task listen */ listen : function(types, paths, func) { if (__DEV__) { if (arguments.length != 3) { JX.$E( 'JX.Stratcom.listen(...): '+ 'requires exactly 3 arguments. Did you mean JX.DOM.listen?'); } if (typeof func != 'function') { JX.$E( 'JX.Stratcom.listen(...): '+ 'callback is not a function.'); } } var ids = []; types = JX.$AX(types); if (!paths) { paths = this._auto; } if (!JX.isArray(paths)) { paths = [[paths]]; } else if (!JX.isArray(paths[0])) { paths = [paths]; } var listener = { _callback : func }; // To listen to multiple event types on multiple paths, we just install // the same listener a whole bunch of times: if we install for two // event types on three paths, we'll end up with six references to the // listener. // // TODO: we'll call your listener twice if you install on two paths where // one path is a subset of another. The solution is "don't do that", but // it would be nice to verify that the caller isn't doing so, in __DEV__. for (var ii = 0; ii < types.length; ++ii) { var type = types[ii]; if (('onpagehide' in window) && type == 'unload') { // If we use "unload", we break the bfcache ("Back-Forward Cache") in // Safari and Firefox. The BFCache makes using the back/forward // buttons really fast since the pages can come out of magical // fairyland instead of over the network, so use "pagehide" as a proxy // for "unload" in these browsers. type = 'pagehide'; } if (!(type in this._targets)) { this._targets[type] = {}; } var type_target = this._targets[type]; for (var jj = 0; jj < paths.length; ++jj) { var path = paths[jj]; var id = this._handlers.length; this._handlers.push(listener); this._need[id] = path.length; ids.push(id); for (var kk = 0; kk < path.length; ++kk) { if (__DEV__) { if (path[kk] == 'tag:#document') { JX.$E( 'JX.Stratcom.listen(..., "tag:#document", ...): ' + 'listen for all events using null, not "tag:#document"'); } if (path[kk] == 'tag:window') { JX.$E( 'JX.Stratcom.listen(..., "tag:window", ...): ' + 'listen for window events using null, not "tag:window"'); } } (type_target[path[kk]] || (type_target[path[kk]] = [])).push(id); } } } // Add a remove function to the listener listener['remove'] = function() { if (listener._callback) { delete listener._callback; for (var ii = 0; ii < ids.length; ii++) { delete JX.Stratcom._handlers[ids[ii]]; } } }; return listener; }, /** * Sometimes you may be interested in removing a listener directly from it's * handler. This is possible by calling JX.Stratcom.removeCurrentListener() * * // Listen to only the first click on the page * JX.Stratcom.listen('click', null, function() { * // do interesting things * JX.Stratcom.removeCurrentListener(); * }); * * @task remove */ removeCurrentListener : function() { var context = this._execContext[this._execContext.length - 1]; var listeners = context.listeners; // JX.Stratcom.pass will have incremented cursor by now var cursor = context.cursor - 1; if (listeners[cursor]) { listeners[cursor].handler.remove(); } }, /** * Dispatch a native Javascript event through the Stratcom control flow. * Generally, this is automatically called for you by the master dispatcher * installed by ##init.js##. When you want to dispatch an application event, * you should instead call invoke(). * * @param Event Native event for dispatch. * @return :JX.Event Dispatched :JX.Event. * @task internal */ dispatch : function(event) { var path = []; var nodes = {}; var distances = {}; var push = function(key, node, distance) { // we explicitly only store the first occurrence of each key if (!nodes.hasOwnProperty(key)) { nodes[key] = node; distances[key] = distance; path.push(key); } }; var target = event.srcElement || event.target; // Touch events may originate from text nodes, but we want to start our // traversal from the nearest Element, so we grab the parentNode instead. if (target && target.nodeType === 3) { target = target.parentNode; } // Since you can only listen by tag, id, or sigil we unset the target if // it isn't an Element. Document and window are Nodes but not Elements. if (!target || !target.getAttribute) { target = null; } var distance = 1; var cursor = target; while (cursor && cursor.getAttribute) { push('tag:' + cursor.nodeName.toLowerCase(), cursor, distance); var id = cursor.id; if (id) { push('id:' + id, cursor, distance); } var sigils = cursor.getAttribute('data-sigil'); if (sigils) { sigils = sigils.split(' '); for (var ii = 0; ii < sigils.length; ii++) { push(sigils[ii], cursor, distance); } } var auto_id = cursor.getAttribute('data-autoid'); if (auto_id) { push('autoid:' + auto_id, cursor, distance); } ++distance; cursor = cursor.parentNode; } var etype = event.type; if (etype == 'focusin') { etype = 'focus'; } else if (etype == 'focusout') { etype = 'blur'; } var proxy = new JX.Event() .setRawEvent(event) .setData(event.customData) .setType(etype) .setTarget(target) .setNodes(nodes) .setNodeDistances(distances) .setPath(path.reverse()); // Don't touch this for debugging purposes //JX.log('~> '+proxy.toString()); return this._dispatchProxy(proxy); }, /** * Dispatch a previously constructed proxy :JX.Event. * * @param :JX.Event Event to dispatch. * @return :JX.Event Returns the event argument. * @task internal */ _dispatchProxy : function(proxy) { var scope = this._targets[proxy.getType()]; if (!scope) { return proxy; } var path = proxy.getPath(); var distances = proxy.getNodeDistances(); var len = path.length; var hits = {}; var hit_distances = {}; var matches; // A large number (larger than any distance we will ever encounter), but // we need to do math on it in the sort function so we can't use // Number.POSITIVE_INFINITY. var far_away = 1000000; for (var root = -1; root < len; ++root) { matches = scope[(root == -1) ? this._auto : path[root]]; if (matches) { var distance = distances[path[root]] || far_away; for (var ii = 0; ii < matches.length; ++ii) { var match = matches[ii]; hits[match] = (hits[match] || 0) + 1; hit_distances[match] = Math.min( hit_distances[match] || distance, distance ); } } } var listeners = []; for (var k in hits) { if (hits[k] == this._need[k]) { var handler = this._handlers[k]; if (handler) { listeners.push({ distance: hit_distances[k], handler: handler }); } } } // Sort listeners by matched sigil closest to the target node // Listeners with the same closest sigil are called in an undefined order listeners.sort(function(a, b) { if (__DEV__) { // Make sure people play by the rules. >:) return (a.distance - b.distance) || (Math.random() - 0.5); } return a.distance - b.distance; }); this._execContext.push({ listeners: listeners, event: proxy, cursor: 0 }); this.pass(); this._execContext.pop(); return proxy; }, /** * Pass on an event, allowing other handlers to process it. The use case * here is generally something like: * * if (JX.Stratcom.pass()) { * // something else handled the event * return; * } * // handle the event * event.prevent(); * * This allows you to install event handlers that operate at a lower * effective priority, and provide a default behavior which is overridable * by listeners. * * @return bool True if the event was stopped or prevented by another * handler. * @task handle */ pass : function() { var context = this._execContext[this._execContext.length - 1]; var event = context.event; var listeners = context.listeners; while (context.cursor < listeners.length) { var cursor = context.cursor++; if (listeners[cursor]) { var handler = listeners[cursor].handler; handler._callback && handler._callback(event); } if (event.getStopped()) { break; } } return event.getStopped() || event.getPrevented(); }, /** * Retrieve the event (if any) which is currently being dispatched. * * @return :JX.Event|null Event which is currently being dispatched, or * null if there is no active dispatch. * @task handle */ context : function() { var len = this._execContext.length; return len ? this._execContext[len - 1].event : null; }, /** * Merge metadata. You must call this (even if you have no metadata) to * start the Stratcom queue. * * @param int The datablock to merge data into. * @param dict Dictionary of metadata. * @return void * @task internal */ mergeData : function(block, data) { if (this._data[block]) { if (__DEV__) { for (var key in data) { if (key in this._data[block]) { JX.$E( 'JX.Stratcom.mergeData(' + block + ', ...); is overwriting ' + 'existing data.'); } } } JX.copy(this._data[block], data); } else { this._data[block] = data; if (block === 0) { JX.Stratcom.ready = true; JX.flushHoldingQueue('install-init', function(fn) { fn(); }); JX.__rawEventQueue({type: 'start-queue'}); } } }, /** * Determine if a node has a specific sigil. * * @param Node Node to test. * @param string Sigil to check for. * @return bool True if the node has the sigil. * * @task sigil */ hasSigil : function(node, sigil) { if (__DEV__) { if (!node || !node.getAttribute) { JX.$E( 'JX.Stratcom.hasSigil(, ...): ' + 'node is not an element. Most likely, you\'re passing window or ' + 'document, which are not elements and can\'t have sigils.'); } } var sigils = node.getAttribute('data-sigil') || false; return sigils && (' ' + sigils + ' ').indexOf(' ' + sigil + ' ') > -1; }, /** * Add a sigil to a node. * * @param Node Node to add the sigil to. * @param string Sigil to name the node with. * @return void * @task sigil */ addSigil: function(node, sigil) { if (__DEV__) { if (!node || !node.getAttribute) { JX.$E( 'JX.Stratcom.addSigil(, ...): ' + 'node is not an element. Most likely, you\'re passing window or ' + 'document, which are not elements and can\'t have sigils.'); } } var sigils = node.getAttribute('data-sigil') || ''; if (!JX.Stratcom.hasSigil(node, sigil)) { sigils += ' ' + sigil; } node.setAttribute('data-sigil', sigils); }, /** * Retrieve a node's metadata. * * @param Node Node from which to retrieve data. * @return object Data attached to the node. If no data has been attached * to the node yet, an empty object will be returned, but * subsequent calls to this method will always retrieve the * same object. * @task meta */ getData : function(node) { if (__DEV__) { if (!node || !node.getAttribute) { JX.$E( 'JX.Stratcom.getData(): ' + 'node is not an element. Most likely, you\'re passing window or ' + 'document, which are not elements and can\'t have data.'); } } var meta_id = (node.getAttribute('data-meta') || '').split('_'); if (meta_id[0] && meta_id[1]) { var block = this._data[meta_id[0]]; var index = meta_id[1]; if (block && (index in block)) { return block[index]; } else if (__DEV__) { JX.$E( 'JX.Stratcom.getData(): Tried to access data (block ' + meta_id[0] + ', index ' + index + ') that was not present. This ' + 'probably means you are calling getData() before the block ' + 'is provided by mergeData().'); } } var data = {}; if (!this._data[1]) { // data block 1 is reserved for JavaScript this._data[1] = {}; } this._data[1][this._dataIndex] = data; node.setAttribute('data-meta', '1_' + (this._dataIndex++)); return data; }, /** * Add data to a node's metadata. * * @param Node Node which data should be attached to. * @param object Data to add to the node's metadata. * @return object Data attached to the node that is returned by * JX.Stratcom.getData(). * @task meta */ addData : function(node, data) { if (__DEV__) { if (!node || !node.getAttribute) { JX.$E( 'JX.Stratcom.addData(, ...): ' + 'node is not an element. Most likely, you\'re passing window or ' + 'document, which are not elements and can\'t have sigils.'); } if (!data || typeof data != 'object') { JX.$E( 'JX.Stratcom.addData(..., ): ' + 'data to attach to node is not an object. You must use ' + 'objects, not primitives, for metadata.'); } } return JX.copy(JX.Stratcom.getData(node), data); }, /** * @task internal */ allocateMetadataBlock : function() { return this._dataBlock++; } } }); diff --git a/webroot/rsrc/externals/javelin/core/install.js b/webroot/rsrc/externals/javelin/core/install.js index 9f492569f8..284d529be4 100644 --- a/webroot/rsrc/externals/javelin/core/install.js +++ b/webroot/rsrc/externals/javelin/core/install.js @@ -1,459 +1,455 @@ /** * @requires javelin-util * javelin-magical-init * @provides javelin-install * * @javelin-installs JX.install * @javelin-installs JX.createClass * * @javelin */ /** * Install a class into the Javelin ("JX") namespace. The first argument is the * name of the class you want to install, and the second is a map of these * attributes (all of which are optional): * * - ##construct## //(function)// Class constructor. If you don't provide one, * one will be created for you (but it will be very boring). * - ##extend## //(string)// The name of another JX-namespaced class to extend * via prototypal inheritance. * - ##members## //(map)// A map of instance methods and properties. * - ##statics## //(map)// A map of static methods and properties. * - ##initialize## //(function)// A function which will be run once, after * this class has been installed. * - ##properties## //(map)// A map of properties that should have instance * getters and setters automatically generated for them. The key is the * property name and the value is its default value. For instance, if you * provide the property "size", the installed class will have the methods * "getSize()" and "setSize()". It will **NOT** have a property ".size" * and no guarantees are made about where install is actually chosing to * store the data. The motivation here is to let you cheaply define a * stable interface and refine it later as necessary. * - ##events## //(list)// List of event types this class is capable of * emitting. * * For example: * * JX.install('Dog', { * construct : function(name) { * this.setName(name); * }, * members : { * bark : function() { * // ... * } * }, * properites : { * name : null, * } * }); * * This creates a new ##Dog## class in the ##JX## namespace: * * var d = new JX.Dog(); * d.bark(); * * Javelin classes are normal Javascript functions and generally behave in * the expected way. Some properties and methods are automatically added to * all classes: * * - ##instance.__id__## Globally unique identifier attached to each instance. * - ##prototype.__class__## Reference to the class constructor. * - ##constructor.__path__## List of path tokens used emit events. It is * probably never useful to access this directly. * - ##constructor.__readable__## Readable class name. You could use this * for introspection. * - ##constructor.__events__## //DEV ONLY!// List of events supported by * this class. * - ##constructor.listen()## Listen to all instances of this class. See * @{JX.Base}. * - ##instance.listen()## Listen to one instance of this class. See * @{JX.Base}. * - ##instance.invoke()## Invoke an event from an instance. See @{JX.Base}. * * * @param string Name of the class to install. It will appear in the JX * "namespace" (e.g., JX.Pancake). * @param map Map of properties, see method documentation. * @return void - * - * @group install */ JX.install = function(new_name, new_junk) { // If we've already installed this, something is up. if (new_name in JX) { if (__DEV__) { JX.$E( 'JX.install("' + new_name + '", ...): ' + 'trying to reinstall something that has already been installed.'); } return; } if (__DEV__) { if ('name' in new_junk) { JX.$E( 'JX.install("' + new_name + '", {"name": ...}): ' + 'trying to install with "name" property.' + 'Either remove it or call JX.createClass directly.'); } } // Since we may end up loading things out of order (e.g., Dog extends Animal // but we load Dog first) we need to keep a list of things that we've been // asked to install but haven't yet been able to install around. (JX.install._queue || (JX.install._queue = [])).push([new_name, new_junk]); var name; do { var junk; var initialize; name = null; for (var ii = 0; ii < JX.install._queue.length; ++ii) { junk = JX.install._queue[ii][1]; if (junk.extend && !JX[junk.extend]) { // We need to extend something that we haven't been able to install // yet, so just keep this in queue. continue; } // Install time! First, get this out of the queue. name = JX.install._queue.splice(ii, 1)[0][0]; --ii; if (junk.extend) { junk.extend = JX[junk.extend]; } initialize = junk.initialize; delete junk.initialize; junk.name = 'JX.' + name; JX[name] = JX.createClass(junk); if (initialize) { if (JX['Stratcom'] && JX['Stratcom'].ready) { initialize.apply(null); } else { // This is a holding queue, defined in init.js. JX['install-init'](initialize); } } } // In effect, this exits the loop as soon as we didn't make any progress // installing things, which means we've installed everything we have the // dependencies for. } while (name); }; /** * Creates a class from a map of attributes. Requires ##extend## property to * be an actual Class object and not a "String". Supports ##name## property * to give the created Class a readable name. * * @see JX.install for description of supported attributes. * * @param junk Map of properties, see method documentation. * @return function Constructor of a class created - * - * @group install */ JX.createClass = function(junk) { var name = junk.name || ''; var k; var ii; if (__DEV__) { var valid = { construct : 1, statics : 1, members : 1, extend : 1, properties : 1, events : 1, name : 1 }; for (k in junk) { if (!(k in valid)) { JX.$E( 'JX.createClass("' + name + '", {"' + k + '": ...}): ' + 'trying to create unknown property `' + k + '`.'); } } if (junk.constructor !== {}.constructor) { JX.$E( 'JX.createClass("' + name + '", {"constructor": ...}): ' + 'property `constructor` should be called `construct`.'); } } // First, build the constructor. If construct is just a function, this // won't change its behavior (unless you have provided a really awesome // function, in which case it will correctly punish you for your attempt // at creativity). var Class = (function(name, junk) { var result = function() { this.__id__ = '__obj__' + (++JX.install._nextObjectID); return (junk.construct || junk.extend || JX.bag).apply(this, arguments); // TODO: Allow mixins to initialize here? // TODO: Also, build mixins? }; if (__DEV__) { var inner = result; result = function() { if (this == window || this == JX) { JX.$E( '<' + Class.__readable__ + '>: ' + 'Tried to construct an instance without the "new" operator.'); } return inner.apply(this, arguments); }; } return result; })(name, junk); Class.__readable__ = name; // Copy in all the static methods and properties. for (k in junk.statics) { // Can't use JX.copy() here yet since it may not have loaded. Class[k] = junk.statics[k]; } var proto; if (junk.extend) { var Inheritance = function() {}; Inheritance.prototype = junk.extend.prototype; proto = Class.prototype = new Inheritance(); } else { proto = Class.prototype = {}; } proto.__class__ = Class; var setter = function(prop) { return function(v) { this[prop] = v; return this; }; }; var getter = function(prop) { return function(v) { return this[prop]; }; }; // Build getters and setters from the `prop' map. for (k in (junk.properties || {})) { var base = k.charAt(0).toUpperCase() + k.substr(1); var prop = '__auto__' + k; proto[prop] = junk.properties[k]; proto['set' + base] = setter(prop); proto['get' + base] = getter(prop); } if (__DEV__) { // Check for aliasing in default values of members. If we don't do this, // you can run into a problem like this: // // JX.install('List', { members : { stuff : [] }}); // // var i_love = new JX.List(); // var i_hate = new JX.List(); // // i_love.stuff.push('Psyduck'); // I love psyduck! // JX.log(i_hate.stuff); // Show stuff I hate. // // This logs ["Psyduck"] because the push operation modifies // JX.List.prototype.stuff, which is what both i_love.stuff and // i_hate.stuff resolve to. To avoid this, set the default value to // null (or any other scalar) and do "this.stuff = [];" in the // constructor. for (var member_name in junk.members) { if (junk.extend && member_name[0] == '_') { JX.$E( 'JX.createClass("' + name + '", ...): ' + 'installed member "' + member_name + '" must not be named with ' + 'a leading underscore because it is in a subclass. Variables ' + 'are analyzed and crushed one file at a time, and crushed ' + 'member variables in subclasses alias crushed member variables ' + 'in superclasses. Remove the underscore, refactor the class so ' + 'it does not extend anything, or fix the minifier to be ' + 'capable of safely crushing subclasses.'); } var member_value = junk.members[member_name]; if (typeof member_value == 'object' && member_value !== null) { JX.$E( 'JX.createClass("' + name + '", ...): ' + 'installed member "' + member_name + '" is not a scalar or ' + 'function. Prototypal inheritance in Javascript aliases object ' + 'references across instances so all instances are initialized ' + 'to point at the exact same object. This is almost certainly ' + 'not what you intended. Make this member static to share it ' + 'across instances, or initialize it in the constructor to ' + 'prevent reference aliasing and give each instance its own ' + 'copy of the value.'); } } } // This execution order intentionally allows you to override methods // generated from the "properties" initializer. for (k in junk.members) { proto[k] = junk.members[k]; } // IE does not enumerate some properties on objects var enumerables = JX.install._enumerables; if (junk.members && enumerables) { ii = enumerables.length; while (ii--){ var property = enumerables[ii]; if (junk.members[property]) { proto[property] = junk.members[property]; } } } // Build this ridiculous event model thing. Basically, this defines // two instance methods, invoke() and listen(), and one static method, // listen(). If you listen to an instance you get events for that // instance; if you listen to a class you get events for all instances // of that class (including instances of classes which extend it). // // This is rigged up through Stratcom. Each class has a path component // like "class:Dog", and each object has a path component like // "obj:23". When you invoke on an object, it emits an event with // a path that includes its class, all parent classes, and its object // ID. // // Calling listen() on an instance listens for just the object ID. // Calling listen() on a class listens for that class's name. This // has the effect of working properly, but installing them is pretty // messy. var parent = junk.extend || {}; var old_events = parent.__events__; var new_events = junk.events || []; var has_events = old_events || new_events.length; if (has_events) { var valid_events = {}; // If we're in dev, we build up a list of valid events (for this class // and our parent class), and then check them on listen and invoke. if (__DEV__) { for (var key in old_events || {}) { valid_events[key] = true; } for (ii = 0; ii < new_events.length; ++ii) { valid_events[junk.events[ii]] = true; } } Class.__events__ = valid_events; // Build the class name chain. Class.__name__ = 'class:' + name; var ancestry = parent.__path__ || []; Class.__path__ = ancestry.concat([Class.__name__]); proto.invoke = function(type) { if (__DEV__) { if (!(type in this.__class__.__events__)) { JX.$E( this.__class__.__readable__ + '.invoke("' + type + '", ...): ' + 'invalid event type. Valid event types are: ' + JX.keys(this.__class__.__events__).join(', ') + '.'); } } // Here and below, this nonstandard access notation is used to mask // these callsites from the static analyzer. JX.Stratcom is always // available by the time we hit these execution points. return JX['Stratcom'].invoke( 'obj:' + type, this.__class__.__path__.concat([this.__id__]), {args : JX.$A(arguments).slice(1)}); }; proto.listen = function(type, callback) { if (__DEV__) { if (!(type in this.__class__.__events__)) { JX.$E( this.__class__.__readable__ + '.listen("' + type + '", ...): ' + 'invalid event type. Valid event types are: ' + JX.keys(this.__class__.__events__).join(', ') + '.'); } } return JX['Stratcom'].listen( 'obj:' + type, this.__id__, JX.bind(this, function(e) { return callback.apply(this, e.getData().args); })); }; Class.listen = function(type, callback) { if (__DEV__) { if (!(type in this.__events__)) { JX.$E( this.__readable__ + '.listen("' + type + '", ...): ' + 'invalid event type. Valid event types are: ' + JX.keys(this.__events__).join(', ') + '.'); } } return JX['Stratcom'].listen( 'obj:' + type, this.__name__, JX.bind(this, function(e) { return callback.apply(this, e.getData().args); })); }; } else if (__DEV__) { var error_message = 'class does not define any events. Pass an "events" property to ' + 'JX.createClass() to define events.'; Class.listen = Class.listen || function() { JX.$E( this.__readable__ + '.listen(...): ' + error_message); }; Class.invoke = Class.invoke || function() { JX.$E( this.__readable__ + '.invoke(...): ' + error_message); }; proto.listen = proto.listen || function() { JX.$E( this.__class__.__readable__ + '.listen(...): ' + error_message); }; proto.invoke = proto.invoke || function() { JX.$E( this.__class__.__readable__ + '.invoke(...): ' + error_message); }; } return Class; }; JX.install._nextObjectID = 0; JX.flushHoldingQueue('install', JX.install); (function() { // IE does not enter this loop. for (var i in {toString: 1}) { return; } JX.install._enumerables = [ 'toString', 'hasOwnProperty', 'valueOf', 'isPrototypeOf', 'propertyIsEnumerable', 'toLocaleString', 'constructor' ]; })(); diff --git a/webroot/rsrc/externals/javelin/core/util.js b/webroot/rsrc/externals/javelin/core/util.js index efd4b7c39a..0d0d8f0b3e 100644 --- a/webroot/rsrc/externals/javelin/core/util.js +++ b/webroot/rsrc/externals/javelin/core/util.js @@ -1,366 +1,346 @@ /** * Javelin utility functions. * * @provides javelin-util * * @javelin-installs JX.$E * @javelin-installs JX.$A * @javelin-installs JX.$AX * @javelin-installs JX.isArray * @javelin-installs JX.copy * @javelin-installs JX.bind * @javelin-installs JX.bag * @javelin-installs JX.keys * @javelin-installs JX.log * @javelin-installs JX.id * @javelin-installs JX.now * * @javelin */ /** * Throw an exception and attach the caller data in the exception. * * @param string Exception message. - * - * @group util */ JX.$E = function(message) { var e = new Error(message); var caller_fn = JX.$E.caller; if (caller_fn) { e.caller_fn = caller_fn.caller; } throw e; }; /** * Convert an array-like object (usually ##arguments##) into a real Array. An * "array-like object" is something with a ##length## property and numerical * keys. The most common use for this is to let you call Array functions on the * magical ##arguments## object. * * JX.$A(arguments).slice(1); * * @param obj Array, or array-like object. * @return Array Actual array. - * - * @group util */ JX.$A = function(object) { // IE8 throws "JScript object expected" when trying to call // Array.prototype.slice on a NodeList, so just copy items one by one here. var r = []; for (var ii = 0; ii < object.length; ii++) { r.push(object[ii]); } return r; }; /** * Cast a value into an array, by wrapping scalars into singletons. If the * argument is an array, it is returned unmodified. If it is a scalar, an array * with a single element is returned. For example: * * JX.$AX([3]); // Returns [3]. * JX.$AX(3); // Returns [3]. * * Note that this function uses a @{function:JX.isArray} check whether or not * the argument is an array, so you may need to convert array-like objects (such * as ##arguments##) into real arrays with @{function:JX.$A}. * * This function is mostly useful to create methods which accept either a * value or a list of values. * * @param wild Scalar or Array. * @return Array If the argument was a scalar, an Array with the argument as * its only element. Otherwise, the original Array. - * - * @group util */ JX.$AX = function(maybe_scalar) { return JX.isArray(maybe_scalar) ? maybe_scalar : [maybe_scalar]; }; /** * Checks whether a value is an array. * * JX.isArray(['an', 'array']); // Returns true. * JX.isArray('Not an Array'); // Returns false. * * @param wild Any value. * @return bool true if the argument is an array, false otherwise. - * - * @group util */ JX.isArray = Array.isArray || function(maybe_array) { return Object.prototype.toString.call(maybe_array) == '[object Array]'; }; /** * Copy properties from one object to another. If properties already exist, they * are overwritten. * * var cat = { * ears: 'clean', * paws: 'clean', * nose: 'DIRTY OH NOES' * }; * var more = { * nose: 'clean', * tail: 'clean' * }; * * JX.copy(cat, more); * * // cat is now: * // { * // ears: 'clean', * // paws: 'clean', * // nose: 'clean', * // tail: 'clean' * // } * * NOTE: This function does not copy the ##toString## property or anything else * which isn't enumerable or is somehow magic or just doesn't work. But it's * usually what you want. * * @param obj Destination object, which properties should be copied to. * @param obj Source object, which properties should be copied from. * @return obj Modified destination object. - * - * @group util */ JX.copy = function(copy_dst, copy_src) { for (var k in copy_src) { copy_dst[k] = copy_src[k]; } return copy_dst; }; /** * Create a function which invokes another function with a bound context and * arguments (i.e., partial function application) when called; king of all * functions. * * Bind performs context binding (letting you select what the value of ##this## * will be when a function is invoked) and partial function application (letting * you create some function which calls another one with bound arguments). * * = Context Binding = * * Normally, when you call ##obj.method()##, the magic ##this## object will be * the ##obj## you invoked the method from. This can be undesirable when you * need to pass a callback to another function. For instance: * * COUNTEREXAMPLE * var dog = new JX.Dog(); * dog.barkNow(); // Makes the dog bark. * * JX.Stratcom.listen('click', 'bark', dog.barkNow); // Does not work! * * This doesn't work because ##this## is ##window## when the function is * later invoked; @{method:JX.Stratcom.listen} does not know about the context * object ##dog##. The solution is to pass a function with a bound context * object: * * var dog = new JX.Dog(); * var bound_function = JX.bind(dog, dog.barkNow); * * JX.Stratcom.listen('click', 'bark', bound_function); * * ##bound_function## is a function with ##dog## bound as ##this##; ##this## * will always be ##dog## when the function is called, no matter what * property chain it is invoked from. * * You can also pass ##null## as the context argument to implicitly bind * ##window##. * * = Partial Function Application = * * @{function:JX.bind} also performs partial function application, which allows * you to bind one or more arguments to a function. For instance, if we have a * simple function which adds two numbers: * * function add(a, b) { return a + b; } * add(3, 4); // 7 * * Suppose we want a new function, like this: * * function add3(b) { return 3 + b; } * add3(4); // 7 * * Instead of doing this, we can define ##add3()## in terms of ##add()## by * binding the value ##3## to the ##a## argument: * * var add3_bound = JX.bind(null, add, 3); * add3_bound(4); // 7 * * Zero or more arguments may be bound in this way. This is particularly useful * when using closures in a loop: * * COUNTEREXAMPLE * for (var ii = 0; ii < button_list.length; ii++) { * button_list[ii].onclick = function() { * JX.log('You clicked button number '+ii+'!'); // Fails! * }; * } * * This doesn't work; all the buttons report the highest number when clicked. * This is because the local ##ii## is captured by the closure. Instead, bind * the current value of ##ii##: * * var func = function(button_num) { * JX.log('You clicked button number '+button_num+'!'); * } * for (var ii = 0; ii < button_list.length; ii++) { * button_list[ii].onclick = JX.bind(null, func, ii); * } * * @param obj|null Context object to bind as ##this##. * @param function Function to bind context and arguments to. * @param ... Zero or more arguments to bind. * @return function New function which invokes the original function with * bound context and arguments when called. - * - * @group util */ JX.bind = function(context, func, more) { if (__DEV__) { if (typeof func != 'function') { JX.$E( 'JX.bind(context, , ...): '+ 'Attempting to bind something that is not a function.'); } } var bound = JX.$A(arguments).slice(2); if (func.bind) { return func.bind.apply(func, [context].concat(bound)); } return function() { return func.apply(context || window, bound.concat(JX.$A(arguments))); }; }; /** * "Bag of holding"; function that does nothing. Primarily, it's used as a * placeholder when you want something to be callable but don't want it to * actually have an effect. * * @return void - * - * @group util */ JX.bag = function() { // \o\ \o/ /o/ woo dance party }; /** * Convert an object's keys into a list. For example: * * JX.keys({sun: 1, moon: 1, stars: 1}); // Returns: ['sun', 'moon', 'stars'] * * @param obj Object to retrieve keys from. * @return list List of keys. - * - * @group util */ JX.keys = Object.keys || function(obj) { var r = []; for (var k in obj) { r.push(k); } return r; }; /** * Identity function; returns the argument unmodified. This is primarily useful * as a placeholder for some callback which may transform its argument. * * @param wild Any value. * @return wild The passed argument. - * - * @group util */ JX.id = function(any) { return any; }; JX.log = JX.bag; if (__DEV__) { if (!window.console || !window.console.log) { if (window.opera && window.opera.postError) { window.console = {log: function(m) { window.opera.postError(m); }}; } else { window.console = {log: function(m) { }}; } } /** * Print a message to the browser debugging console (like Firebug). This * method exists only in ##__DEV__##. * * @param string Message to print to the browser debugging console. * @return void - * - * @group util */ JX.log = function(message) { window.console.log(message); }; window.alert = (function(native_alert) { var recent_alerts = []; var in_alert = false; return function(msg) { if (in_alert) { JX.log( 'alert(...): '+ 'discarded reentrant alert.'); return; } in_alert = true; recent_alerts.push(JX.now()); if (recent_alerts.length > 3) { recent_alerts.splice(0, recent_alerts.length - 3); } if (recent_alerts.length >= 3 && (recent_alerts[recent_alerts.length - 1] - recent_alerts[0]) < 5000) { if (confirm(msg + "\n\nLots of alert()s recently. Kill them?")) { window.alert = JX.bag; } } else { // Note that we can't .apply() the IE6 version of this "function". native_alert(msg); } in_alert = false; }; })(window.alert); } /** * Date.now is the fastest timestamp function, but isn't supported by every * browser. This gives the fastest version the environment can support. * The wrapper function makes the getTime call even slower, but benchmarking * shows it to be a marginal perf loss. Considering how small of a perf * difference this makes overall, it's not really a big deal. The primary * reason for this is to avoid hacky "just think of the byte savings" JS * like +new Date() that has an unclear outcome for the unexposed. * * @return Int A Unix timestamp of the current time on the local machine */ JX.now = (Date.now || function() { return new Date().getTime(); }); diff --git a/webroot/rsrc/externals/javelin/docs/Base.js b/webroot/rsrc/externals/javelin/docs/Base.js index d2352e9c99..0d32239608 100644 --- a/webroot/rsrc/externals/javelin/docs/Base.js +++ b/webroot/rsrc/externals/javelin/docs/Base.js @@ -1,70 +1,69 @@ /** * @requires javelin-install * @javelin */ /** * This is not a real class, but @{function:JX.install} provides several methods * which exist on all Javelin classes. This class documents those methods. * * @task events Builtin Events - * @group install */ JX.install('Base', { members : { /** * Invoke a class event, notifying all listeners. You must declare the * events your class invokes when you install it; see @{function:JX.install} * for documentation. Any arguments you provide will be passed to listener * callbacks. * * @param string Event type, must be declared when class is * installed. * @param ... Zero or more arguments. * * @return @{JX.Event} Event object which was dispatched. * @task events */ invoke : function(type, more) { // // }, /** * Listen for events emitted by this object instance. You can also use * the static flavor of this method to listen to events emitted by any * instance of this object. * * See also @{method:JX.Stratcom.listen}. * * @param string Type of event to listen for. * @param function Function to call when this event occurs. * @return object A reference to the installed listener. You can later * remove the listener by calling this object's remove() * method. * @task events */ listen : function(type, callback) { // // } }, statics : { /** * Static listen interface for listening to events produced by any instance * of this class. See @{method:listen} for documentation. * * @param string Type of event to listen for. * @param function Function to call when this event occurs. * @return object A reference to the installed listener. You can later * remove the listener by calling this object's remove() * method. * @task events */ listen : function(type, callback) { // // } } }); diff --git a/webroot/rsrc/externals/javelin/docs/onload.js b/webroot/rsrc/externals/javelin/docs/onload.js index 7c76c08598..61865b94de 100644 --- a/webroot/rsrc/externals/javelin/docs/onload.js +++ b/webroot/rsrc/externals/javelin/docs/onload.js @@ -1,21 +1,20 @@ /** * @javelin */ /** * Register a callback for invocation after DOMContentReady. * * NOTE: Although it isn't private, use of this function is heavily discouraged. * See @{article:Concepts: Behaviors} for information on using behaviors to * structure and invoke glue code. * * This function is defined as a side effect of init.js. * * @param function Callback function to invoke after DOMContentReady. * @return void - * @group util */ JX.onload = function(callback) { // This isn't the real function definition, it's only defined here to let the // documentation generator find it. The actual definition is in init.js. }; diff --git a/webroot/rsrc/externals/javelin/lib/DOM.js b/webroot/rsrc/externals/javelin/lib/DOM.js index dea3d880f6..60ac09603f 100644 --- a/webroot/rsrc/externals/javelin/lib/DOM.js +++ b/webroot/rsrc/externals/javelin/lib/DOM.js @@ -1,965 +1,955 @@ /** * @requires javelin-magical-init * javelin-install * javelin-util * javelin-vector * javelin-stratcom * @provides javelin-dom * * @javelin-installs JX.$ * @javelin-installs JX.$N * @javelin-installs JX.$H * * @javelin */ /** * Select an element by its "id" attribute, like ##document.getElementById()##. * For example: * * var node = JX.$('some_id'); * * This will select the node with the specified "id" attribute: * * LANG=HTML *
    ...
    * * If the specified node does not exist, @{JX.$()} will throw an exception. * * For other ways to select nodes from the document, see @{JX.DOM.scry()} and * @{JX.DOM.find()}. * * @param string "id" attribute to select from the document. * @return Node Node with the specified "id" attribute. - * - * @group dom */ JX.$ = function(id) { if (__DEV__) { if (!id) { JX.$E('Empty ID passed to JX.$()!'); } } var node = document.getElementById(id); if (!node || (node.id != id)) { if (__DEV__) { if (node && (node.id != id)) { JX.$E( 'JX.$("'+id+'"): '+ 'document.getElementById() returned an element without the '+ 'correct ID. This usually means that the element you are trying '+ 'to select is being masked by a form with the same value in its '+ '"name" attribute.'); } } JX.$E("JX.$('" + id + "') call matched no nodes."); } return node; }; /** * Upcast a string into an HTML object so it is treated as markup instead of * plain text. See @{JX.$N} for discussion of Javelin's security model. Every * time you call this function you potentially open up a security hole. Avoid * its use wherever possible. * * This class intentionally supports only a subset of HTML because many browsers * named "Internet Explorer" have awkward restrictions around what they'll * accept for conversion to document fragments. Alter your datasource to emit * valid HTML within this subset if you run into an unsupported edge case. All * the edge cases are crazy and you should always be reasonably able to emit * a cohesive tag instead of an unappendable fragment. * * You may use @{JX.$H} as a shortcut for creating new JX.HTML instances: * * JX.$N('div', {}, some_html_blob); // Treat as string (safe) * JX.$N('div', {}, JX.$H(some_html_blob)); // Treat as HTML (unsafe!) * * @task build String into HTML * @task nodes HTML into Nodes - * - * @group dom */ JX.install('HTML', { construct : function(str) { if (str instanceof JX.HTML) { this._content = str._content; return; } if (__DEV__) { if ((typeof str !== 'string') && (!str || !str.match)) { JX.$E( 'new JX.HTML(): ' + 'call initializes an HTML object with an empty value.'); } var tags = ['legend', 'thead', 'tbody', 'tfoot', 'column', 'colgroup', 'caption', 'tr', 'th', 'td', 'option']; var evil_stuff = new RegExp('^\\s*<(' + tags.join('|') + ')\\b', 'i'); var match = str.match(evil_stuff); if (match) { JX.$E( 'new JX.HTML("<' + match[1] + '>..."): ' + 'call initializes an HTML object with an invalid partial fragment ' + 'and can not be converted into DOM nodes. The enclosing tag of an ' + 'HTML content string must be appendable to a document fragment. ' + 'For example, is allowed but or are not.'); } var really_evil = /..."): ' + 'call initializes an HTML object with an embedded script tag! ' + 'Are you crazy?! Do NOT do this!!!'); } var wont_work = /..."): ' + 'call initializes an HTML object with an embedded tag. IE ' + 'will not do the right thing with this.'); } // TODO(epriestley): May need to deny