Eclipse PDT - Include Paths
December 28th, 2008 by Aaron
When troubleshooting a different problem the other day, I re-entered the world of eclipse include paths. For those of you who are not familiar, the eclipse include path is located as the last option of a eclipse PDT PHP project.
What is the Include Paths option used for?
While it wasn’t so clear to me, trial and error seemed to be my friend. It turns out, most of my PHP projects that I built, all of my files were already included inside of the project, so I never had to use this option. However, when using other external system level libraries, like PEAR or Cake, you may not build your project with these directories included. However, your code may still use parts of these libraries. In order to provide proper auto completion and error checking, you can add these external libraries to the include paths option.
What the Include Paths option is NOT
This is not a direct replacement for your include path in your php.ini. Remember, when writing and testing code, make sure to use the same version of PHP - as well as the near exact same php.ini file - as you do on production. The Include Paths option is just used to make sure that auto completion and in IDE debugging (not to be confused with XDebug type debugging) works correctly. You may even want to change your default php binary to your normal php binary if the one that came with the PDT install is not your exact configuration.
So, how does this thing work?
First, create your project. Next, right click on the Include Paths option and choose “Configure Include Path”.
You will see two options, Add Variable… and Add External Folder…
Add Variable…
The add variable option allows you to add PHP path variables in. You can configure this with the Window -> Preferences method - or just click the Configure Variables button at the bottom of the Add Variable option screen. For example, if you do a lot of PEAR programming, you might have a variable for PEAR available here.
Add External Folder…
This options is nice for third party php libraries that you may have on another drive or you might be accessing remotely. For example, I’ve mapped my E: drive on windows to an OpenID library that I use. Sometimes I need to include this with my projects when I’m using this third party tool.
Do I really need these?
Like I said, for the longest time, I never used the include paths. But, I started using more third party libraries and not wanting to include them in my main repository - and then this tool became useful. Try it out - and see if its useful for you.
SPL Documentation - Standard PHP Library
December 5th, 2008 by Aaron
Great documentation site for SPL - check it out immediately!!
PDO - can you handle identical prepared statements?
December 1st, 2008 by Aaron
I’ve been wondering if I should be concerned about re-preparing a prepared statement when using PDO. Right now, I use code like this when preparing a statement:
1 2 3 4 5 6 7 8 9 10 | public function prep($statement) { if ($statement != $this->_lastPrepared) { /** * Store our clear text statement, and then our object */ $this->_lastPrepared = $statement; $this->_ps = $this->db->prepare($statement); } } |
I end up storing the last statement and do a quick compare. My concern comes from MySQL’s admission of this:
“If a prepared statement with the given name already exists, it is deallocated implicitly before the new statement is prepared.”
So, not knowing the internal workings of PDO, I wonder how they handle it. Do they…
- Create each prepared statement with the same name, causing them to be deallocated each time
- Create each one with a random name, so that there are never any deallocations unless you unset the statement?
Anyone have any insight?
The Observer Pattern in PHP: Refactored
November 22nd, 2008 by Aaron
You may remember the article I wrote about the observer pattern in php - but it lacked some of PHP’s advanced features.
In this next example, I’m not going to explain the logic as much - read the original post for more - but I did comment it pretty thoroughly. Here are the things that I added, however:
- Type hinting in functions
- Abstract classes
- Interfaces
- A registry of observers
So, here is the code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 | <?php /** * twitter transport from "library" - on successful tweet will print it out */ class twitterTransport { public function __construct() { /** logic here **/ } public function tweet(twitterMessage $object) { /** logic here **/ print $object->message . ' was just tweeted'; } } /** * url shortener from "library" - just replaces [url] with [u] in a string */ class urlShortener { public function shorten($message) { return str_replace('[url]', '[u]', (string) $message); } } /** * twitter message class - very simple - just holds the message inside itself as a string */ class twitterMessage { public $message = ''; public function __construct($message) { $this->message = (string) $message; } } /** * Observer for shortening URLs * * This class gets envoked when there is a PREPOST action, and invokes * the urlShortener::shorten() against it * @see iObserver */ class urlShortenerObserver implements iObserver { /** * Tells the type of observing this will do * @return string */ public static function getType() { return 'PREPOST'; } /** * The notification method - is called when a PREPOST is done. shortens url * @param twitterMessage $object */ public function notify($object) { $urlShortener = new urlShortener(); $object->message = $urlShortener->shorten($object->message); } } /** * The twitter message sender, is observable, sends a message with the postMessage * function * @see observable */ class twitterTransportObservable extends observable { /** * post a message to twitter, notifying any observers * @param twitterMessage $object * @see twitterTransport */ public function postMessage(twitterMessage $object) { $this->_notify('PREPOST', $object); $sender = new twitterTransport(); $sender->tweet($object); $this->_notify('POSTED', $sender); } } /** * abstract observable class - extended for all observable items, contains the array * of observers, the register and the notify commands */ abstract class observable { /** * @var array the observer classes */ protected $_observers = array(); /** * used for registering observers, or adding them to the array * * Keep in mind, this adds them in the same order as they're added to * the array, so that may affect the final outcome * * @param string $type The type of observer, hook name * @param object $observer The observer class */ public function registerObserver($type, iObserver $observer) { if (empty($type)) throw new exception("type was empty when registering " . get_class($observer)); if (!isset($this->_observers[$type])) $this->_observers[$type] = array(); $this->_observers[$type][] = $observer; } /** * used to notify self of actions of a certain type, launches observers * * @param string $type The Type of observer, the hook * @param object $object The observer object */ protected function _notify($type, $object) { if (isset($this->_observers[$type])) { foreach ($this->_observers[$type] as $observer) { $observer->notify($object); } } } } /** * interface for any observer classes */ interface iObserver { public function notify($object); public static function getType(); } /** * launching code */ /** * global observers is an array of items which are shared in all launching software * tells which observers are associated with with observable */ $globalObservers = array('twitterTransportObservable'=>array('urlShortenerObserver')); $tweeter = new twitterTransportObservable(); /** * assign all observers from our globalObservers */ if (isset($globalObservers['twitterTransportObservable'])) { foreach ($globalObservers['twitterTransportObservable'] as $observer) { $tweeter->registerObserver(call_user_func(array($observer, 'getType')), new $observer); } } $message = new twitterMessage("this is my message [url]"); $tweeter->postMessage($message); |
How custom passphrases/pictures still don’t protect against phishing
November 20th, 2008 by Aaron
As you probably remember, I have lots of interest in phishing techniques (I talked about one here, and preventing them here). I’ve noticed a new trend: a dual stage login form with a custom picture or passphrase. Users are to gain trust in the login page because their custom configured option is displayed. The more I started thinking about this, however, I kept seeing an issue - this still can be easily phished! I’m going to demonstrate a method of phishing the passphrase version. I don’t want to do a picture example because it a) takes more code and b) more people have moved to that thinking it is more secure. Lets go:
First off, all phishing starts with getting the user to a login page not at the respected domain. So, lets just skip that step, and examine our login page. This will be a duplicate of our real site’s login page - note the reminder that they will have to verify their passphrase.
login.php @ fakedomain.com
1 2 3 4 5 | <form action="login2.php" method="post">
<label>Username: <input name="username" /><br />
<em>Remember, you will be asked to verify your passphrase on the next page.</em><br />
<input type="submit" value="Login" />
</form> |
Very simple login which sends it to another page - hopefully named the same as the real domain’s login page.
Lets look at the page we’ll be submitting to:
login2.php @ fakedomain.com
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /** cutting out a lot of code - make sure its not empty, etc **/ $args = array ('username'=>$_POST['username']); $uri = 'http://realdomain.com/login.do.php'; $opts = array('http'=>array('method'=>'POST', 'header'=>'Content-Type: application/x-www-form-urlencoded', 'content'=>http_build_query($args))); $context = stream_context_create($opts); $page_with_phrase = file_get_contents($uri, false, $context); $doc = new DomDocument(); $doc->loadHTML($page_with_phrase ); $passphrase = $doc->getElementById('passphrase_node')->nodeValue; /** next login form page actually shows the $passphrase phrase and asks for password **/ include('next_login_form.php'); |
Ok, first off, you’ll see we create a nice post with our stream context creation (detailed here) - so we basically send the username to the real domain as they had logged in. (Depending on the target site, you might also have to send referrers, cookies, etc - but we’re making it a really simple example here.)
We retrieve the page after a successful post of the username. This content should now contain the custom passphrase somewhere. For our example, there is a nicely named div or span with an id of ‘passphrase_node’. Probably, in real life, you’d have to use a complex xpath to get the actual value.
From then on, we just include our ’second’ login page which shows the passphrase, and then requests the password from the user. From there, you can do whatever you want.
Ok so…
Its nice to see that people are trying to eliminate phishing - but there still is only one real solution IMHO - and that is to educate users on the address bar (or get them to install a plugin that validates the page they’re on.).
Unofficial xdebug ini - with comments!
November 18th, 2008 by Aaron
I found a great article about the xdebug ini file - someone went through and added comments - much like the apache conf files. Amazingly well written.
Understanding the Observer Pattern in PHP
November 17th, 2008 by Aaron
For a while, I’ve been looking at plugin systems, but not really fully understanding the pattern behind them. Don’t get me wrong, I see how they work, but I didn’t know the reason why - the theory or pattern behind it. Well turns out, generally, they’re based upon the observer pattern. I decided to write my own observer pattern demonstration here.
Our example is going to be very simple: post a message to twitter. We’re not going to work with any credentials or anything, just want to post a message. I do want to add an observer that will shorten any url however. In this example, we’ll be making the logic classes stubs (you can create these later), and using ‘[url]‘ to stand for an URL we might replace.
Lets start in:
Our two logic classes
Remember, we’re just going to have some blank stub logic classes here. They are for demonstration purposes only.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class twitterTransport { public function __construct() { /** logic here **/ } public function tweet($object) { /** logic here **/ print $object->message . ' was just tweeted'; } } class urlShortener { public function shorten($message) { return str_replace('[url]', '[u]', $message); } } |
Pretty simple, the first class all it does is post to twitter with a public method called tweet(). This accepts an object of the twitter message (which we’ll list next!). It prints out the message so you know what we would have sent to twitter. The url shortening class - all it is is your logic to shorten urls inside of a message. In this case, pretty simple.
Ok - as promised, here is our twitter message class:
1 2 3 4 5 6 7 8 9 | class twitterMessage { public $message = ''; public function __construct($message) { $this->message = $message; } } |
The actual launching code
We’re going to jump a head here and show what code we’ll be using to add the url shorterner as well as post the message. It’s really short - but it’ll give us an idea of what class we need to create next:
1 2 3 4 | $tweeter = new twitterTransportObservable(); $tweeter->registerObserver('PREPOST', new urlShortenerObserver()); $message = new twitterMessage("this is my message [url]"); $tweeter->postMessage($message); |
Ok good. First off, we create a new instance of twitterTransportObservable. By the keyword Observable, we can tell that this class is something that will “do something we can watch” or observe. Any time a class is Observable, it has to have a method to add watchers to itself - or registerObserver(). In our example, we’re sending in a type - “PREPOST” - so before we post the message, and a new object.
The new object is of type urlShortenerObserver(). We can see that this will be a ‘watcher’ by the name. No more details are given here, so its methods must be used/exposed inside of the Observable class.
Next, we’re just making a new twitter message object, pretty simple.
Finally, we’re calling postMessage() sending in our twitter message. Remember, $tweeter is an instance of twitterTransportObservable, so there must be a method called twitterTransportObservable::postMessage().
So far so good.
Looking at the Observable Class
So now we know we need to build twitterTransportObservable. I’m going to post the code here, but don’t worry, we’ll take it apart, step by step:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class twitterTransportObservable { protected $_observers = array(); public function registerObserver($type, $observer) { if (!isset($this->_observers[$type])) $this->_observers[$type] = array(); $this->_observers[$type][] = $observer; } public function postMessage($object) { $this->_notify('PREPOST', $object); $sender = new twitterTransport(); $sender->tweet($object); $this->_notify('POSTED', $sender); } protected function _notify($type, $object) { if (isset($this->_observers[$type])) { foreach ($this->_observers[$type] as $observer) { $observer->notify($object); } } } } |
Ok - pretty big - lets go slow:
First off, we have the $_observers. Since this class is of type observable, we know it has watchers. Well, it has to be aware of its watchers, because it registers them… so we need an array to hold all of our watchers, or $_observers.
The first method is registerObserver(). You’ll see this takes in a variable called $type and a variable called $object. Well, we saw this used in our launching code. It appears that this was started with ‘PREPOST’ as the $type and a new urlShortenerObserver as the $object. Moving along, the $_observers array is keyed by $type - so we just did some good programming: if the key is not set, set it by creating an empty array. Finally, the next line grabs that array, and adds the passed in $object to the internal array of $_observers. So now, our observable class has its first observer. An important thing to note is that the order you add them using registerObserver(), is the order they will remain in the array in the Observable class.
Quick reminder: Remember, objects are passed by reference!
Next, we have the postMessage() function - which takes in an object of a twitter message. The first thing the function does is notify our self that we’re PREPOST, while passing in the $object to that notify call. Think of this as the ‘hook’ - or someone yelling at the watchers saying “Anyone of type PREPOST, I’ve got this $object for you to deal with!”. Next, this function creates a new twitterTransport and tweets the object. Remember, the $object has now returned from the notify call and may be changed. Finally, there is another call to _notify with “POSTED” as the type. This is just for example, our example doesn’t really need this. But, imagine you created an observer which logged the output of twitter’s response to your post? This would be perfect for that hook.
Ok, so the last thing we have to look at is the _notify() function - which we’ve called a few times during our postMessage(). This simply looks to see if there is an observer that we’ve been storing locally keyed on the $type key. If there is a key of this $type, we loop through each observer of that $type, and pass in our object to its notify() function. OK - don’t get confused, that notify() function is different than our _notify() function. It belongs to the observer (in our example, urlShortenerObserver::notify()). So basically, it calls all the observer’s notify() with a reference to the object, and its done.
Whew, that was a lot - but we have one more part left:
The Observer class
We have another class that is used to observe or watch the observable classes. In this case, we wanted to have any URLs shortened before we posted a message to twitter… so we registered this observer with PREPOST. During the Observable’s _notify() function, we called this observerable class’s notify() method. So, lets finally take a look at the code:
1 2 3 4 5 6 7 8 | class urlShortenerObserver { public function notify($object) { $urlShortener = new urlShortener(); $object->message = $urlShortener->shorten($object->message); } } |
Pretty simple class. It has only one method, called notify() which accepts an object - of type twitterMessage. The first line just creates a new urlShortener() - you remember from way up top? Just a quick str_replace type method. Then, the next line accesses the urlShortender::shorten() method - by passing in the public $message variable of the twitterMessage. The return value is assigned to the twitterMessage::$message var. And remember, since objects are passed by reference, when the next line of the the observable’s class is called, the object will now be modified.
Wrapping Up
Ok - well this was a pretty simple example of this behavior. There are definitely more complex ways and more business logic intense scenarios to use the observer in. Another thing we didn’t do is use many of PHP’s OO properties - but we could always refactor and do that in the future.
All the code
In case you want to run it yourself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | <?php class twitterTransport { public function __construct() { /** logic here **/ } public function tweet($object) { /** logic here **/ print $object->message . ' was just tweeted'; } } class urlShortener { public function shorten($message) { return str_replace('[url]', '[u]', $message); } } class twitterMessage { public $message = ''; public function __construct($message) { $this->message = $message; } } class urlShortenerObserver { public function notify($object) { $urlShortener = new urlShortener(); $object->message = $urlShortener->shorten($object->message); } } class twitterTransportObservable { protected $_observers = array(); public function registerObserver($type, $observer) { if (!isset($this->_observers[$type])) $this->_observers[$type] = array(); $this->_observers[$type][] = $observer; } public function postMessage($object) { $this->_notify('PREPOST', $object); $sender = new twitterTransport(); $sender->tweet($object); $this->_notify('POSTED', $sender); } protected function _notify($type, $object) { if (isset($this->_observers[$type])) { foreach ($this->_observers[$type] as $observer) { $observer->notify($object); } } } } $tweeter = new twitterTransportObservable(); $tweeter->registerObserver('PREPOST', new urlShortenerObserver()); $message = new twitterMessage("this is my message [url]"); $tweeter->postMessage($message); |
Posting Requests in PHP without CURL
November 14th, 2008 by Aaron
Can it be done? YES! Luckily, functions like file_get_contents() support stream contexts.
In this example, I want to post to my form my login credentials of username “aaron” and password “chicken”. This will be posting to the URL of http://test.com/login.do.php. I’ll show the code first, and then lets talk about what it does.
1 2 3 4 5 6 7 8 9 10 | $args = array ('username'=>'aaron', 'password'=>'chicken'); $uri = 'http://test.com/login.do.php'; $opts = array('http'=>array('method'=>'POST', 'header'=>'Content-Type: application/x-www-form-urlencoded', 'content'=>http_build_query($args))); $context = stream_context_create($opts); $result = file_get_contents($uri, false, $context); print $result; |
Lets disect line by line:
The first line dealing with $args is setting up our post parameters. The next line is our uri target. If this was an HTML form, it might look like this:
1 2 3 4 5 | <form action="http://test.com/login.do.php" method="POST"> Username: <input name="username" /><br /> Password: <input name="password" /><br /> <input type="submit" /> </form> |
The next line is the $opts array. This will be the options that we send to the stream context. The array is keyed by the type of stream we’re creating here - in this case ‘http’. This points to an array of options. First, the method of the request, in this case “POST”. Next, the header that must be sent in order to submit the request. For the most part, your browser handles sending this - but we have to specify it here. It simply is keying the request to let it know that its a form submission. The final key is the content key - which is what is submitted in a typical request below the headers. Here we’re using PHP’s http_build_query() to save ourselves some time.
Moving on, we create a new context using stream_context_create(), assign that to $context using $opts as our parameter. Think of $context not as a value, but as a handle - similar to an fopen or other resource handle.
Finally, we retrieve the contents of our request using file_get_contents(). Do note: you must have fopen_url = true in your php.ini. This allows us to retrieve content via an external URL. We pass the location of our post request, false because we don’t want send any additional flags, and a pointer to our stream context.
After this is complete, we should have the output in the $result variable.
How to throw an AMAZING meeting
November 13th, 2008 by Aaron
I’ve been to far more meetings than any one on earth should ever have had to by the time they’re 25. With that experience, however, I can start to pick out key points of running a great meeting or demonstration. I know this is a different type of post for this blog, but I think its important. There may be times that you’re called on to conduct a meeting or demonstration, as a technical resource. Follow these tips and your meeting will go along smooth, effeciently and successfully.
Planning
Have an Outline
Outline the points you’re going to cover. Provide more elaboration on an outline that you refer to as your Internal outline. Examples of this include writing a more thorough document for yourself or using the notes section for your powerpoint presentation (simply print out the slides with notes for yourself, give everyone else the slides only.)
Send your outline out before the event. Gauge the busyness of your audience to figure out when you should send it out. Don’t send it out 10 minutes before the meeting if you have people who are traveling or may be in other meetings. Conversely, don’t send it out too many days in advance - it may get lost in the pile of other things your atendees have to review.
Test Technical Items
You should run through any technical demonstration at least once before you create the presentation. Everyone remembers Bill Gate’s famous BSOD during his demonstration of Windows - and I’m sure that was even a rehearsed demonstration. Try to avoid situations like this by testing your technical aspects ahead of time.
Arrival
Arrive Early
If you are either facilitating the meeting or participating in it, make sure to get there early. We’re not talking hours early - but you should aim to be the first person there. This will help you build confidence in arriving parties: they know you’re there, prepared, in control and ready to present to them. It can be an uncomfortable experience to arrive at a meeting when there is no one else in the room. Attendees might question that they’re in the right room, if they have the right time, etc. This also provides you time to verify that any presentation aides are available.
Intro on Time
Generally, when sending out a meeting invite, there is a concise summary of what the meeting is about. When the meeting is scheduled to begin, begin your introduction. This does two useful things for you: first, it reminds attendees who may be hopping from meeting to meeting what the content of this meeting will be. Second, it sends a message that you are in control of the meeting, and that attendees can trust that you will end the meeting on time - and that their time is not being wasted. Sometimes people take this to an extreme and start the content right at the time. Don’t do this! Sometimes things happen - allow for a little leeway - but make sure that the time counts. Your intro may be 1 to 2 minutes starting exactly at the meeting start time. Enough time for those who are trying to make it in on the wire to attend.
Present
Provide easy ways of feedback
Some attendees may feel too nervous to shout out a question during your presentation. Start out by telling them that you can either stop you with a question or get my attention by waving your hand or directing it at you. You may want to give a demonstration of casually getting someone’s attention. Then, be attentive! Sometimes your best questions will come from people who are too meek to make a huge presentation.
Ask Questions
Along the way, its fine to ask questions - or better yet - encourage questions. Summarize a point and ask if there is any clarification needed on that point. Its useful to remind people that they can ask questions - that this is an interactive presentation. Don’t be surprised if someone asks a questoin about the last point. Sometimes its easy to become presentation-tranced and forget to ask the question.
“No” Questions vs “Yes” Questions
This is particularly useful on telephone conversations - where you can’t see any visual cues. Try phrasing your questions to elicit answers only if the conditions are not satisfactory. So, for example, instead of saying “Does everybody understand what was just demonstrated?” - and waiting for people to overlap and possibly not answer or interupt each other, rephrase: “Is there anyone that does not understand what was just demonstrated? We can demonstrate it again.” You’ve provided an opening to receive only feedback from those who need it - while framing the question into a positive situation. An attendee might be scared to say that they missed something - but would be willing to agree with your suggestion to do the demonstration again.
Finishing
One more time - questions?
Ask for questions. Attendees may have been writing down questions, intent on not interupting your demonstration.
Thank them
Thank your attendees for their time. While you may have spent more time preparing and presenting, you did interupt their day to present your content. Presenting and serving someone else is a highly regarded activity - so thank them for the opportunity by recognizing their important contribution and time.
Explain and Send Followup
If any other materials were created during the presentation, or if it was recorded, tell your attendees about this. Make sure that they know that you will be sending it to them. Give a timeline and then stick with it!
Well that about sums up my tips I try to follow when organizing a meeting. Any others?
Name CSS Classes More Descriptive
October 30th, 2008 by Aaron
One thing I remember being pounded into my head is to not create CSS classes after their physical attributes. So, for example, if your error text is red, do not call the class red. Instead, be more descriptive of the content.
BAD!
1 | .red { color: red } |
1 | <span class="red">There was an error!</span> |
Instead, I was always encouraged to give the class something more descriptive, such as ‘error’.
GOOD!
1 | .error { color: red; } |
1 | <span class="error">There was an error!</span> |
Well, that seems pretty cut-n-dry for a simple example like that. However, in my most recent design, I’ve come across some more complex situations. For example, when you’re visiting the webpage, the background of an element might be a really dark grey. When you’re an authenticated user, however, I need it to be a medium grey (hey don’t ask - just wait for WhereIsTheBand.com to be done!).
Of course, during design, I went to the dark side right away:
BAD!
1 2 | .darkGrey { color: #101011 } .mediumGrey { color: #212122 } |
Now, I know I should come up with some descriptive name, perhaps something like “userLoggedIn” or something, but I plan on using these classes in different areas as well. They might not be dependent on the user being logged in - just might look better that way. I didn’t want to make a lot of duplicate CSS code either.
The compromise: be semi descriptive.
COMPROMISE!
1 2 | .lowContrastBackground { color: #101011 } .mediumContractBackground { color: #212122 } |
Not perfect, but seems like a better alternative.
